tkivy: add confirm_tx_dialog, similar to qt - electrum - Electrum Bitcoin wallet (HTM) git clone https://git.parazyd.org/electrum (DIR) Log (DIR) Files (DIR) Refs (DIR) Submodules --- (DIR) commit 90d66953cfd53f1a055709fe99f25d23d0a190c7 (DIR) parent 12c9de6bf998f2a0a4cbc9c9c4bb1d11dac44dc5 (HTM) Author: ThomasV <thomasv@electrum.org> Date: Tue, 19 Jan 2021 14:15:07 +0100 kivy: add confirm_tx_dialog, similar to qt Diffstat: M electrum/gui/kivy/main_window.py | 14 ++++++++------ A electrum/gui/kivy/uix/dialogs/conf… | 174 +++++++++++++++++++++++++++++++ M electrum/gui/kivy/uix/dialogs/fee_… | 86 +++++++++++++++++++------------ M electrum/gui/kivy/uix/dialogs/sett… | 8 +++++--- M electrum/gui/kivy/uix/screens.py | 48 ++++--------------------------- M electrum/gui/kivy/uix/ui_screens/s… | 16 ---------------- M electrum/simple_config.py | 6 +++++- 7 files changed, 249 insertions(+), 103 deletions(-) --- (DIR) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py t@@ -408,7 +408,7 @@ class ElectrumWindow(App, Logger): self._settings_dialog = None self._channels_dialog = None self._addresses_dialog = None - self.fee_status = self.electrum_config.get_fee_status() + self.set_fee_status() self.invoice_popup = None self.request_popup = None t@@ -1160,15 +1160,17 @@ class ElectrumWindow(App, Logger): self._addresses_dialog.update() self._addresses_dialog.open() - def fee_dialog(self, label, dt): + def fee_dialog(self): from .uix.dialogs.fee_dialog import FeeDialog - def cb(): - self.fee_status = self.electrum_config.get_fee_status() - fee_dialog = FeeDialog(self, self.electrum_config, cb) + fee_dialog = FeeDialog(self, self.electrum_config, self.set_fee_status) fee_dialog.open() + def set_fee_status(self): + target, tooltip, dyn = self.electrum_config.get_fee_target() + self.fee_status = target + def on_fee(self, event, *arg): - self.fee_status = self.electrum_config.get_fee_status() + self.set_fee_status() def protected(self, msg, f, args): if self.electrum_config.get('pin_code'): (DIR) diff --git a/electrum/gui/kivy/uix/dialogs/confirm_tx_dialog.py b/electrum/gui/kivy/uix/dialogs/confirm_tx_dialog.py t@@ -0,0 +1,174 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder +from kivy.uix.checkbox import CheckBox +from kivy.uix.label import Label +from kivy.uix.widget import Widget +from kivy.clock import Clock + +from decimal import Decimal + +from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING +from electrum.gui.kivy.i18n import _ +from electrum.plugin import run_hook + +from .fee_dialog import FeeSliderDialog, FeeDialog + +Builder.load_string(''' +<ConfirmTxDialog@Popup> + id: popup + title: _('Confirm Payment') + message: '' + warning: '' + extra_fee: '' + show_final: False + size_hint: 0.8, 0.8 + pos_hint: {'top':0.9} + BoxLayout: + orientation: 'vertical' + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Label: + text: _('Amount to be sent:') + Label: + id: amount_label + text: '' + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Label: + text: _('Mining fee:') + Label: + id: fee_label + text: '' + BoxLayout: + orientation: 'horizontal' + size_hint: 1, (0.5 if root.extra_fee else 0.01) + Label: + text: _('Additional fees') if root.extra_fee else '' + Label: + text: root.extra_fee + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Label: + text: _('Fee target:') + Button: + id: fee_button + text: '' + background_color: (0,0,0,0) + bold: True + on_release: + root.on_fee_button() + Slider: + id: slider + range: 0, 4 + step: 1 + on_value: root.on_slider(self.value) + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.2 + Label: + text: _('Final') + opacity: int(root.show_final) + CheckBox: + id: final_cb + opacity: int(root.show_final) + disabled: not root.show_final + Label: + text: root.warning + text_size: self.width, None + Widget: + size_hint: 1, 0.5 + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Button: + text: _('Cancel') + size_hint: 0.5, None + height: '48dp' + on_release: + popup.dismiss() + Button: + text: _('OK') + size_hint: 0.5, None + height: '48dp' + on_release: + root.pay() + popup.dismiss() +''') + + + + +class ConfirmTxDialog(FeeSliderDialog, Factory.Popup): + + def __init__(self, app, invoice): + + Factory.Popup.__init__(self) + FeeSliderDialog.__init__(self, app.electrum_config, self.ids.slider) + self.app = app + self.show_final = bool(self.config.get('use_rbf')) + self.invoice = invoice + self.update_slider() + self.update_text() + self.update_tx() + + def update_tx(self): + outputs = self.invoice.outputs + try: + # make unsigned transaction + coins = self.app.wallet.get_spendable_coins(None) + tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs) + except NotEnoughFunds: + self.warning = _("Not enough funds") + return + except Exception as e: + self.logger.exception('') + self.app.show_error(repr(e)) + return + rbf = not bool(self.ids.final_cb.active) if self.show_final else False + tx.set_rbf(rbf) + amount = sum(map(lambda x: x.value, outputs)) if '!' not in [x.value for x in outputs] else tx.output_value() + fee = tx.get_fee() + feerate = Decimal(fee) / tx.estimated_size() # sat/byte + self.ids.fee_label.text = self.app.format_amount_and_units(fee) + f' ({feerate:.1f} sat/B)' + self.ids.amount_label.text = self.app.format_amount_and_units(amount) + x_fee = run_hook('get_tx_extra_fee', self.app.wallet, tx) + if x_fee: + x_fee_address, x_fee_amount = x_fee + self.extra_fee = self.app.format_amount_and_units(x_fee_amount) + else: + self.extra_fee = '' + fee_ratio = Decimal(fee) / amount if amount else 1 + if fee_ratio >= FEE_RATIO_HIGH_WARNING: + self.warning = _('Warning') + ': ' + _("The fee for this transaction seems unusually high.") + f' ({fee_ratio*100:.2f}% of amount)' + elif feerate > FEERATE_WARNING_HIGH_FEE / 1000: + self.warning = _('Warning') + ': ' + _("The fee for this transaction seems unusually high.") + f' (feerate: {feerate:.2f} sat/byte)' + else: + self.warning = '' + self.tx = tx + + def on_slider(self, value): + self.save_config() + self.update_text() + Clock.schedule_once(lambda dt: self.update_tx()) + + def update_text(self): + target, tooltip, dyn = self.config.get_fee_target() + self.ids.fee_button.text = target + + def pay(self): + self.app.protected(_('Send payment?'), self.app.send_screen.send_tx, (self.tx, self.invoice)) + + def on_fee_button(self): + fee_dialog = FeeDialog(self, self.config, self.after_fee_changed) + fee_dialog.open() + + def after_fee_changed(self): + self.read_config() + self.update_slider() + self.update_text() + Clock.schedule_once(lambda dt: self.update_tx()) (DIR) diff --git a/electrum/gui/kivy/uix/dialogs/fee_dialog.py b/electrum/gui/kivy/uix/dialogs/fee_dialog.py t@@ -68,37 +68,16 @@ Builder.load_string(''' root.dismiss() ''') -class FeeDialog(Factory.Popup): - def __init__(self, app, config, callback): - Factory.Popup.__init__(self) - self.app = app - self.config = config - self.callback = callback - mempool = self.config.use_mempool_fees() - dynfees = self.config.is_dynfee() - self.method = (2 if mempool else 1) if dynfees else 0 - self.update_slider() - self.update_text() - def update_text(self): - pos = int(self.ids.slider.value) - dynfees, mempool = self.get_method() - if self.method == 2: - fee_rate = self.config.depth_to_fee(pos) - target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) - msg = 'In the current network conditions, in order to be positioned %s, a transaction will require a fee of %s.' % (target, estimate) - elif self.method == 1: - fee_rate = self.config.eta_to_fee(pos) - target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) - msg = 'In the last few days, transactions that confirmed %s usually paid a fee of at least %s.' % (target.lower(), estimate) - else: - fee_rate = self.config.static_fee(pos) - target, estimate = self.config.get_fee_text(pos, dynfees, True, fee_rate) - msg = 'In the current network conditions, a transaction paying %s would be positioned %s.' % (target, estimate) - self.ids.fee_target.text = target - self.ids.fee_estimate.text = msg +class FeeSliderDialog: + + def __init__(self, config, slider): + self.config = config + self.slider = slider + self.read_config() + self.update_slider() def get_method(self): dynfees = self.method > 0 t@@ -106,15 +85,19 @@ class FeeDialog(Factory.Popup): return dynfees, mempool def update_slider(self): - slider = self.ids.slider dynfees, mempool = self.get_method() maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool) - slider.range = (0, maxp) - slider.step = 1 - slider.value = pos + self.slider.range = (0, maxp) + self.slider.step = 1 + self.slider.value = pos - def on_ok(self): - value = int(self.ids.slider.value) + def read_config(self): + mempool = self.config.use_mempool_fees() + dynfees = self.config.is_dynfee() + self.method = (2 if mempool else 1) if dynfees else 0 + + def save_config(self): + value = int(self.slider.value) dynfees, mempool = self.get_method() self.config.set_key('dynamic_fees', dynfees, False) self.config.set_key('mempool_fees', mempool, False) t@@ -125,7 +108,42 @@ class FeeDialog(Factory.Popup): self.config.set_key('fee_level', value, True) else: self.config.set_key('fee_per_kb', self.config.static_fee(value), True) + + def update_text(self): + pass + + +class FeeDialog(FeeSliderDialog, Factory.Popup): + + def __init__(self, app, config, callback): + Factory.Popup.__init__(self) + FeeSliderDialog.__init__(self, config, self.ids.slider) + self.app = app + self.config = config + self.callback = callback + self.update_text() + + def on_ok(self): + self.save_config() self.callback() def on_slider(self, value): self.update_text() + + def update_text(self): + pos = int(self.ids.slider.value) + dynfees, mempool = self.get_method() + if self.method == 2: + fee_rate = self.config.depth_to_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) + msg = 'In the current network conditions, in order to be positioned %s, a transaction will require a fee of %s.' % (target, estimate) + elif self.method == 1: + fee_rate = self.config.eta_to_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, mempool, fee_rate) + msg = 'In the last few days, transactions that confirmed %s usually paid a fee of at least %s.' % (target.lower(), estimate) + else: + fee_rate = self.config.static_fee(pos) + target, estimate = self.config.get_fee_text(pos, dynfees, True, fee_rate) + msg = 'In the current network conditions, a transaction paying %s would be positioned %s.' % (target, estimate) + self.ids.fee_target.text = target + self.ids.fee_estimate.text = msg (DIR) diff --git a/electrum/gui/kivy/uix/dialogs/settings.py b/electrum/gui/kivy/uix/dialogs/settings.py t@@ -50,6 +50,11 @@ Builder.load_string(''' action: partial(root.unit_dialog, self) CardSeparator SettingsItem: + title: _('Onchain fees') + ': ' + app.fee_status + description: _('Choose how transaction fees are estimated') + action: lambda dt: app.fee_dialog() + CardSeparator + SettingsItem: status: root.fx_status() title: _('Fiat Currency') + ': ' + self.status description: _("Display amounts in fiat currency.") t@@ -217,9 +222,6 @@ class SettingsDialog(Factory.Popup): d = CheckBoxDialog(fullname, descr, status, callback) d.open() - def fee_status(self): - return self.config.get_fee_status() - def boolean_dialog(self, name, title, message, dt): from .checkbox_dialog import CheckBoxDialog CheckBoxDialog(title, message, getattr(self.app, name), lambda x: setattr(self.app, name, x)).open() (DIR) diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py t@@ -29,10 +29,8 @@ from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATIO from electrum import bitcoin, constants from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice -from electrum.plugin import run_hook from electrum.wallet import InternalAddressCorruption from electrum import simple_config -from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING from electrum.lnaddr import lndecode from electrum.lnutil import RECEIVED, SENT, PaymentFailure from electrum.logging import Logger t@@ -364,12 +362,7 @@ class SendScreen(CScreen, Logger): else: self.app.show_error(_("Lightning payments are not available for this wallet")) else: - do_pay = lambda rbf: self._do_pay_onchain(invoice, rbf) - if self.app.electrum_config.get('use_rbf'): - d = Question(_('Should this transaction be replaceable?'), do_pay) - d.open() - else: - do_pay(False) + self._do_pay_onchain(invoice) def _do_pay_lightning(self, invoice: LNInvoice, pw) -> None: def pay_thread(): t@@ -380,41 +373,10 @@ class SendScreen(CScreen, Logger): self.save_invoice(invoice) threading.Thread(target=pay_thread).start() - def _do_pay_onchain(self, invoice: OnchainInvoice, rbf: bool) -> None: - # make unsigned transaction - outputs = invoice.outputs - coins = self.app.wallet.get_spendable_coins(None) - try: - tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs) - except NotEnoughFunds: - self.app.show_error(_("Not enough funds")) - return - except Exception as e: - self.logger.exception('') - self.app.show_error(repr(e)) - return - if rbf: - tx.set_rbf(True) - fee = tx.get_fee() - amount = sum(map(lambda x: x.value, outputs)) if '!' not in [x.value for x in outputs] else tx.output_value() - msg = [ - _("Amount to be sent") + ": " + self.app.format_amount_and_units(amount), - _("Mining fee") + ": " + self.app.format_amount_and_units(fee), - ] - x_fee = run_hook('get_tx_extra_fee', self.app.wallet, tx) - if x_fee: - x_fee_address, x_fee_amount = x_fee - msg.append(_("Additional fees") + ": " + self.app.format_amount_and_units(x_fee_amount)) - - feerate = Decimal(fee) / tx.estimated_size() # sat/byte - fee_ratio = Decimal(fee) / amount if amount else 1 - if fee_ratio >= FEE_RATIO_HIGH_WARNING: - msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high.") - + f' ({fee_ratio*100:.2f}% of amount)') - elif feerate > FEERATE_WARNING_HIGH_FEE / 1000: - msg.append(_('Warning') + ': ' + _("The fee for this transaction seems unusually high.") - + f' (feerate: {feerate:.2f} sat/byte)') - self.app.protected('\n'.join(msg), self.send_tx, (tx, invoice)) + def _do_pay_onchain(self, invoice: OnchainInvoice) -> None: + from .dialogs.confirm_tx_dialog import ConfirmTxDialog + d = ConfirmTxDialog(self.app, invoice) + d.open() def send_tx(self, tx, invoice, password): if self.app.wallet.has_password() and password is None: (DIR) diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv t@@ -131,22 +131,6 @@ text: s.message if s.message else (_('No Description') if root.is_locked else _('Description')) disabled: root.is_locked on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) - CardSeparator: - color: blue_bottom.foreground_color - BoxLayout: - size_hint: 1, None - height: blue_bottom.item_height - spacing: '5dp' - Image: - source: f'atlas://{KIVY_GUI_PATH}/theming/light/star_big_inactive' - size_hint: None, None - size: '22dp', '22dp' - pos_hint: {'center_y': .5} - BlueButton: - id: fee_e - default_text: _('Fee') - text: app.fee_status if not root.is_lightning else '' - on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if not root.is_lightning else None BoxLayout: size_hint: 1, None height: '48dp' (DIR) diff --git a/electrum/simple_config.py b/electrum/simple_config.py t@@ -423,12 +423,16 @@ class SimpleConfig(Logger): else: return _('Within {} blocks').format(x) - def get_fee_status(self): + def get_fee_target(self): dyn = self.is_dynfee() mempool = self.use_mempool_fees() pos = self.get_depth_level() if mempool else self.get_fee_level() fee_rate = self.fee_per_kb() target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate) + return target, tooltip, dyn + + def get_fee_status(self): + target, tooltip, dyn = self.get_fee_target() return tooltip + ' [%s]'%target if dyn else target + ' [Static]' def get_fee_text(