qt/kivy: receive tab: add wallet.ReceiveRequestHelp and refactor
This commit is contained in:
@@ -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,13 +45,28 @@ Builder.load_string('''
|
|||||||
size_hint: 1, 1
|
size_hint: 1, 1
|
||||||
padding: '10dp'
|
padding: '10dp'
|
||||||
spacing: '10dp'
|
spacing: '10dp'
|
||||||
QRCodeWidget:
|
TabbedPanelWithHiddenHeader:
|
||||||
id: qr
|
id: qrdata_tabs
|
||||||
shaded: False
|
do_default_tab: False
|
||||||
foreground_color: (0, 0, 0, 0.5) if self.shaded else (0, 0, 0, 0)
|
TabbedPanelItem:
|
||||||
on_touch_down:
|
id: qrdata_tab_qr
|
||||||
touch = args[1]
|
border: 0,0,0,0 # to hide visual artifact around hidden tab header
|
||||||
if self.collide_point(*touch.pos): self.shaded = not self.shaded
|
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:
|
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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user