From 67cd73cae0b9eea630c4852d0e495ab876d6ae1e Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 9 Oct 2020 19:01:48 +0200 Subject: [PATCH] kivy: implement dscancel --- .../gui/kivy/uix/dialogs/bump_fee_dialog.py | 8 +- .../gui/kivy/uix/dialogs/dscancel_dialog.py | 111 ++++++++++++++++++ electrum/gui/kivy/uix/dialogs/tx_dialog.py | 27 ++++- 3 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 electrum/gui/kivy/uix/dialogs/dscancel_dialog.py diff --git a/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py b/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py index 21f3ca2b7..01979f1dd 100644 --- a/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/bump_fee_dialog.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from kivy.app import App from kivy.factory import Factory from kivy.properties import ObjectProperty @@ -5,6 +7,10 @@ from kivy.lang import Builder from electrum.gui.kivy.i18n import _ +if TYPE_CHECKING: + from ...main_window import ElectrumWindow + + Builder.load_string(''' title: _('Bump fee') @@ -68,7 +74,7 @@ Builder.load_string(''' class BumpFeeDialog(Factory.Popup): - def __init__(self, app, fee, size, callback): + def __init__(self, app: 'ElectrumWindow', fee, size, callback): Factory.Popup.__init__(self) self.app = app self.init_fee = fee diff --git a/electrum/gui/kivy/uix/dialogs/dscancel_dialog.py b/electrum/gui/kivy/uix/dialogs/dscancel_dialog.py new file mode 100644 index 000000000..91a316d05 --- /dev/null +++ b/electrum/gui/kivy/uix/dialogs/dscancel_dialog.py @@ -0,0 +1,111 @@ +from typing import TYPE_CHECKING + +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder + +from electrum.gui.kivy.i18n import _ + +if TYPE_CHECKING: + from ...main_window import ElectrumWindow + + +Builder.load_string(''' + + title: _('Cancel transaction') + size_hint: 0.8, 0.8 + pos_hint: {'top':0.9} + BoxLayout: + orientation: 'vertical' + padding: '10dp' + + GridLayout: + height: self.minimum_height + size_hint_y: None + cols: 1 + spacing: '10dp' + BoxLabel: + id: old_fee + text: _('Current Fee') + value: '' + BoxLabel: + id: old_feerate + text: _('Current Fee rate') + value: '' + Label: + id: tooltip1 + text: '' + size_hint_y: None + Label: + id: tooltip2 + text: '' + size_hint_y: None + Slider: + id: slider + range: 0, 4 + step: 1 + on_value: root.on_slider(self.value) + Widget: + size_hint: 1, 1 + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Button: + text: 'Cancel' + size_hint: 0.5, None + height: '48dp' + on_release: root.dismiss() + Button: + text: 'OK' + size_hint: 0.5, None + height: '48dp' + on_release: + root.dismiss() + root.on_ok() +''') + +class DSCancelDialog(Factory.Popup): + + def __init__(self, app: 'ElectrumWindow', fee, size, callback): + Factory.Popup.__init__(self) + self.app = app + self.init_fee = fee + self.tx_size = size + self.callback = callback + self.config = app.electrum_config + self.mempool = self.config.use_mempool_fees() + self.dynfees = self.config.is_dynfee() and bool(self.app.network) and self.config.has_dynamic_fees_ready() + self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee) + self.ids.old_feerate.value = self.app.format_fee_rate(fee / self.tx_size * 1000) + self.update_slider() + self.update_text() + + def update_text(self): + pos = int(self.ids.slider.value) + new_fee_rate = self.get_fee_rate() + text, tooltip = self.config.get_fee_text(pos, self.dynfees, self.mempool, new_fee_rate) + self.ids.tooltip1.text = text + self.ids.tooltip2.text = tooltip + + def update_slider(self): + slider = self.ids.slider + maxp, pos, fee_rate = self.config.get_fee_slider(self.dynfees, self.mempool) + slider.range = (0, maxp) + slider.step = 1 + slider.value = pos + + def get_fee_rate(self): + pos = int(self.ids.slider.value) + if self.dynfees: + fee_rate = self.config.depth_to_fee(pos) if self.mempool else self.config.eta_to_fee(pos) + else: + fee_rate = self.config.static_fee(pos) + return fee_rate # sat/kbyte + + def on_ok(self): + new_fee_rate = self.get_fee_rate() / 1000 + self.callback(new_fee_rate) + + def on_slider(self, value): + self.update_text() diff --git a/electrum/gui/kivy/uix/dialogs/tx_dialog.py b/electrum/gui/kivy/uix/dialogs/tx_dialog.py index 6c0f0336e..9edc7f055 100644 --- a/electrum/gui/kivy/uix/dialogs/tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/tx_dialog.py @@ -16,7 +16,7 @@ from electrum.gui.kivy.i18n import _ from electrum.util import InvalidPassword from electrum.address_synchronizer import TX_HEIGHT_LOCAL -from electrum.wallet import CannotBumpFee +from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx from electrum.transaction import Transaction, PartialTransaction from ...util import address_colors @@ -151,6 +151,7 @@ class TxDialog(Factory.Popup): self.description = tx_details.label self.can_broadcast = tx_details.can_broadcast self.can_rbf = tx_details.can_bump + self.can_dscancel = tx_details.can_dscancel self.tx_hash = tx_details.txid or '' if tx_mined_status.timestamp: self.date_label = _('Date') @@ -196,6 +197,7 @@ class TxDialog(Factory.Popup): ActionButtonOption(text=_('Sign'), func=lambda btn: self.do_sign(), enabled=self.can_sign), ActionButtonOption(text=_('Broadcast'), func=lambda btn: self.do_broadcast(), enabled=self.can_broadcast), ActionButtonOption(text=_('Bump fee'), func=lambda btn: self.do_rbf(), enabled=self.can_rbf), + ActionButtonOption(text=_('Cancel (double-spend)'), func=lambda btn: self.do_dscancel(), enabled=self.can_dscancel), ActionButtonOption(text=_('Remove'), func=lambda btn: self.remove_local_tx(), enabled=self.can_remove_tx), ) num_options = sum(map(lambda o: bool(o.enabled), options)) @@ -253,6 +255,29 @@ class TxDialog(Factory.Popup): self.update() self.do_sign() + def do_dscancel(self): + from .dscancel_dialog import DSCancelDialog + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(self.tx) + if fee is None: + self.app.show_error(_('Cannot cancel transaction') + ': ' + _('unknown fee for original transaction')) + return + size = self.tx.estimated_size() + d = DSCancelDialog(self.app, fee, size, self._do_dscancel) + d.open() + + def _do_dscancel(self, new_fee_rate): + if new_fee_rate is None: + return + try: + new_tx = self.wallet.dscancel(tx=self.tx, + new_fee_rate=new_fee_rate) + except CannotDoubleSpendTx as e: + self.app.show_error(str(e)) + return + self.tx = new_tx + self.update() + self.do_sign() + def do_sign(self): self.app.protected(_("Sign this transaction?"), self._do_sign, ())