Browse Source

kivy: add confirm_tx_dialog, similar to qt

patch-4
ThomasV 4 years ago
parent
commit
90d66953cf
  1. 14
      electrum/gui/kivy/main_window.py
  2. 174
      electrum/gui/kivy/uix/dialogs/confirm_tx_dialog.py
  3. 86
      electrum/gui/kivy/uix/dialogs/fee_dialog.py
  4. 8
      electrum/gui/kivy/uix/dialogs/settings.py
  5. 48
      electrum/gui/kivy/uix/screens.py
  6. 16
      electrum/gui/kivy/uix/ui_screens/send.kv
  7. 6
      electrum/simple_config.py

14
electrum/gui/kivy/main_window.py

@ -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
@ -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'):

174
electrum/gui/kivy/uix/dialogs/confirm_tx_dialog.py

@ -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())

86
electrum/gui/kivy/uix/dialogs/fee_dialog.py

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

8
electrum/gui/kivy/uix/dialogs/settings.py

@ -49,6 +49,11 @@ Builder.load_string('''
description: _("Base unit for Bitcoin amounts.")
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
@ -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()

48
electrum/gui/kivy/uix/screens.py

@ -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
@ -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():
@ -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:

16
electrum/gui/kivy/uix/ui_screens/send.kv

@ -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'

6
electrum/simple_config.py

@ -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(

Loading…
Cancel
Save