From c95791d7eeff6f64968e391c78e344e0a02e1669 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Thu, 11 Aug 2022 19:39:05 +0000 Subject: [PATCH] qt/kivy: receive tab: add wallet.ReceiveRequestHelp and refactor --- .../gui/kivy/uix/dialogs/request_dialog.py | 98 +++++++++++++------ electrum/gui/qt/receive_tab.py | 74 +++++--------- electrum/wallet.py | 86 ++++++++++++++++ 3 files changed, 178 insertions(+), 80 deletions(-) diff --git a/electrum/gui/kivy/uix/dialogs/request_dialog.py b/electrum/gui/kivy/uix/dialogs/request_dialog.py index 8f66ce957..547b22f0a 100644 --- a/electrum/gui/kivy/uix/dialogs/request_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/request_dialog.py @@ -6,6 +6,7 @@ from kivy.core.clipboard import Clipboard from kivy.app import App from kivy.clock import Clock from kivy.properties import NumericProperty, StringProperty +from kivy.uix.tabbedpanel import TabbedPanel from electrum.gui.kivy.i18n import _ from electrum.invoices import pr_tooltips, pr_color @@ -18,6 +19,10 @@ if TYPE_CHECKING: Builder.load_string(''' #:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH +: + tab_height: "0dp" + tab_width: "1dp" + id: popup amount_str: '' @@ -27,6 +32,7 @@ Builder.load_string(''' key:'' data:'' warning: '' + error_text: '' status_str: '' status_color: 1,1,1,1 shaded: False @@ -39,13 +45,28 @@ Builder.load_string(''' size_hint: 1, 1 padding: '10dp' spacing: '10dp' - QRCodeWidget: - id: qr - shaded: False - foreground_color: (0, 0, 0, 0.5) if self.shaded else (0, 0, 0, 0) - on_touch_down: - touch = args[1] - if self.collide_point(*touch.pos): self.shaded = not self.shaded + TabbedPanelWithHiddenHeader: + id: qrdata_tabs + do_default_tab: False + TabbedPanelItem: + id: qrdata_tab_qr + border: 0,0,0,0 # to hide visual artifact around hidden tab header + QRCodeWidget: + id: qr + shaded: False + foreground_color: (0, 0, 0, 0.5) if self.shaded else (0, 0, 0, 0) + on_touch_down: + touch = args[1] + if self.collide_point(*touch.pos): self.shaded = not self.shaded + TabbedPanelItem: + id: qrdata_tab_error + border: 0,0,0,0 # to hide visual artifact around hidden tab header + BoxLayout: + padding: '20dp' + TopLabel: + text: root.error_text + pos_hint: {'center_x': .5, 'center_y': .5} + halign: "center" TopLabel: text: root.data[0:70] + ('...' if len(root.data)>70 else '') BoxLayout: @@ -110,6 +131,13 @@ Builder.load_string(''' on_release: popup.dismiss() ''') + +class TabbedPanelWithHiddenHeader(TabbedPanel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._tab_strip.opacity = 0 + + class RequestDialog(Factory.Popup): MODE_ADDRESS = 0 @@ -140,15 +168,7 @@ class RequestDialog(Factory.Popup): self.update_status() def on_mode(self, instance, x): - r = self.app.wallet.get_request(self.key) - if self.mode == self.MODE_ADDRESS: - self.data = r.get_address() or '' - elif self.mode == self.MODE_URI: - self.data = self.app.wallet.get_request_URI(r) or '' - elif self.mode == self.MODE_LIGHTNING: - self.data = r.lightning_invoice or '' - else: - raise Exception(f"unexpected {self.mode=!r}") + self.update_status() qr_data = self.data if self.mode == self.MODE_LIGHTNING: # encode lightning invoices as uppercase so QR encoding can use @@ -159,25 +179,47 @@ class RequestDialog(Factory.Popup): self.ids.qr.opacity = 1 else: self.ids.qr.opacity = 0 - self.update_status() + if not qr_data and self.error_text: + Clock.schedule_once(lambda dt: self.ids.qrdata_tabs.switch_to(self.ids.qrdata_tab_error)) + else: + Clock.schedule_once(lambda dt: self.ids.qrdata_tabs.switch_to(self.ids.qrdata_tab_qr)) def update_status(self): req = self.app.wallet.get_request(self.key) + help_texts = self.app.wallet.get_help_texts_for_receive_request(req) + address = req.get_address() or '' + URI = self.app.wallet.get_request_URI(req) or '' + lnaddr = req.lightning_invoice or '' self.status = self.app.wallet.get_request_status(self.key) self.status_str = req.get_status_str(self.status) self.status_color = pr_color[self.status] self.has_lightning = req.is_lightning() - warning = '' - if self.status == PR_UNPAID and self.mode == self.MODE_LIGHTNING and self.app.wallet.lnworker: - if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_receive(): - warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels') - if not self.mode == self.MODE_LIGHTNING: - address = req.get_address() - if not address: - warning = _('Warning') + ': ' + _('This request cannot be paid on-chain') - elif self.app.wallet.adb.is_used(address): - warning = _('Warning') + ': ' + _('This address is being reused') - self.warning = warning + + self.warning = '' + self.error_text = '' + self.data = '' + if self.mode == self.MODE_ADDRESS: + if help_texts.address_is_error: + self.error_text = help_texts.address_help + else: + self.data = address + self.warning = help_texts.address_help + elif self.mode == self.MODE_URI: + if help_texts.URI_is_error: + self.error_text = help_texts.URI_help + else: + self.data = URI + self.warning = help_texts.URI_help + elif self.mode == self.MODE_LIGHTNING: + if help_texts.ln_is_error: + self.error_text = help_texts.ln_help + else: + self.data = lnaddr + self.warning = help_texts.ln_help + else: + raise Exception(f"unexpected {self.mode=!r}") + if self.warning: + self.warning = _('Warning') + ': ' + self.warning def on_dismiss(self): self.app.request_popup = None diff --git a/electrum/gui/qt/receive_tab.py b/electrum/gui/qt/receive_tab.py index c248396ae..696a94161 100644 --- a/electrum/gui/qt/receive_tab.py +++ b/electrum/gui/qt/receive_tab.py @@ -216,49 +216,18 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): self.receive_lightning_e.setText('') self.receive_address_e.setText('') return - addr = req.get_address() or '' - amount_sat = req.get_amount_sat() or 0 - address_help = '' - URI_help = '' - lnaddr = req.lightning_invoice - URI = self.wallet.get_request_URI(req) or '' - lightning_online = self.wallet.lnworker and self.wallet.lnworker.num_peers() > 0 - can_receive_lightning = self.wallet.lnworker and amount_sat <= self.wallet.lnworker.num_sats_can_receive() - has_expired = self.wallet.get_request_status(key) == PR_EXPIRED - if not addr: - address_help = _('Amount too small to be received onchain') - if not URI: - URI_help = _('Amount too small to be received onchain') - if has_expired: - URI_help = ln_help = address_help = _('This request has expired') - URI = lnaddr = address = '' - can_rebalance = False - can_swap = False - elif lnaddr is None: - ln_help = _('This request does not have a Lightning invoice.') - lnaddr = '' - can_rebalance = False - can_swap = False - elif not lightning_online: - ln_help = _('You must be online to receive Lightning payments.') - lnaddr = '' - can_rebalance = False - can_swap = False - elif not can_receive_lightning: - self.receive_rebalance_button.suggestion = self.wallet.lnworker.suggest_rebalance_to_receive(amount_sat) - self.receive_swap_button.suggestion = self.wallet.lnworker.suggest_swap_to_receive(amount_sat) - can_rebalance = bool(self.receive_rebalance_button.suggestion) - can_swap = bool(self.receive_swap_button.suggestion) - lnaddr = '' - ln_help = _('You do not have the capacity to receive that amount with Lightning.') - if can_rebalance: - ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.') - elif can_swap: - ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.') - else: - ln_help = '' - can_rebalance = False - can_swap = False + help_texts = self.wallet.get_help_texts_for_receive_request(req) + addr = (req.get_address() or '') if not help_texts.address_is_error else '' + URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else '' + lnaddr = (req.lightning_invoice or '') if not help_texts.ln_is_error else '' + address_help = help_texts.address_help + URI_help = help_texts.URI_help + ln_help = help_texts.ln_help + can_rebalance = help_texts.can_rebalance() + can_swap = help_texts.can_swap() + self.receive_rebalance_button.suggestion = help_texts.ln_rebalance_suggestion + self.receive_swap_button.suggestion = help_texts.ln_swap_suggestion + self.receive_rebalance_button.setVisible(can_rebalance) self.receive_swap_button.setVisible(can_swap) self.receive_rebalance_button.setEnabled(can_rebalance and self.window.num_tasks() == 0) @@ -269,7 +238,6 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): # alphanumeric mode; resulting in smaller QR codes lnaddr_qr = lnaddr.upper() self.receive_address_e.setText(addr) - self.update_receive_address_styling() self.receive_address_qr.setData(addr) self.receive_address_help_text.setText(address_help) self.receive_URI_e.setText(URI) @@ -278,6 +246,9 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): self.receive_lightning_e.setText(lnaddr) # TODO maybe prepend "lightning:" ?? self.receive_lightning_help_text.setText(ln_help) self.receive_lightning_qr.setData(lnaddr_qr) + self.update_textedit_warning(text_e=self.receive_address_e, warning_text=address_help) + self.update_textedit_warning(text_e=self.receive_URI_e, warning_text=URI_help) + self.update_textedit_warning(text_e=self.receive_lightning_e, warning_text=ln_help) # macOS hack (similar to #4777) self.receive_lightning_e.repaint() self.receive_URI_e.repaint() @@ -387,15 +358,13 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger): self.expires_combo.show() self.request_list.clearSelection() - def update_receive_address_styling(self): - addr = str(self.receive_address_e.text()) - if is_address(addr) and self.wallet.adb.is_used(addr): - self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) - self.receive_address_e.setToolTip(_("This address has already been used. " - "For better privacy, do not reuse it for new payments.")) + def update_textedit_warning(self, *, text_e: ButtonsTextEdit, warning_text: Optional[str]): + if bool(text_e.text()) and warning_text: + text_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) + text_e.setToolTip(warning_text) else: - self.receive_address_e.setStyleSheet("") - self.receive_address_e.setToolTip("") + text_e.setStyleSheet("") + text_e.setToolTip(text_e._default_tooltip) class ReceiveTabWidget(QWidget): @@ -411,6 +380,7 @@ class ReceiveTabWidget(QWidget): for w in [textedit, qr]: w.mousePressEvent = receive_tab.toggle_receive_qr tooltip = _('Click to switch between text and QR code view') + w._default_tooltip = tooltip w.setToolTip(tooltip) textedit.setFocusPolicy(Qt.NoFocus) if isinstance(help_widget, QLabel): diff --git a/electrum/wallet.py b/electrum/wallet.py index 9be25875e..16923447e 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -244,6 +244,27 @@ class InternalAddressCorruption(Exception): return _("Wallet file corruption detected. " "Please restore your wallet from seed, and compare the addresses in both files") + +class ReceiveRequestHelp(NamedTuple): + # help texts (warnings/errors): + address_help: str + URI_help: str + ln_help: str + # whether the texts correspond to an error (or just a warning): + address_is_error: bool + URI_is_error: bool + ln_is_error: bool + + ln_swap_suggestion: Optional[Any] = None + ln_rebalance_suggestion: Optional[Any] = None + + def can_swap(self) -> bool: + return bool(self.ln_swap_suggestion) + + def can_rebalance(self) -> bool: + return bool(self.ln_rebalance_suggestion) + + class TxWalletDelta(NamedTuple): is_relevant: bool # "related to wallet?" is_any_input_ismine: bool @@ -2805,6 +2826,71 @@ class Abstract_Wallet(ABC, Logger, EventListener): else: return allow_send, long_warning, short_warning + def get_help_texts_for_receive_request(self, req: Invoice) -> ReceiveRequestHelp: + key = self.get_key_for_receive_request(req) + addr = req.get_address() or '' + amount_sat = req.get_amount_sat() or 0 + address_help = '' + URI_help = '' + ln_help = '' + address_is_error = False + URI_is_error = False + ln_is_error = False + ln_swap_suggestion = None + ln_rebalance_suggestion = None + lnaddr = req.lightning_invoice or '' + URI = self.get_request_URI(req) or '' + lightning_online = self.lnworker and self.lnworker.num_peers() > 0 + can_receive_lightning = self.lnworker and amount_sat <= self.lnworker.num_sats_can_receive() + status = self.get_request_status(key) + + if status == PR_EXPIRED: + address_help = URI_help = ln_help = _('This request has expired') + + is_amt_too_small_for_onchain = amount_sat < self.dust_threshold() + if not addr: + address_is_error = True + address_help = _('This request cannot be paid on-chain') + if is_amt_too_small_for_onchain: + address_help = _('Amount too small to be received onchain') + if not URI: + URI_is_error = True + URI_help = _('This request cannot be paid on-chain') + if is_amt_too_small_for_onchain: + URI_help = _('Amount too small to be received onchain') + if not lnaddr: + ln_is_error = True + ln_help = _('This request does not have a Lightning invoice.') + + if status == PR_UNPAID: + if self.adb.is_used(addr): + address_help = URI_help = (_("This address has already been used. " + "For better privacy, do not reuse it for new payments.")) + if lnaddr: + if not lightning_online: + ln_is_error = True + ln_help = _('You must be online to receive Lightning payments.') + elif not can_receive_lightning: + ln_is_error = True + ln_rebalance_suggestion = self.lnworker.suggest_rebalance_to_receive(amount_sat) + ln_swap_suggestion = self.lnworker.suggest_swap_to_receive(amount_sat) + ln_help = _('You do not have the capacity to receive this amount with Lightning.') + if bool(ln_rebalance_suggestion): + ln_help += '\n\n' + _('You may have that capacity if you rebalance your channels.') + elif bool(ln_swap_suggestion): + ln_help += '\n\n' + _('You may have that capacity if you swap some of your funds.') + return ReceiveRequestHelp( + address_help=address_help, + URI_help=URI_help, + ln_help=ln_help, + address_is_error=address_is_error, + URI_is_error=URI_is_error, + ln_is_error=ln_is_error, + ln_rebalance_suggestion=ln_rebalance_suggestion, + ln_swap_suggestion=ln_swap_suggestion, + ) + + def synchronize(self) -> int: """Returns the number of new addresses we generated.""" return 0