From 7102fb732e4ee7064f81a62bf61628a83e1e731d Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 29 Mar 2022 18:17:38 +0200 Subject: [PATCH] follow-up prev: - detect payment of requests both onchain or LN - create single type of requests in GUI --- electrum/commands.py | 3 +- electrum/gui/qt/main_window.py | 55 +++++++++-------------- electrum/invoices.py | 22 ++++++++- electrum/lnaddr.py | 6 ++- electrum/lnworker.py | 13 +----- electrum/submarine_swaps.py | 1 + electrum/wallet.py | 81 ++++++++++++++++++++-------------- 7 files changed, 97 insertions(+), 84 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 84b593481..730102946 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -913,8 +913,7 @@ class Commands: return False amount = satoshis(amount) expiration = int(expiration) if expiration else None - req = wallet.make_payment_request(addr, amount, memo, expiration) - wallet.add_payment_request(req) + req = wallet.create_request(amount, memo, expiration, addr, False) return wallet.export_request(req) @command('wnl') diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index f1cdb292a..af725106c 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1151,20 +1151,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): self.clear_invoice_button = QPushButton(_('Clear')) self.clear_invoice_button.clicked.connect(self.clear_receive_tab) - self.create_invoice_button = QPushButton(_('New Address')) - self.create_invoice_button.setIcon(read_QIcon("bitcoin.png")) - self.create_invoice_button.setToolTip('Create on-chain request') - self.create_invoice_button.clicked.connect(lambda: self.create_invoice(False)) + self.create_invoice_button = QPushButton(_('Create Request')) + self.create_invoice_button.clicked.connect(lambda: self.create_invoice()) self.receive_buttons = buttons = QHBoxLayout() buttons.addStretch(1) buttons.addWidget(self.clear_invoice_button) buttons.addWidget(self.create_invoice_button) - if self.wallet.has_lightning(): - self.create_lightning_invoice_button = QPushButton(_('Lightning')) - self.create_lightning_invoice_button.setToolTip('Create lightning request') - self.create_lightning_invoice_button.setIcon(read_QIcon("lightning.png")) - self.create_lightning_invoice_button.clicked.connect(lambda: self.create_invoice(True)) - buttons.addWidget(self.create_lightning_invoice_button) grid.addLayout(buttons, 4, 0, 1, -1) self.receive_payreq_e = ButtonsTextEdit() @@ -1262,27 +1254,31 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): else: return - def create_invoice(self, is_lightning: bool): - amount = self.receive_amount_e.get_amount() + def create_invoice(self): + amount_sat = self.receive_amount_e.get_amount() message = self.receive_message_e.text() expiry = self.config.get('request_expiry', PR_DEFAULT_EXPIRATION_WHEN_CREATING) + + if amount_sat and amount_sat < self.wallet.dust_threshold(): + address = None + if not self.wallet.has_lightning(): + return + else: + address = self.get_bitcoin_address_for_request(amount_sat) + if not address: + return + self.address_list.update() + + # generate even if we cannot receive + lightning = self.wallet.has_lightning() try: - if is_lightning: - if not self.wallet.lnworker.channels: - self.show_error(_("You need to open a Lightning channel first.")) - return - # TODO maybe show a warning if amount exceeds lnworker.num_sats_can_receive (as in kivy) - key = self.wallet.lnworker.add_request(amount, message, expiry) - else: - key = self.create_bitcoin_request(amount, message, expiry) - if not key: - return - self.address_list.refresh_all() + key = self.wallet.create_request(amount_sat, message, expiry, address, lightning=lightning) except InvoiceError as e: self.show_error(_('Error creating payment request') + ':\n' + str(e)) return assert key is not None + self.address_list.refresh_all() self.request_list.update() self.request_list.select_key(key) # clear request fields @@ -1291,10 +1287,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): # copy to clipboard r = self.wallet.get_request(key) content = r.lightning_invoice if r.is_lightning() else r.get_address() - title = _('Invoice') if is_lightning else _('Address') + title = _('Invoice') if r.is_lightning() else _('Address') self.do_copy(content, title=title) - def create_bitcoin_request(self, amount: int, message: str, expiration: int) -> Optional[str]: + def get_bitcoin_address_for_request(self, amount: int) -> Optional[str]: addr = self.wallet.get_unused_address() if addr is None: if not self.wallet.is_deterministic(): # imported wallet @@ -1311,15 +1307,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger): if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")): return addr = self.wallet.create_new_address(False) - timestamp = int(time.time()) - req = self.wallet.make_payment_request(amount, message, timestamp, expiration, address=addr) - try: - self.wallet.add_payment_request(req) - except Exception as e: - self.logger.exception('Error adding payment request') - self.show_error(_('Error adding payment request') + ':\n' + repr(e)) - else: - self.sign_payment_request(addr) return addr def do_copy(self, content: str, *, title: str = None) -> None: diff --git a/electrum/invoices.py b/electrum/invoices.py index 68febe271..a745d75a1 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -10,6 +10,7 @@ from .util import age, InvoiceError from .lnaddr import lndecode, LnAddr from . import constants from .bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC +from .bitcoin import address_to_script from .transaction import PartialTxOutput if TYPE_CHECKING: @@ -116,7 +117,18 @@ class Invoice(StoredObject): def get_address(self) -> str: """returns the first address, to be displayed in GUI""" - return self.outputs[0].address + if self.is_lightning(): + return self._lnaddr.get_fallback_address() or None + else: + return self.outputs[0].address + + def get_outputs(self): + if self.is_lightning(): + address = self.get_address() + outputs = [PartialTxOutput.from_address_and_value(address, int(self.get_amount_sat()))] if address else [] + else: + outputs = self.outputs + return outputs def get_expiration_date(self): # 0 means never @@ -141,6 +153,14 @@ class Invoice(StoredObject): return None return int(amount_msat / 1000) + def get_bip21_URI(self): + from electrum.util import create_bip21_uri + addr = self.get_address() + amount = int(self.get_amount_sat()) + message = self.message + uri = create_bip21_uri(addr, amount, message) + return str(uri) + @lightning_invoice.validator def _validate_invoice_str(self, attribute, value): if value is not None: diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py index bbac87ef8..f78defec8 100644 --- a/electrum/lnaddr.py +++ b/electrum/lnaddr.py @@ -211,7 +211,8 @@ def lnencode(addr: 'LnAddr', privkey) -> str: route = bitstring.BitArray(pubkey) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv) data += tagged('t', route) elif k == 'f': - data += encode_fallback(v, addr.net) + if v is not None: + data += encode_fallback(v, addr.net) elif k == 'd': # truncate to max length: 1024*5 bits = 639 bytes data += tagged_bytes('d', v.encode()[0:639]) @@ -336,6 +337,9 @@ class LnAddr(object): def get_description(self) -> str: return self.get_tag('d') or '' + def get_fallback_address(self) -> str: + return self.get_tag('f') or '' + def get_expiry(self) -> int: exp = self.get_tag('x') if exp is None: diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 55e741d3f..bd94cce3e 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1793,18 +1793,7 @@ class LNWallet(LNWorker): expiry=expiry, write_to_disk=False, ) - req = self.wallet.make_payment_request( - amount_sat, - message, - timestamp, - expiry, - address=None, - lightning_invoice=invoice - ) - key = self.wallet.add_payment_request(req, write_to_disk=False) - self.wallet.set_label(key, message) - self.wallet.save_db() - return key + return invoice def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True): assert sha256(preimage) == payment_hash diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 297fd1478..4c3d7dc2a 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -278,6 +278,7 @@ class SwapManager(Logger): amount_msat=lightning_amount_sat * 1000, message='swap', expiry=3600 * 24, + fallback_address=None, ) payment_hash = lnaddr.paymenthash preimage = self.lnworker.get_preimage(payment_hash) diff --git a/electrum/wallet.py b/electrum/wallet.py index 6804de20f..eed2d5254 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -789,7 +789,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if self.is_onchain_invoice_paid(invoice, 0): self.logger.info("saving invoice... but it is already paid!") with self.transaction_lock: - for txout in invoice.outputs: + for txout in invoice.get_outputs(): self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key) self.invoices[key] = invoice self.save_db() @@ -854,15 +854,18 @@ class Abstract_Wallet(AddressSynchronizer, ABC): # scriptpubkey -> list(invoice_keys) self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]] for invoice_key, invoice in self.invoices.items(): - if not invoice.is_lightning(): - for txout in invoice.outputs: - self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key) + if invoice.is_lightning() and not invoice.get_address(): + continue + for txout in invoice.get_outputs(): + self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key) def _is_onchain_invoice_paid(self, invoice: Invoice, conf: int) -> Tuple[bool, Sequence[str]]: """Returns whether on-chain invoice is satisfied, and list of relevant TXIDs.""" - assert not invoice.is_lightning() + if invoice.is_lightning() and not invoice.get_address(): + return False, [] + outputs = invoice.get_outputs() invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats - for txo in invoice.outputs: # type: PartialTxOutput + for txo in outputs: # type: PartialTxOutput invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value relevant_txs = [] with self.lock, self.transaction_lock: @@ -2048,6 +2051,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): def get_unused_addresses(self) -> Sequence[str]: domain = self.get_receiving_addresses() # TODO we should index receive_requests by id + # add lightning requests. (use as key) in_use_by_request = [k for k in self.receive_requests.keys() if self.get_request_status(k) != PR_EXPIRED] in_use_by_request = set(in_use_by_request) @@ -2116,6 +2120,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC): return False, None def get_request_URI(self, req: Invoice) -> str: + # todo: should be a method of invoice? addr = req.get_address() message = self.get_label(addr) amount = req.get_amount_sat() @@ -2139,31 +2144,34 @@ class Abstract_Wallet(AddressSynchronizer, ABC): return status def get_invoice_status(self, invoice: Invoice): - if invoice.is_lightning(): - status = self.lnworker.get_invoice_status(invoice) if self.lnworker else PR_UNKNOWN + # lightning invoices can be paid onchain + if invoice.is_lightning() and self.lnworker: + status = self.lnworker.get_invoice_status(invoice) + if status != PR_UNPAID: + return self.check_expired_status(invoice, status) + if self.is_onchain_invoice_paid(invoice, 1): + status = PR_PAID + elif self.is_onchain_invoice_paid(invoice, 0): + status = PR_UNCONFIRMED else: - if self.is_onchain_invoice_paid(invoice, 1): - status =PR_PAID - elif self.is_onchain_invoice_paid(invoice, 0): - status = PR_UNCONFIRMED - else: - status = PR_UNPAID + status = PR_UNPAID return self.check_expired_status(invoice, status) def get_request_status(self, key): r = self.get_request(key) if r is None: return PR_UNKNOWN - if r.is_lightning(): - status = self.lnworker.get_payment_status(bfh(r.rhash)) if self.lnworker else PR_UNKNOWN + if r.is_lightning() and self.lnworker: + status = self.lnworker.get_payment_status(bfh(r.rhash)) + if status != PR_UNPAID: + return self.check_expired_status(r, status) + paid, conf = self.get_onchain_request_status(r) + if not paid: + status = PR_UNPAID + elif conf == 0: + status = PR_UNCONFIRMED else: - paid, conf = self.get_onchain_request_status(r) - if not paid: - status = PR_UNPAID - elif conf == 0: - status = PR_UNCONFIRMED - else: - status = PR_PAID + status = PR_PAID return self.check_expired_status(r, status) def get_request(self, key): @@ -2268,23 +2276,26 @@ class Abstract_Wallet(AddressSynchronizer, ABC): status = self.get_request_status(addr) util.trigger_callback('request_status', self, addr, status) - def make_payment_request(self, amount_sat, message, timestamp, expiration, address=None, lightning_invoice=None): - # TODO maybe merge with wallet.create_invoice()... - # note that they use incompatible "id" + def create_request(self, amount_sat: int, message: str, exp_delay: int, address: str, lightning: bool): + # for receiving amount_sat = amount_sat or 0 - #_id = bh2u(sha256d(address + "%d"%timestamp))[0:10] - expiration = expiration or 0 - outputs=[PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] - return Invoice( + exp_delay = exp_delay or 0 + timestamp = int(time.time()) + lightning_invoice = self.lnworker.add_request(amount_sat, message, exp_delay) if lightning else None + outputs = [ PartialTxOutput.from_address_and_value(address, amount_sat)] if address else [] + height = self.get_local_height() + req = Invoice( outputs=outputs, message=message, time=timestamp, amount_msat=amount_sat*1000, - exp=expiration, - height=self.get_local_height(), + exp=exp_delay, + height=height, bip70=None, lightning_invoice=lightning_invoice, ) + key = self.add_payment_request(req, write_to_disk=False) + return key def sign_payment_request(self, key, alias, alias_addr, password): # FIXME this is broken raise @@ -2304,11 +2315,12 @@ class Abstract_Wallet(AddressSynchronizer, ABC): if invoice.is_lightning(): key = invoice.rhash else: - key = bh2u(sha256d(repr(invoice.outputs) + "%d"%invoice.time))[0:10] + key = bh2u(sha256d(repr(invoice.get_outputs()) + "%d"%invoice.time))[0:10] return key def get_key_for_receive_request(self, req: Invoice, *, sanity_checks: bool = False) -> str: """Return the key to use for this invoice in self.receive_requests.""" + # FIXME: this should be a method of Invoice if not req.is_lightning(): addr = req.get_address() if sanity_checks: @@ -2318,7 +2330,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC): raise Exception(_('Address not in wallet.')) key = addr else: - key = req.rhash + addr = req.get_address() + key = req.rhash if addr is None else addr return key def add_payment_request(self, req: Invoice, *, write_to_disk: bool = True):