tx dialog: uniform high fee warnings between GUIs
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
from decimal import Decimal
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import ObjectProperty
|
from kivy.properties import ObjectProperty
|
||||||
@@ -7,8 +10,6 @@ from kivy.uix.label import Label
|
|||||||
from kivy.uix.widget import Widget
|
from kivy.uix.widget import Widget
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
|
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING
|
from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING
|
||||||
from electrum.gui.kivy.i18n import _
|
from electrum.gui.kivy.i18n import _
|
||||||
from electrum.plugin import run_hook
|
from electrum.plugin import run_hook
|
||||||
@@ -16,6 +17,9 @@ from electrum.util import NotEnoughFunds
|
|||||||
|
|
||||||
from .fee_dialog import FeeSliderDialog, FeeDialog
|
from .fee_dialog import FeeSliderDialog, FeeDialog
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from electrum.gui.kivy.main_window import ElectrumWindow
|
||||||
|
|
||||||
Builder.load_string('''
|
Builder.load_string('''
|
||||||
<ConfirmTxDialog@Popup>
|
<ConfirmTxDialog@Popup>
|
||||||
id: popup
|
id: popup
|
||||||
@@ -106,7 +110,7 @@ Builder.load_string('''
|
|||||||
|
|
||||||
class ConfirmTxDialog(FeeSliderDialog, Factory.Popup):
|
class ConfirmTxDialog(FeeSliderDialog, Factory.Popup):
|
||||||
|
|
||||||
def __init__(self, app, invoice):
|
def __init__(self, app: 'ElectrumWindow', invoice):
|
||||||
|
|
||||||
Factory.Popup.__init__(self)
|
Factory.Popup.__init__(self)
|
||||||
FeeSliderDialog.__init__(self, app.electrum_config, self.ids.slider)
|
FeeSliderDialog.__init__(self, app.electrum_config, self.ids.slider)
|
||||||
@@ -133,8 +137,9 @@ class ConfirmTxDialog(FeeSliderDialog, Factory.Popup):
|
|||||||
rbf = not bool(self.ids.final_cb.active) if self.show_final else False
|
rbf = not bool(self.ids.final_cb.active) if self.show_final else False
|
||||||
tx.set_rbf(rbf)
|
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()
|
amount = sum(map(lambda x: x.value, outputs)) if '!' not in [x.value for x in outputs] else tx.output_value()
|
||||||
|
tx_size = tx.estimated_size()
|
||||||
fee = tx.get_fee()
|
fee = tx.get_fee()
|
||||||
feerate = Decimal(fee) / tx.estimated_size() # sat/byte
|
feerate = Decimal(fee) / tx_size # sat/byte
|
||||||
self.ids.fee_label.text = self.app.format_amount_and_units(fee) + f' ({feerate:.1f} sat/B)'
|
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)
|
self.ids.amount_label.text = self.app.format_amount_and_units(amount)
|
||||||
x_fee = run_hook('get_tx_extra_fee', self.app.wallet, tx)
|
x_fee = run_hook('get_tx_extra_fee', self.app.wallet, tx)
|
||||||
@@ -143,11 +148,11 @@ class ConfirmTxDialog(FeeSliderDialog, Factory.Popup):
|
|||||||
self.extra_fee = self.app.format_amount_and_units(x_fee_amount)
|
self.extra_fee = self.app.format_amount_and_units(x_fee_amount)
|
||||||
else:
|
else:
|
||||||
self.extra_fee = ''
|
self.extra_fee = ''
|
||||||
fee_ratio = Decimal(fee) / amount if amount else 1
|
fee_warning_tuple = self.app.wallet.get_tx_fee_warning(
|
||||||
if fee_ratio >= FEE_RATIO_HIGH_WARNING:
|
invoice_amt=amount, tx_size=tx_size, fee=fee)
|
||||||
self.warning = _('Warning') + ': ' + _("The fee for this transaction seems unusually high.") + f' ({fee_ratio*100:.2f}% of amount)'
|
if fee_warning_tuple:
|
||||||
elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:
|
allow_send, long_warning, short_warning = fee_warning_tuple
|
||||||
self.warning = _('Warning') + ': ' + _("The fee for this transaction seems unusually high.") + f' (feerate: {feerate:.2f} sat/byte)'
|
self.warning = long_warning
|
||||||
else:
|
else:
|
||||||
self.warning = ''
|
self.warning = ''
|
||||||
self.tx = tx
|
self.tx = tx
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ class ConfirmTxDialog(TxEditor, WindowModalDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
fee = tx.get_fee()
|
fee = tx.get_fee()
|
||||||
|
assert fee is not None
|
||||||
self.fee_label.setText(self.main_window.format_amount_and_units(fee))
|
self.fee_label.setText(self.main_window.format_amount_and_units(fee))
|
||||||
x_fee = run_hook('get_tx_extra_fee', self.wallet, tx)
|
x_fee = run_hook('get_tx_extra_fee', self.wallet, tx)
|
||||||
if x_fee:
|
if x_fee:
|
||||||
@@ -248,21 +249,11 @@ class ConfirmTxDialog(TxEditor, WindowModalDialog):
|
|||||||
self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount))
|
self.extra_fee_value.setText(self.main_window.format_amount_and_units(x_fee_amount))
|
||||||
|
|
||||||
amount = tx.output_value() if self.output_value == '!' else self.output_value
|
amount = tx.output_value() if self.output_value == '!' else self.output_value
|
||||||
feerate = Decimal(fee) / tx.estimated_size() # sat/byte
|
tx_size = tx.estimated_size()
|
||||||
fee_ratio = Decimal(fee) / amount if amount else 1
|
fee_warning_tuple = self.wallet.get_tx_fee_warning(
|
||||||
if feerate < self.wallet.relayfee() / 1000:
|
invoice_amt=amount, tx_size=tx_size, fee=fee)
|
||||||
msg = '\n'.join([
|
if fee_warning_tuple:
|
||||||
_("This transaction requires a higher fee, or it will not be propagated by your current server"),
|
allow_send, long_warning, short_warning = fee_warning_tuple
|
||||||
_("Try to raise your transaction fee, or use a server with a lower relay fee.")
|
self.toggle_send_button(allow_send, message=long_warning)
|
||||||
])
|
|
||||||
self.toggle_send_button(False, message=msg)
|
|
||||||
elif fee_ratio >= FEE_RATIO_HIGH_WARNING:
|
|
||||||
self.toggle_send_button(True,
|
|
||||||
message=_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")
|
|
||||||
+ f'\n({fee_ratio*100:.2f}% of amount)')
|
|
||||||
elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:
|
|
||||||
self.toggle_send_button(True,
|
|
||||||
message=_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")
|
|
||||||
+ f'\n(feerate: {feerate:.2f} sat/byte)')
|
|
||||||
else:
|
else:
|
||||||
self.toggle_send_button(True)
|
self.toggle_send_button(True)
|
||||||
|
|||||||
@@ -1670,22 +1670,26 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
return
|
return
|
||||||
|
|
||||||
output_value = '!' if '!' in output_values else sum(output_values)
|
output_value = '!' if '!' in output_values else sum(output_values)
|
||||||
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
|
conf_dlg = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, is_sweep=is_sweep)
|
||||||
if d.not_enough_funds:
|
if conf_dlg.not_enough_funds:
|
||||||
# Check if we had enough funds excluding fees,
|
# Check if we had enough funds excluding fees,
|
||||||
# if so, still provide opportunity to set lower fees.
|
# if so, still provide opportunity to set lower fees.
|
||||||
if not d.have_enough_funds_assuming_zero_fees():
|
if not conf_dlg.have_enough_funds_assuming_zero_fees():
|
||||||
text = self.get_text_not_enough_funds_mentioning_frozen()
|
text = self.get_text_not_enough_funds_mentioning_frozen()
|
||||||
self.show_message(text)
|
self.show_message(text)
|
||||||
return
|
return
|
||||||
|
|
||||||
# shortcut to advanced preview (after "enough funds" check!)
|
# shortcut to advanced preview (after "enough funds" check!)
|
||||||
if self.config.get('advanced_preview'):
|
if self.config.get('advanced_preview'):
|
||||||
self.preview_tx_dialog(make_tx=make_tx,
|
preview_dlg = PreviewTxDialog(
|
||||||
external_keypairs=external_keypairs)
|
window=self,
|
||||||
|
make_tx=make_tx,
|
||||||
|
external_keypairs=external_keypairs,
|
||||||
|
output_value=output_value)
|
||||||
|
preview_dlg.show()
|
||||||
return
|
return
|
||||||
|
|
||||||
cancelled, is_send, password, tx = d.run()
|
cancelled, is_send, password, tx = conf_dlg.run()
|
||||||
if cancelled:
|
if cancelled:
|
||||||
return
|
return
|
||||||
if is_send:
|
if is_send:
|
||||||
@@ -1696,13 +1700,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||||||
self.sign_tx_with_password(tx, callback=sign_done, password=password,
|
self.sign_tx_with_password(tx, callback=sign_done, password=password,
|
||||||
external_keypairs=external_keypairs)
|
external_keypairs=external_keypairs)
|
||||||
else:
|
else:
|
||||||
self.preview_tx_dialog(make_tx=make_tx,
|
preview_dlg = PreviewTxDialog(
|
||||||
external_keypairs=external_keypairs)
|
window=self,
|
||||||
|
make_tx=make_tx,
|
||||||
def preview_tx_dialog(self, *, make_tx, external_keypairs=None):
|
external_keypairs=external_keypairs,
|
||||||
d = PreviewTxDialog(make_tx=make_tx, external_keypairs=external_keypairs,
|
output_value=output_value)
|
||||||
window=self)
|
preview_dlg.show()
|
||||||
d.show()
|
|
||||||
|
|
||||||
def broadcast_or_show(self, tx: Transaction):
|
def broadcast_or_show(self, tx: Transaction):
|
||||||
if not tx.is_complete():
|
if not tx.is_complete():
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import copy
|
|||||||
import datetime
|
import datetime
|
||||||
import traceback
|
import traceback
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Callable, Optional, List
|
from typing import TYPE_CHECKING, Callable, Optional, List, Union
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
@@ -502,11 +502,22 @@ class BaseTxDialog(QDialog, MessageBoxMixin):
|
|||||||
size_str = _("Size:") + ' %d bytes'% size
|
size_str = _("Size:") + ' %d bytes'% size
|
||||||
fee_str = _("Fee") + ': %s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
|
fee_str = _("Fee") + ': %s' % (format_amount(fee) + ' ' + base_unit if fee is not None else _('unknown'))
|
||||||
if fee is not None:
|
if fee is not None:
|
||||||
fee_rate = fee/size*1000
|
fee_rate = Decimal(fee) / size # sat/byte
|
||||||
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate)
|
fee_str += ' ( %s ) ' % self.main_window.format_fee_rate(fee_rate * 1000)
|
||||||
feerate_warning = simple_config.FEERATE_WARNING_HIGH_FEE
|
if isinstance(self.tx, PartialTransaction):
|
||||||
if fee_rate > feerate_warning:
|
if isinstance(self, PreviewTxDialog):
|
||||||
fee_str += ' - ' + _('Warning') + ': ' + _("high fee") + '!'
|
invoice_amt = self.tx.output_value() if self.output_value == '!' else self.output_value
|
||||||
|
else:
|
||||||
|
invoice_amt = amount
|
||||||
|
fee_warning_tuple = self.wallet.get_tx_fee_warning(
|
||||||
|
invoice_amt=invoice_amt, tx_size=size, fee=fee)
|
||||||
|
if fee_warning_tuple:
|
||||||
|
allow_send, long_warning, short_warning = fee_warning_tuple
|
||||||
|
fee_str += " - <font color={color}>{header}: {body}</font>".format(
|
||||||
|
header=_('Warning'),
|
||||||
|
body=short_warning,
|
||||||
|
color=ColorScheme.RED.as_color().name(),
|
||||||
|
)
|
||||||
if isinstance(self.tx, PartialTransaction):
|
if isinstance(self.tx, PartialTransaction):
|
||||||
risk_of_burning_coins = (can_sign and fee is not None
|
risk_of_burning_coins = (can_sign and fee is not None
|
||||||
and self.wallet.get_warning_for_risk_of_burning_coins_as_fees(self.tx))
|
and self.wallet.get_warning_for_risk_of_burning_coins_as_fees(self.tx))
|
||||||
@@ -742,11 +753,23 @@ class TxDialog(BaseTxDialog):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PreviewTxDialog(BaseTxDialog, TxEditor):
|
class PreviewTxDialog(BaseTxDialog, TxEditor):
|
||||||
|
|
||||||
def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow'):
|
def __init__(
|
||||||
TxEditor.__init__(self, window=window, make_tx=make_tx, is_sweep=bool(external_keypairs))
|
self,
|
||||||
|
*,
|
||||||
|
make_tx,
|
||||||
|
external_keypairs,
|
||||||
|
window: 'ElectrumWindow',
|
||||||
|
output_value: Union[int, str],
|
||||||
|
):
|
||||||
|
TxEditor.__init__(
|
||||||
|
self,
|
||||||
|
window=window,
|
||||||
|
make_tx=make_tx,
|
||||||
|
is_sweep=bool(external_keypairs),
|
||||||
|
output_value=output_value,
|
||||||
|
)
|
||||||
BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False,
|
BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False,
|
||||||
finalized=False, external_keypairs=external_keypairs)
|
finalized=False, external_keypairs=external_keypairs)
|
||||||
BlockingWaitingDialog(window, _("Preparing transaction..."),
|
BlockingWaitingDialog(window, _("Preparing transaction..."),
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
|
|||||||
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
|
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
|
||||||
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
|
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
|
||||||
from .util import get_backup_dir
|
from .util import get_backup_dir
|
||||||
from .simple_config import SimpleConfig
|
from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE
|
||||||
from .bitcoin import COIN, TYPE_ADDRESS
|
from .bitcoin import COIN, TYPE_ADDRESS
|
||||||
from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
|
from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
|
||||||
from .crypto import sha256d
|
from .crypto import sha256d
|
||||||
@@ -2451,6 +2451,40 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
|
|||||||
"do not accept to sign it more than once,\n"
|
"do not accept to sign it more than once,\n"
|
||||||
"otherwise you could end up paying a different fee."))
|
"otherwise you could end up paying a different fee."))
|
||||||
|
|
||||||
|
def get_tx_fee_warning(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
invoice_amt: int,
|
||||||
|
tx_size: int,
|
||||||
|
fee: int,
|
||||||
|
) -> Optional[Tuple[bool, str, str]]:
|
||||||
|
feerate = Decimal(fee) / tx_size # sat/byte
|
||||||
|
fee_ratio = Decimal(fee) / invoice_amt if invoice_amt else 1
|
||||||
|
long_warning = None
|
||||||
|
short_warning = None
|
||||||
|
allow_send = True
|
||||||
|
if feerate < self.relayfee() / 1000:
|
||||||
|
long_warning = (
|
||||||
|
_("This transaction requires a higher fee, or it will not be propagated by your current server") + "\n"
|
||||||
|
+ _("Try to raise your transaction fee, or use a server with a lower relay fee.")
|
||||||
|
)
|
||||||
|
short_warning = _("below relay fee") + "!"
|
||||||
|
allow_send = False
|
||||||
|
elif fee_ratio >= FEE_RATIO_HIGH_WARNING:
|
||||||
|
long_warning = (
|
||||||
|
_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")
|
||||||
|
+ f'\n({fee_ratio*100:.2f}% of amount)')
|
||||||
|
short_warning = _("high fee ratio") + "!"
|
||||||
|
elif feerate > FEERATE_WARNING_HIGH_FEE / 1000:
|
||||||
|
long_warning = (
|
||||||
|
_('Warning') + ': ' + _("The fee for this transaction seems unusually high.")
|
||||||
|
+ f'\n(feerate: {feerate:.2f} sat/byte)')
|
||||||
|
short_warning = _("high fee rate") + "!"
|
||||||
|
if long_warning is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return allow_send, long_warning, short_warning
|
||||||
|
|
||||||
|
|
||||||
class Simple_Wallet(Abstract_Wallet):
|
class Simple_Wallet(Abstract_Wallet):
|
||||||
# wallet with a single keystore
|
# wallet with a single keystore
|
||||||
|
|||||||
Reference in New Issue
Block a user