electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 6f2cd8b4f53272dd3695fb9677907fa3a29be028
parent d8180c678b9f476da9807556debf1974284e3825
Author: SomberNight <somber.night@protonmail.com>
Date:   Sun,  1 Mar 2020 09:14:50 +0100

Qt tx dialog: allow setting custom locktime

closes #2405
closes #1685

Diffstat:
Melectrum/bitcoin.py | 4++++
Aelectrum/gui/qt/locktimeedit.py | 173+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Melectrum/gui/qt/transaction_dialog.py | 38++++++++++++++++++++++++++++++++------
Melectrum/lnpeer.py | 4++--
4 files changed, 211 insertions(+), 8 deletions(-)

diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py @@ -44,6 +44,10 @@ COINBASE_MATURITY = 100 COIN = 100000000 TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000 +NLOCKTIME_MIN = 0 +NLOCKTIME_BLOCKHEIGHT_MAX = 500_000_000 - 1 +NLOCKTIME_MAX = 2 ** 32 - 1 + # supported types of transaction outputs # TODO kill these with fire TYPE_ADDRESS = 0 diff --git a/electrum/gui/qt/locktimeedit.py b/electrum/gui/qt/locktimeedit.py @@ -0,0 +1,173 @@ +# Copyright (C) 2020 The Electrum developers +# Distributed under the MIT software license, see the accompanying +# file LICENCE or http://www.opensource.org/licenses/mit-license.php + +import time +from datetime import datetime +from typing import Optional, Any + +from PyQt5.QtCore import Qt, QDateTime +from PyQt5.QtGui import QPalette, QPainter +from PyQt5.QtWidgets import (QWidget, QLineEdit, QStyle, QStyleOptionFrame, QComboBox, + QHBoxLayout, QDateTimeEdit) + +from electrum.i18n import _ +from electrum.bitcoin import NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX + +from .util import char_width_in_lineedit + + +class LockTimeEdit(QWidget): + + def __init__(self, parent=None): + QWidget.__init__(self, parent) + + hbox = QHBoxLayout() + self.setLayout(hbox) + hbox.setContentsMargins(0, 0, 0, 0) + hbox.setSpacing(0) + + self.locktime_raw_e = LockTimeRawEdit() + self.locktime_height_e = LockTimeHeightEdit() + self.locktime_date_e = LockTimeDateEdit() + self.editors = [self.locktime_raw_e, self.locktime_height_e, self.locktime_date_e] + + self.combo = QComboBox() + options = [_("Raw"), _("Block height"), _("Date")] + option_index_to_editor_map = { + 0: self.locktime_raw_e, + 1: self.locktime_height_e, + 2: self.locktime_date_e, + } + default_index = 1 + self.combo.addItems(options) + + def on_current_index_changed(i): + for w in self.editors: + w.setVisible(False) + w.setEnabled(False) + prev_locktime = self.editor.get_locktime() + self.editor = option_index_to_editor_map[i] + if self.editor.is_acceptable_locktime(prev_locktime): + self.editor.set_locktime(prev_locktime) + self.editor.setVisible(True) + self.editor.setEnabled(True) + + self.editor = option_index_to_editor_map[default_index] + self.combo.currentIndexChanged.connect(on_current_index_changed) + self.combo.setCurrentIndex(default_index) + on_current_index_changed(default_index) + + hbox.addWidget(self.combo) + for w in self.editors: + hbox.addWidget(w) + hbox.addStretch(1) + + def get_locktime(self) -> Optional[int]: + return self.editor.get_locktime() + + def set_locktime(self, x: Any) -> None: + self.editor.set_locktime(x) + + +class _LockTimeEditor: + min_allowed_value = NLOCKTIME_MIN + max_allowed_value = NLOCKTIME_MAX + + def get_locktime(self) -> Optional[int]: + raise NotImplementedError() + + def set_locktime(self, x: Any) -> None: + raise NotImplementedError() + + @classmethod + def is_acceptable_locktime(cls, x: Any) -> bool: + if not x: # e.g. empty string + return True + try: + x = int(x) + except: + return False + return cls.min_allowed_value <= x <= cls.max_allowed_value + + +class LockTimeRawEdit(QLineEdit, _LockTimeEditor): + + def __init__(self, parent=None): + QLineEdit.__init__(self, parent) + self.setFixedWidth(14 * char_width_in_lineedit()) + self.textChanged.connect(self.numbify) + + def numbify(self): + text = self.text().strip() + chars = '0123456789' + pos = self.cursorPosition() + pos = len(''.join([i for i in text[:pos] if i in chars])) + s = ''.join([i for i in text if i in chars]) + self.set_locktime(s) + # setText sets Modified to False. Instead we want to remember + # if updates were because of user modification. + self.setModified(self.hasFocus()) + self.setCursorPosition(pos) + + def get_locktime(self) -> Optional[int]: + try: + return int(str(self.text())) + except: + return None + + def set_locktime(self, x: Any) -> None: + try: + x = int(x) + except: + self.setText('') + return + x = max(x, self.min_allowed_value) + x = min(x, self.max_allowed_value) + self.setText(str(x)) + + +class LockTimeHeightEdit(LockTimeRawEdit): + max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + + def __init__(self, parent=None): + LockTimeRawEdit.__init__(self, parent) + self.setFixedWidth(20 * char_width_in_lineedit()) + self.help_palette = QPalette() + + def paintEvent(self, event): + super().paintEvent(event) + panel = QStyleOptionFrame() + self.initStyleOption(panel) + textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self) + textRect.adjust(2, 0, -10, 0) + painter = QPainter(self) + painter.setPen(self.help_palette.brush(QPalette.Disabled, QPalette.Text).color()) + painter.drawText(textRect, Qt.AlignRight | Qt.AlignVCenter, "height") + + +class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor): + min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1 + + def __init__(self, parent=None): + QDateTimeEdit.__init__(self, parent) + self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value)) + self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value)) + self.setDateTime(QDateTime.currentDateTime()) + + def get_locktime(self) -> Optional[int]: + dt = self.dateTime().toPyDateTime() + locktime = int(time.mktime(dt.timetuple())) + return locktime + + def set_locktime(self, x: Any) -> None: + if not self.is_acceptable_locktime(x): + self.setDateTime(QDateTime.currentDateTime()) + return + try: + x = int(x) + except: + self.setDateTime(QDateTime.currentDateTime()) + return + dt = datetime.fromtimestamp(x) + self.setDateTime(dt) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py @@ -41,7 +41,7 @@ from qrcode import exceptions from electrum.simple_config import SimpleConfig from electrum.util import quantize_feerate -from electrum.bitcoin import base_encode +from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config @@ -58,6 +58,7 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, from .fee_slider import FeeSlider from .confirm_tx_dialog import TxEditor from .amountedit import FeerateEdit, BTCAmountEdit +from .locktimeedit import LockTimeEdit if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -434,7 +435,13 @@ class BaseTxDialog(QDialog, MessageBoxMixin): self.date_label.show() else: self.date_label.hide() - self.locktime_label.setText(f"LockTime: {self.tx.locktime}") + if self.tx.locktime <= NLOCKTIME_BLOCKHEIGHT_MAX: + locktime_final_str = f"LockTime: {self.tx.locktime} (height)" + else: + locktime_final_str = f"LockTime: {self.tx.locktime} ({datetime.datetime.fromtimestamp(self.tx.locktime)})" + self.locktime_final_label.setText(locktime_final_str) + if self.locktime_e.get_locktime() is None: + self.locktime_e.set_locktime(self.tx.locktime) self.rbf_label.setText(_('Replace by fee') + f": {not self.tx.is_final()}") if tx_mined_status.header_hash: @@ -611,8 +618,22 @@ class BaseTxDialog(QDialog, MessageBoxMixin): self.rbf_cb.setChecked(bool(self.config.get('use_rbf', True))) vbox_right.addWidget(self.rbf_cb) - self.locktime_label = TxDetailLabel() - vbox_right.addWidget(self.locktime_label) + self.locktime_final_label = TxDetailLabel() + vbox_right.addWidget(self.locktime_final_label) + + locktime_setter_hbox = QHBoxLayout() + locktime_setter_hbox.setContentsMargins(0, 0, 0, 0) + locktime_setter_hbox.setSpacing(0) + locktime_setter_label = TxDetailLabel() + locktime_setter_label.setText("LockTime: ") + self.locktime_e = LockTimeEdit() + locktime_setter_hbox.addWidget(locktime_setter_label) + locktime_setter_hbox.addWidget(self.locktime_e) + locktime_setter_hbox.addStretch(1) + self.locktime_setter_widget = QWidget() + self.locktime_setter_widget.setLayout(locktime_setter_hbox) + vbox_right.addWidget(self.locktime_setter_widget) + self.block_height_label = TxDetailLabel() vbox_right.addWidget(self.block_height_label) vbox_right.addStretch(1) @@ -620,12 +641,15 @@ class BaseTxDialog(QDialog, MessageBoxMixin): vbox.addLayout(hbox_stats) + # below columns self.block_hash_label = TxDetailLabel(word_wrap=True) vbox.addWidget(self.block_hash_label) # set visibility after parenting can be determined by Qt self.rbf_label.setVisible(self.finalized) self.rbf_cb.setVisible(not self.finalized) + self.locktime_final_label.setVisible(self.finalized) + self.locktime_setter_widget.setVisible(not self.finalized) def set_title(self): self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction")) @@ -838,10 +862,12 @@ class PreviewTxDialog(BaseTxDialog, TxEditor): return self.finalized = True self.tx.set_rbf(self.rbf_cb.isChecked()) - for widget in [self.fee_slider, self.feecontrol_fields, self.rbf_cb]: + self.tx.locktime = self.locktime_e.get_locktime() + for widget in [self.fee_slider, self.feecontrol_fields, self.rbf_cb, + self.locktime_setter_widget, self.locktime_e]: widget.setEnabled(False) widget.setVisible(False) - for widget in [self.rbf_label]: + for widget in [self.rbf_label, self.locktime_final_label]: widget.setVisible(True) self.set_title() self.set_buttons_visibility() diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py @@ -1135,9 +1135,9 @@ class Peer(Logger): processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey) if chan.get_state() != channel_states.OPEN: raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()}") - if cltv_expiry >= 500_000_000: + if cltv_expiry > bitcoin.NLOCKTIME_BLOCKHEIGHT_MAX: asyncio.ensure_future(self.lnworker.force_close_channel(channel_id)) - raise RemoteMisbehaving(f"received update_add_htlc with cltv_expiry >= 500_000_000. value was {cltv_expiry}") + raise RemoteMisbehaving(f"received update_add_htlc with cltv_expiry > BLOCKHEIGHT_MAX. value was {cltv_expiry}") # add htlc htlc = UpdateAddHtlc(amount_msat=amount_msat_htlc, payment_hash=payment_hash,