URI: 
       tQt tx dialog: allow setting custom locktime - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 6f2cd8b4f53272dd3695fb9677907fa3a29be028
   DIR parent d8180c678b9f476da9807556debf1974284e3825
  HTML 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:
         M electrum/bitcoin.py                 |       4 ++++
         A electrum/gui/qt/locktimeedit.py     |     173 +++++++++++++++++++++++++++++++
         M electrum/gui/qt/transaction_dialog… |      38 ++++++++++++++++++++++++++-----
         M electrum/lnpeer.py                  |       4 ++--
       
       4 files changed, 211 insertions(+), 8 deletions(-)
       ---
   DIR diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py
       t@@ -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
   DIR diff --git a/electrum/gui/qt/locktimeedit.py b/electrum/gui/qt/locktimeedit.py
       t@@ -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)
   DIR diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py
       t@@ -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
       t@@ -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
       t@@ -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:
       t@@ -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)
       t@@ -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"))
       t@@ -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()
   DIR diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py
       t@@ -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,