Browse Source

Qt tx dialog: allow setting custom locktime

closes #2405
closes #1685
hard-fail-on-bad-server-string
SomberNight 5 years ago
parent
commit
6f2cd8b4f5
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 4
      electrum/bitcoin.py
  2. 173
      electrum/gui/qt/locktimeedit.py
  3. 38
      electrum/gui/qt/transaction_dialog.py
  4. 4
      electrum/lnpeer.py

4
electrum/bitcoin.py

@ -44,6 +44,10 @@ COINBASE_MATURITY = 100
COIN = 100000000 COIN = 100000000
TOTAL_COIN_SUPPLY_LIMIT_IN_BTC = 21000000 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 # supported types of transaction outputs
# TODO kill these with fire # TODO kill these with fire
TYPE_ADDRESS = 0 TYPE_ADDRESS = 0

173
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)

38
electrum/gui/qt/transaction_dialog.py

@ -41,7 +41,7 @@ from qrcode import exceptions
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.util import quantize_feerate 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.i18n import _
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum import simple_config from electrum import simple_config
@ -58,6 +58,7 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path,
from .fee_slider import FeeSlider from .fee_slider import FeeSlider
from .confirm_tx_dialog import TxEditor from .confirm_tx_dialog import TxEditor
from .amountedit import FeerateEdit, BTCAmountEdit from .amountedit import FeerateEdit, BTCAmountEdit
from .locktimeedit import LockTimeEdit
if TYPE_CHECKING: if TYPE_CHECKING:
from .main_window import ElectrumWindow from .main_window import ElectrumWindow
@ -434,7 +435,13 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
self.date_label.show() self.date_label.show()
else: else:
self.date_label.hide() 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()}") self.rbf_label.setText(_('Replace by fee') + f": {not self.tx.is_final()}")
if tx_mined_status.header_hash: 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))) self.rbf_cb.setChecked(bool(self.config.get('use_rbf', True)))
vbox_right.addWidget(self.rbf_cb) vbox_right.addWidget(self.rbf_cb)
self.locktime_label = TxDetailLabel() self.locktime_final_label = TxDetailLabel()
vbox_right.addWidget(self.locktime_label) 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() self.block_height_label = TxDetailLabel()
vbox_right.addWidget(self.block_height_label) vbox_right.addWidget(self.block_height_label)
vbox_right.addStretch(1) vbox_right.addStretch(1)
@ -620,12 +641,15 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
vbox.addLayout(hbox_stats) vbox.addLayout(hbox_stats)
# below columns
self.block_hash_label = TxDetailLabel(word_wrap=True) self.block_hash_label = TxDetailLabel(word_wrap=True)
vbox.addWidget(self.block_hash_label) vbox.addWidget(self.block_hash_label)
# set visibility after parenting can be determined by Qt # set visibility after parenting can be determined by Qt
self.rbf_label.setVisible(self.finalized) self.rbf_label.setVisible(self.finalized)
self.rbf_cb.setVisible(not 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): def set_title(self):
self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction")) self.setWindowTitle(_("Create transaction") if not self.finalized else _("Transaction"))
@ -838,10 +862,12 @@ class PreviewTxDialog(BaseTxDialog, TxEditor):
return return
self.finalized = True self.finalized = True
self.tx.set_rbf(self.rbf_cb.isChecked()) 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.setEnabled(False)
widget.setVisible(False) widget.setVisible(False)
for widget in [self.rbf_label]: for widget in [self.rbf_label, self.locktime_final_label]:
widget.setVisible(True) widget.setVisible(True)
self.set_title() self.set_title()
self.set_buttons_visibility() self.set_buttons_visibility()

4
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) processed_onion = process_onion_packet(onion_packet, associated_data=payment_hash, our_onion_private_key=self.privkey)
if chan.get_state() != channel_states.OPEN: if chan.get_state() != channel_states.OPEN:
raise RemoteMisbehaving(f"received update_add_htlc while chan.get_state() != OPEN. state was {chan.get_state()}") 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)) 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 # add htlc
htlc = UpdateAddHtlc(amount_msat=amount_msat_htlc, htlc = UpdateAddHtlc(amount_msat=amount_msat_htlc,
payment_hash=payment_hash, payment_hash=payment_hash,

Loading…
Cancel
Save