diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py index d95b668a9..ae9f82f0a 100644 --- a/electrum/gui/kivy/main_window.py +++ b/electrum/gui/kivy/main_window.py @@ -505,17 +505,17 @@ class ElectrumWindow(App, Logger): tab = self.tabs.ids[name + '_tab'] panel.switch_to(tab) - def show_request(self, is_lightning, key): + def show_request(self, key): from .uix.dialogs.request_dialog import RequestDialog self.request_popup = RequestDialog('Request', key) self.request_popup.open() - def show_invoice(self, is_lightning, key): + def show_invoice(self, key): from .uix.dialogs.invoice_dialog import InvoiceDialog invoice = self.wallet.get_invoice(key) if not invoice: return - data = invoice.invoice if is_lightning else key + data = invoice.lightning_invoice if invoice.is_lightning() else key self.invoice_popup = InvoiceDialog('Invoice', data, key) self.invoice_popup.open() diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py index f6ad2b6c0..3b52900e8 100644 --- a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py @@ -8,7 +8,7 @@ from kivy.clock import Clock from electrum.gui.kivy.i18n import _ from electrum.invoices import pr_tooltips, pr_color -from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN +from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED if TYPE_CHECKING: from electrum.gui.kivy.main_window import ElectrumWindow diff --git a/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py b/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py index c361bec5d..bdd588d73 100644 --- a/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/lightning_tx_dialog.py @@ -13,7 +13,7 @@ from kivy.uix.dropdown import DropDown from kivy.uix.button import Button from electrum.gui.kivy.i18n import _ -from electrum.invoices import LNInvoice +from electrum.invoices import Invoice if TYPE_CHECKING: @@ -121,7 +121,6 @@ class LightningTxDialog(Factory.Popup): invoice = (self.app.wallet.get_invoice(self.payment_hash) or self.app.wallet.get_request(self.payment_hash)) if invoice: - assert isinstance(invoice, LNInvoice), f"{self.invoice!r}" - self.invoice = invoice.invoice + self.invoice = invoice.lightning_invoice or '' else: self.invoice = '' diff --git a/electrum/gui/kivy/uix/dialogs/request_dialog.py b/electrum/gui/kivy/uix/dialogs/request_dialog.py index 151fde96f..af5722551 100644 --- a/electrum/gui/kivy/uix/dialogs/request_dialog.py +++ b/electrum/gui/kivy/uix/dialogs/request_dialog.py @@ -5,15 +5,22 @@ from kivy.lang import Builder from kivy.core.clipboard import Clipboard from kivy.app import App from kivy.clock import Clock +from kivy.properties import NumericProperty, StringProperty from electrum.gui.kivy.i18n import _ from electrum.invoices import pr_tooltips, pr_color -from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN +from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED if TYPE_CHECKING: from ...main_window import ElectrumWindow + +MODE_ADDRESS = 0 +MODE_URI = 1 +MODE_LIGHTNING = 2 + + Builder.load_string(''' #:import KIVY_GUI_PATH electrum.gui.kivy.KIVY_GUI_PATH @@ -22,8 +29,9 @@ Builder.load_string(''' amount_str: '' title: '' description:'' - is_lightning: False + mode:0 key:'' + data:'' warning: '' status_str: '' status_color: 1,1,1,1 @@ -43,15 +51,37 @@ Builder.load_string(''' on_touch_down: touch = args[1] if self.collide_point(*touch.pos): self.shaded = not self.shaded + TopLabel: + text: root.data[0:70] + ('...' if len(root.data)>70 else '') + BoxLayout: + size_hint: 1, None + height: '48dp' + ToggleButton: + id: b0 + group:'g' + size_hint: 1, None + height: '48dp' + text: _('Address') + on_release: root.mode = 0 + ToggleButton: + id: b1 + group:'g' + size_hint: 1, None + height: '48dp' + text: _('URI') + on_release: root.mode = 1 + state: 'down' + ToggleButton: + id: b2 + group:'g' + size_hint: 1, None + height: '48dp' + text: _('Lightning') + on_release: root.mode = 2 TopLabel: text: _('Description') + ': ' + root.description or _('None') TopLabel: text: _('Amount') + ': ' + root.amount_str - TopLabel: - text: (_('Address') if not root.is_lightning else _('Payment hash')) + ': ' - RefLabel: - data: root.key - name: (_('Address') if not root.is_lightning else _('Payment hash')) TopLabel: text: _('Status') + ': ' + root.status_str color: root.status_color @@ -87,6 +117,9 @@ Builder.load_string(''' class RequestDialog(Factory.Popup): + mode = NumericProperty(0) + data = StringProperty('') + def __init__(self, title, key): self.status = PR_UNKNOWN Factory.Popup.__init__(self) @@ -94,33 +127,50 @@ class RequestDialog(Factory.Popup): self.title = title self.key = key r = self.app.wallet.get_request(key) - self.is_lightning = r.is_lightning() - self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r) self.amount_sat = r.get_amount_sat() self.amount_str = self.app.format_amount_and_units(self.amount_sat) self.description = r.message + self.mode = 1 + self.on_mode(0, 0) + self.ids.b0.pressed = True self.update_status() - def on_open(self): - data = self.data - if self.is_lightning: + def on_mode(self, instance, x): + r = self.app.wallet.get_request(self.key) + if self.mode == MODE_ADDRESS: + self.data = r.get_address() or '' + elif self.mode == MODE_URI: + self.data = self.app.wallet.get_request_URI(r) or '' + else: + self.data = r.lightning_invoice or '' + qr_data = self.data + if self.mode == MODE_LIGHTNING: # encode lightning invoices as uppercase so QR encoding can use # alphanumeric mode; resulting in smaller QR codes - data = data.upper() - self.ids.qr.set_data(data) + qr_data = qr_data.upper() + if qr_data: + self.ids.qr.set_data(qr_data) + self.ids.qr.opacity = 1 + else: + self.ids.qr.opacity = 0 + self.update_status() def update_status(self): req = self.app.wallet.get_request(self.key) 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] - if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker: + warning = '' + if self.status == PR_UNPAID and self.mode == MODE_LIGHTNING and self.app.wallet.lnworker: if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_receive(): - self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels') - if self.status == PR_UNPAID and not self.is_lightning: + warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels') + if not self.mode == MODE_LIGHTNING: address = req.get_address() - if self.app.wallet.is_used(address): - self.warning = _('Warning') + ': ' + _('This address is being reused') + if not address: + warning = _('Warning') + ': ' + _('This request cannot be paid on-chain') + elif self.app.wallet.is_used(address): + warning = _('Warning') + ': ' + _('This address is being reused') + self.warning = warning def on_dismiss(self): self.app.request_popup = None diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index a3571c143..7c6439f80 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -9,10 +9,11 @@ from kivy.properties import ObjectProperty from kivy.lang import Builder from kivy.factory import Factory from kivy.uix.recycleview import RecycleView +from kivy.properties import StringProperty -from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, +from electrum.invoices import (PR_DEFAULT_EXPIRATION_WHEN_CREATING, PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, - LNInvoice, pr_expiration_values, Invoice, OnchainInvoice) + pr_expiration_values, Invoice) from electrum import bitcoin, constants from electrum.transaction import tx_from_any, PartialTxOutput from electrum.util import (parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice, @@ -224,15 +225,14 @@ class SendScreen(CScreen, Logger): payments_container.refresh_from_data() def show_item(self, obj): - self.app.show_invoice(obj.is_lightning, obj.key) + self.app.show_invoice(obj.key) def get_card(self, item: Invoice) -> Dict[str, Any]: status = self.app.wallet.get_invoice_status(item) status_str = item.get_status_str(status) - is_lightning = item.type == PR_TYPE_LN + is_lightning = item.is_lightning() key = self.app.wallet.get_key_for_outgoing_invoice(item) if is_lightning: - assert isinstance(item, LNInvoice) address = item.rhash if self.app.wallet.lnworker: log = self.app.wallet.lnworker.logs.get(key) @@ -240,7 +240,6 @@ class SendScreen(CScreen, Logger): status_str += '... (%d)'%len(log) is_bip70 = False else: - assert isinstance(item, OnchainInvoice) address = item.get_address() is_bip70 = bool(item.bip70) return { @@ -313,7 +312,7 @@ class SendScreen(CScreen, Logger): message = self.message try: if self.is_lightning: - return LNInvoice.from_bech32(address) + return Invoice.from_bech32(address) else: # on-chain if self.payment_request: outputs = self.payment_request.get_outputs() @@ -358,10 +357,11 @@ class SendScreen(CScreen, Logger): else: self._do_pay_onchain(invoice) - def _do_pay_lightning(self, invoice: LNInvoice, pw) -> None: + def _do_pay_lightning(self, invoice: Invoice, pw) -> None: + amount_msat = invoice.get_amount_msat() def pay_thread(): try: - coro = self.app.wallet.lnworker.pay_invoice(invoice.invoice, attempts=10) + coro = self.app.wallet.lnworker.pay_invoice(invoice.lightning_invoice, amount_msat=amount_msat, attempts=10) fut = asyncio.run_coroutine_threadsafe(coro, self.app.network.asyncio_loop) fut.result() except Exception as e: @@ -369,7 +369,7 @@ class SendScreen(CScreen, Logger): self.save_invoice(invoice) threading.Thread(target=pay_thread).start() - def _do_pay_onchain(self, invoice: OnchainInvoice) -> None: + def _do_pay_onchain(self, invoice: Invoice) -> None: outputs = invoice.outputs amount = sum(map(lambda x: x.value, outputs)) if not any(parse_max_spend(x.value) for x in outputs) else '!' coins = self.app.wallet.get_spendable_coins(None) @@ -399,11 +399,17 @@ class SendScreen(CScreen, Logger): class ReceiveScreen(CScreen): kvname = 'receive' + expiration_text = StringProperty('') def __init__(self, **kwargs): super(ReceiveScreen, self).__init__(**kwargs) Clock.schedule_interval(lambda dt: self.update(), 5) self.is_max = False # not used for receiving (see app.amount_dialog) + self.expiration_text = pr_expiration_values[self.expiry()] + + def on_open(self): + c = self.expiry() + self.expiration_text = pr_expiration_values[c] def expiry(self): return self.app.electrum_config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) @@ -437,45 +443,39 @@ class ReceiveScreen(CScreen): self.app._clipboard.copy(uri) self.app.show_info(_('Request copied to clipboard')) - def new_request(self, lightning): - amount = self.amount - amount = self.app.get_amount(amount) if amount else 0 + def new_request(self): + amount_str = self.amount + amount_sat = self.app.get_amount(amount_str) if amount_str else 0 message = self.message - lnworker = self.app.wallet.lnworker - try: - if lightning: - if lnworker: - key = lnworker.add_request(amount, message, self.expiry()) + expiry = self.expiry() + if amount_sat and amount_sat < self.wallet.dust_threshold(): + self.address = '' + if not self.app.wallet.has_lightning(): + return + else: + addr = self.address or self.app.wallet.get_unused_address() + if not addr: + if not self.app.wallet.is_deterministic(): + addr = self.app.wallet.get_receiving_address() else: - self.app.show_error(_("Lightning payments are not available for this wallet")) + self.app.show_info(_('No address available. Please remove some of your pending requests.')) return - else: - addr = self.address or self.app.wallet.get_unused_address() - if not addr: - if not self.app.wallet.is_deterministic(): - addr = self.app.wallet.get_receiving_address() - else: - self.app.show_info(_('No address available. Please remove some of your pending requests.')) - return - self.address = addr - req = self.app.wallet.make_payment_request(addr, amount, message, self.expiry()) - self.app.wallet.add_payment_request(req) - key = addr + self.address = addr + try: + key = self.app.wallet.create_request(amount_sat, message, expiry, self.address, lightning=self.app.wallet.has_lightning()) except InvoiceError as e: self.app.show_error(_('Error creating payment request') + ':\n' + str(e)) return self.clear() self.update() - self.app.show_request(lightning, key) + self.app.show_request(key) def get_card(self, req: Invoice) -> Dict[str, Any]: is_lightning = req.is_lightning() if not is_lightning: - assert isinstance(req, OnchainInvoice) address = req.get_address() else: - assert isinstance(req, LNInvoice) - address = req.invoice + address = req.lightning_invoice key = self.app.wallet.get_key_for_receive_request(req) amount = req.get_amount_sat() description = req.message @@ -513,12 +513,13 @@ class ReceiveScreen(CScreen): payments_container.refresh_from_data() def show_item(self, obj): - self.app.show_request(obj.is_lightning, obj.key) + self.app.show_request(obj.key) def expiration_dialog(self, obj): from .dialogs.choice_dialog import ChoiceDialog def callback(c): self.app.electrum_config.set_key('request_expiry', c) + self.expiration_text = pr_expiration_values[c] d = ChoiceDialog(_('Expiration date'), pr_expiration_values, self.expiry(), callback) d.open() diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv index 754964d4c..89d1029a8 100644 --- a/electrum/gui/kivy/uix/ui_screens/receive.kv +++ b/electrum/gui/kivy/uix/ui_screens/receive.kv @@ -67,7 +67,7 @@ amount: '' message: '' status: '' - is_lightning: False + expiration_text: _('Expiry') BoxLayout padding: '12dp', '12dp', '12dp', '12dp' @@ -83,33 +83,34 @@ height: blue_bottom.item_height spacing: '5dp' Image: - source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/lightning' if root.is_lightning else f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/globe' + source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/calculator' + opacity: 0.7 size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} BlueButton: - id: address_label - text: _('Lightning') if root.is_lightning else (s.address if s.address else _('Bitcoin Address')) - shorten: True - on_release: root.is_lightning = not root.is_lightning if app.wallet.has_lightning() else False + id: amount_label + default_text: _('Amount') + text: s.amount if s.amount else _('Amount') + on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, False)) CardSeparator: opacity: message_selection.opacity color: blue_bottom.foreground_color BoxLayout: + id: message_selection + opacity: 1 size_hint: 1, None height: blue_bottom.item_height spacing: '5dp' Image: - source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/calculator' - opacity: 0.7 + source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/pen' size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} BlueButton: - id: amount_label - default_text: _('Amount') - text: s.amount if s.amount else _('Amount') - on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, False)) + id: description + text: s.message if s.message else _('Description') + on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) CardSeparator: opacity: message_selection.opacity color: blue_bottom.foreground_color @@ -120,32 +121,27 @@ height: blue_bottom.item_height spacing: '5dp' Image: - source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/pen' + source: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/clock1' size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} BlueButton: id: description - text: s.message if s.message else _('Description') - on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) + text: s.expiration_text + on_release: Clock.schedule_once(lambda dt: s.expiration_dialog(s)) BoxLayout: size_hint: 1, None height: '48dp' - IconButton: - icon: f'atlas://{KIVY_GUI_PATH}/theming/atlas/light/clock1' - size_hint: 0.5, None - height: '48dp' - on_release: Clock.schedule_once(lambda dt: s.expiration_dialog(s)) Button: text: _('Clear') size_hint: 1, None height: '48dp' on_release: Clock.schedule_once(lambda dt: s.clear()) Button: - text: _('Request') + text: _('New Request') size_hint: 1, None height: '48dp' - on_release: Clock.schedule_once(lambda dt: s.new_request(root.is_lightning)) + on_release: Clock.schedule_once(lambda dt: s.new_request()) Widget: size_hint: 1, 0.1 RequestRecycleView: