1
0

kivy: add confirm_tx_dialog, similar to qt

This commit is contained in:
ThomasV
2021-01-19 14:15:07 +01:00
parent 12c9de6bf9
commit 90d66953cf
7 changed files with 252 additions and 106 deletions

View File

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

View File

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

View File

@@ -68,17 +68,66 @@ Builder.load_string('''
root.dismiss()
''')
class FeeDialog(Factory.Popup):
def __init__(self, app, config, callback):
Factory.Popup.__init__(self)
self.app = app
class FeeSliderDialog:
def __init__(self, config, slider):
self.config = config
self.callback = callback
self.slider = slider
self.read_config()
self.update_slider()
def get_method(self):
dynfees = self.method > 0
mempool = self.method == 2
return dynfees, mempool
def update_slider(self):
dynfees, mempool = self.get_method()
maxp, pos, fee_rate = self.config.get_fee_slider(dynfees, mempool)
self.slider.range = (0, maxp)
self.slider.step = 1
self.slider.value = pos
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
self.update_slider()
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)
if dynfees:
if mempool:
self.config.set_key('depth_level', value, True)
else:
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):
@@ -96,36 +145,5 @@ class FeeDialog(Factory.Popup):
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
def get_method(self):
dynfees = self.method > 0
mempool = self.method == 2
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
def on_ok(self):
value = int(self.ids.slider.value)
dynfees, mempool = self.get_method()
self.config.set_key('dynamic_fees', dynfees, False)
self.config.set_key('mempool_fees', mempool, False)
if dynfees:
if mempool:
self.config.set_key('depth_level', value, True)
else:
self.config.set_key('fee_level', value, True)
else:
self.config.set_key('fee_per_kb', self.config.static_fee(value), True)
self.callback()
def on_slider(self, value):
self.update_text()

View File

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

View File

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

View File

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

View File

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