Browse Source

qt/kivy: receive tab: add wallet.ReceiveRequestHelp and refactor

master
SomberNight 3 years ago committed by ThomasV
parent
commit
c95791d7ee
  1. 84
      electrum/gui/kivy/uix/dialogs/request_dialog.py
  2. 74
      electrum/gui/qt/receive_tab.py
  3. 86
      electrum/wallet.py

84
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.app import App
from kivy.clock import Clock from kivy.clock import Clock
from kivy.properties import NumericProperty, StringProperty from kivy.properties import NumericProperty, StringProperty
from kivy.uix.tabbedpanel import TabbedPanel
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from electrum.invoices import pr_tooltips, pr_color from electrum.invoices import pr_tooltips, pr_color
@ -18,6 +19,10 @@ if TYPE_CHECKING:
Builder.load_string(''' Builder.load_string('''
#:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH #:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH
<TabbedPanelWithHiddenHeader@TabbedPanel>:
tab_height: "0dp"
tab_width: "1dp"
<RequestDialog@Popup> <RequestDialog@Popup>
id: popup id: popup
amount_str: '' amount_str: ''
@ -27,6 +32,7 @@ Builder.load_string('''
key:'' key:''
data:'' data:''
warning: '' warning: ''
error_text: ''
status_str: '' status_str: ''
status_color: 1,1,1,1 status_color: 1,1,1,1
shaded: False shaded: False
@ -39,6 +45,12 @@ Builder.load_string('''
size_hint: 1, 1 size_hint: 1, 1
padding: '10dp' padding: '10dp'
spacing: '10dp' spacing: '10dp'
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: QRCodeWidget:
id: qr id: qr
shaded: False shaded: False
@ -46,6 +58,15 @@ Builder.load_string('''
on_touch_down: on_touch_down:
touch = args[1] touch = args[1]
if self.collide_point(*touch.pos): self.shaded = not self.shaded 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: TopLabel:
text: root.data[0:70] + ('...' if len(root.data)>70 else '') text: root.data[0:70] + ('...' if len(root.data)>70 else '')
BoxLayout: BoxLayout:
@ -110,6 +131,13 @@ Builder.load_string('''
on_release: popup.dismiss() on_release: popup.dismiss()
''') ''')
class TabbedPanelWithHiddenHeader(TabbedPanel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._tab_strip.opacity = 0
class RequestDialog(Factory.Popup): class RequestDialog(Factory.Popup):
MODE_ADDRESS = 0 MODE_ADDRESS = 0
@ -140,15 +168,7 @@ class RequestDialog(Factory.Popup):
self.update_status() self.update_status()
def on_mode(self, instance, x): def on_mode(self, instance, x):
r = self.app.wallet.get_request(self.key) self.update_status()
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}")
qr_data = self.data qr_data = self.data
if self.mode == self.MODE_LIGHTNING: if self.mode == self.MODE_LIGHTNING:
# encode lightning invoices as uppercase so QR encoding can use # encode lightning invoices as uppercase so QR encoding can use
@ -159,25 +179,47 @@ class RequestDialog(Factory.Popup):
self.ids.qr.opacity = 1 self.ids.qr.opacity = 1
else: else:
self.ids.qr.opacity = 0 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): def update_status(self):
req = self.app.wallet.get_request(self.key) 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 = self.app.wallet.get_request_status(self.key)
self.status_str = req.get_status_str(self.status) self.status_str = req.get_status_str(self.status)
self.status_color = pr_color[self.status] self.status_color = pr_color[self.status]
self.has_lightning = req.is_lightning() self.has_lightning = req.is_lightning()
warning = ''
if self.status == PR_UNPAID and self.mode == self.MODE_LIGHTNING and self.app.wallet.lnworker: self.warning = ''
if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_receive(): self.error_text = ''
warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels') self.data = ''
if not self.mode == self.MODE_LIGHTNING: if self.mode == self.MODE_ADDRESS:
address = req.get_address() if help_texts.address_is_error:
if not address: self.error_text = help_texts.address_help
warning = _('Warning') + ': ' + _('This request cannot be paid on-chain') else:
elif self.app.wallet.adb.is_used(address): self.data = address
warning = _('Warning') + ': ' + _('This address is being reused') self.warning = help_texts.address_help
self.warning = warning 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): def on_dismiss(self):
self.app.request_popup = None self.app.request_popup = None

74
electrum/gui/qt/receive_tab.py

@ -216,49 +216,18 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
self.receive_lightning_e.setText('') self.receive_lightning_e.setText('')
self.receive_address_e.setText('') self.receive_address_e.setText('')
return return
addr = req.get_address() or '' help_texts = self.wallet.get_help_texts_for_receive_request(req)
amount_sat = req.get_amount_sat() or 0 addr = (req.get_address() or '') if not help_texts.address_is_error else ''
address_help = '' URI = (self.wallet.get_request_URI(req) or '') if not help_texts.URI_is_error else ''
URI_help = '' lnaddr = (req.lightning_invoice or '') if not help_texts.ln_is_error else ''
lnaddr = req.lightning_invoice address_help = help_texts.address_help
URI = self.wallet.get_request_URI(req) or '' URI_help = help_texts.URI_help
lightning_online = self.wallet.lnworker and self.wallet.lnworker.num_peers() > 0 ln_help = help_texts.ln_help
can_receive_lightning = self.wallet.lnworker and amount_sat <= self.wallet.lnworker.num_sats_can_receive() can_rebalance = help_texts.can_rebalance()
has_expired = self.wallet.get_request_status(key) == PR_EXPIRED can_swap = help_texts.can_swap()
if not addr: self.receive_rebalance_button.suggestion = help_texts.ln_rebalance_suggestion
address_help = _('Amount too small to be received onchain') self.receive_swap_button.suggestion = help_texts.ln_swap_suggestion
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
self.receive_rebalance_button.setVisible(can_rebalance) self.receive_rebalance_button.setVisible(can_rebalance)
self.receive_swap_button.setVisible(can_swap) self.receive_swap_button.setVisible(can_swap)
self.receive_rebalance_button.setEnabled(can_rebalance and self.window.num_tasks() == 0) 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 # alphanumeric mode; resulting in smaller QR codes
lnaddr_qr = lnaddr.upper() lnaddr_qr = lnaddr.upper()
self.receive_address_e.setText(addr) self.receive_address_e.setText(addr)
self.update_receive_address_styling()
self.receive_address_qr.setData(addr) self.receive_address_qr.setData(addr)
self.receive_address_help_text.setText(address_help) self.receive_address_help_text.setText(address_help)
self.receive_URI_e.setText(URI) 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_e.setText(lnaddr) # TODO maybe prepend "lightning:" ??
self.receive_lightning_help_text.setText(ln_help) self.receive_lightning_help_text.setText(ln_help)
self.receive_lightning_qr.setData(lnaddr_qr) 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) # macOS hack (similar to #4777)
self.receive_lightning_e.repaint() self.receive_lightning_e.repaint()
self.receive_URI_e.repaint() self.receive_URI_e.repaint()
@ -387,15 +358,13 @@ class ReceiveTab(QWidget, MessageBoxMixin, Logger):
self.expires_combo.show() self.expires_combo.show()
self.request_list.clearSelection() self.request_list.clearSelection()
def update_receive_address_styling(self): def update_textedit_warning(self, *, text_e: ButtonsTextEdit, warning_text: Optional[str]):
addr = str(self.receive_address_e.text()) if bool(text_e.text()) and warning_text:
if is_address(addr) and self.wallet.adb.is_used(addr): text_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True))
self.receive_address_e.setStyleSheet(ColorScheme.RED.as_stylesheet(True)) text_e.setToolTip(warning_text)
self.receive_address_e.setToolTip(_("This address has already been used. "
"For better privacy, do not reuse it for new payments."))
else: else:
self.receive_address_e.setStyleSheet("") text_e.setStyleSheet("")
self.receive_address_e.setToolTip("") text_e.setToolTip(text_e._default_tooltip)
class ReceiveTabWidget(QWidget): class ReceiveTabWidget(QWidget):
@ -411,6 +380,7 @@ class ReceiveTabWidget(QWidget):
for w in [textedit, qr]: for w in [textedit, qr]:
w.mousePressEvent = receive_tab.toggle_receive_qr w.mousePressEvent = receive_tab.toggle_receive_qr
tooltip = _('Click to switch between text and QR code view') tooltip = _('Click to switch between text and QR code view')
w._default_tooltip = tooltip
w.setToolTip(tooltip) w.setToolTip(tooltip)
textedit.setFocusPolicy(Qt.NoFocus) textedit.setFocusPolicy(Qt.NoFocus)
if isinstance(help_widget, QLabel): if isinstance(help_widget, QLabel):

86
electrum/wallet.py

@ -244,6 +244,27 @@ class InternalAddressCorruption(Exception):
return _("Wallet file corruption detected. " return _("Wallet file corruption detected. "
"Please restore your wallet from seed, and compare the addresses in both files") "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): class TxWalletDelta(NamedTuple):
is_relevant: bool # "related to wallet?" is_relevant: bool # "related to wallet?"
is_any_input_ismine: bool is_any_input_ismine: bool
@ -2805,6 +2826,71 @@ class Abstract_Wallet(ABC, Logger, EventListener):
else: else:
return allow_send, long_warning, short_warning 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: def synchronize(self) -> int:
"""Returns the number of new addresses we generated.""" """Returns the number of new addresses we generated."""
return 0 return 0

Loading…
Cancel
Save