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:
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,