diff --git a/electrum/commands.py b/electrum/commands.py index d5d9cee4c..b002b678c 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -880,21 +880,27 @@ class Commands: return decrypted.decode('utf-8') @command('w') - async def getrequest(self, key, wallet: Abstract_Wallet = None): - """Return a payment request""" - r = wallet.get_request(key) + async def get_request(self, request_id, wallet: Abstract_Wallet = None): + """Returns a payment request""" + r = wallet.get_request(request_id) if not r: raise Exception("Request not found") return wallet.export_request(r) + @command('w') + async def get_invoice(self, invoice_id, wallet: Abstract_Wallet = None): + """Returns an invoice (request for outgoing payment)""" + r = wallet.get_invoice(invoice_id) + if not r: + raise Exception("Request not found") + return wallet.export_invoice(r) + #@command('w') #async def ackrequest(self, serialized): # """""" # pass - @command('w') - async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None): - """List the payment requests you made.""" + def _filter_invoices(self, _list, wallet, pending, expired, paid): if pending: f = PR_UNPAID elif expired: @@ -903,11 +909,23 @@ class Commands: f = PR_PAID else: f = None - out = wallet.get_sorted_requests() if f is not None: - out = [req for req in out - if f == wallet.get_invoice_status(req)] - return [wallet.export_request(x) for x in out] + _list = [x for x in _list if f == wallet.get_invoice_status(x)] + return _list + + @command('w') + async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None): + """Returns the list of incoming payment requests saved in the wallet.""" + l = wallet.get_sorted_requests() + l = self._filter_invoices(l, wallet, pending, expired, paid) + return [wallet.export_request(x) for x in l] + + @command('w') + async def list_invoices(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None): + """Returns the list of invoices (requests for outgoing payments) saved in the wallet.""" + l = wallet.get_invoices() + l = self._filter_invoices(l, wallet, pending, expired, paid) + return [wallet.export_invoice(x) for x in l] @command('w') async def createnewaddress(self, wallet: Abstract_Wallet = None): @@ -971,9 +989,14 @@ class Commands: return tx.txid() @command('w') - async def delete_request(self, address, wallet: Abstract_Wallet = None): - """Remove a payment request""" - return wallet.delete_request(address) + async def delete_request(self, request_id, wallet: Abstract_Wallet = None): + """Remove an incoming payment request""" + return wallet.delete_request(request_id) + + @command('w') + async def delete_invoice(self, invoice_id, wallet: Abstract_Wallet = None): + """Remove an outgoing payment invoice""" + return wallet.delete_invoice(invoice_id) @command('w') async def clear_requests(self, wallet: Abstract_Wallet = None): @@ -1175,11 +1198,6 @@ class Commands: if self.network.path_finder: self.network.path_finder.liquidity_hints.reset_liquidity_hints() - @command('w') - async def list_invoices(self, wallet: Abstract_Wallet = None): - l = wallet.get_invoices() - return [wallet.export_invoice(x) for x in l] - @command('wnl') async def close_channel(self, channel_point, force=False, wallet: Abstract_Wallet = None): txid, index = channel_point.split(':') diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py index 62a57c1cb..5070f3815 100644 --- a/electrum/gui/kivy/uix/screens.py +++ b/electrum/gui/kivy/uix/screens.py @@ -272,7 +272,7 @@ class SendScreen(CScreen, Logger): status = self.app.wallet.get_invoice_status(item) status_str = item.get_status_str(status) is_lightning = item.is_lightning() - key = self.app.wallet.get_key_for_outgoing_invoice(item) + key = item.get_id() if is_lightning: address = item.rhash if self.app.wallet.lnworker: @@ -486,7 +486,7 @@ class ReceiveScreen(CScreen): self.address = addr def on_address(self, addr): - req = self.app.wallet.get_request(addr) + req = self.app.wallet.get_request_by_addr(addr) self.status = '' if req: self.message = req.get('memo', '') @@ -539,7 +539,7 @@ class ReceiveScreen(CScreen): address = req.get_address() else: address = req.lightning_invoice - key = self.app.wallet.get_key_for_receive_request(req) + key = req.get_id() amount = req.get_amount_sat() description = req.message status = self.app.wallet.get_invoice_status(req) diff --git a/electrum/gui/qml/qeinvoice.py b/electrum/gui/qml/qeinvoice.py index b64677510..697e8d1df 100644 --- a/electrum/gui/qml/qeinvoice.py +++ b/electrum/gui/qml/qeinvoice.py @@ -389,7 +389,7 @@ class QEInvoiceParser(QEInvoice): if not self._effectiveInvoice: return # TODO detect duplicate? - self.key = self._wallet.wallet.get_key_for_outgoing_invoice(self._effectiveInvoice) + self.key = self._effectiveInvoice.get_id() self._wallet.wallet.save_invoice(self._effectiveInvoice) self.invoiceSaved.emit() @@ -486,7 +486,7 @@ class QEUserEnteredPayment(QEInvoice): self.invoiceCreateError.emit('fatal', _('Error creating payment') + ':\n' + str(e)) return - self.key = self._wallet.wallet.get_key_for_outgoing_invoice(invoice) + self.key = invoice.get_id() self._wallet.wallet.save_invoice(invoice) self.invoiceSaved.emit() diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py index bd1a3ded9..9a928d4f0 100644 --- a/electrum/gui/qt/invoice_list.py +++ b/electrum/gui/qt/invoice_list.py @@ -102,7 +102,7 @@ class InvoiceList(MyTreeView): self.std_model.clear() self.update_headers(self.__class__.headers) for idx, item in enumerate(self.wallet.get_unpaid_invoices()): - key = self.wallet.get_key_for_outgoing_invoice(item) + key = item.get_id() if item.is_lightning(): icon_name = 'lightning.png' else: diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py index 5685e95f5..e6470a37f 100644 --- a/electrum/gui/qt/request_list.py +++ b/electrum/gui/qt/request_list.py @@ -126,7 +126,7 @@ class RequestList(MyTreeView): self.std_model.clear() self.update_headers(self.__class__.headers) for req in self.wallet.get_unpaid_requests(): - key = self.wallet.get_key_for_receive_request(req) + key = req.get_id() status = self.wallet.get_invoice_status(req) status_str = req.get_status_str(status) timestamp = req.get_time() diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 395cba612..b601221a5 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -646,7 +646,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): def pay_lightning_invoice(self, invoice: Invoice): amount_sat = invoice.get_amount_sat() - key = self.wallet.get_key_for_outgoing_invoice(invoice) + key = invoice.get_id() if amount_sat is None: raise Exception("missing amount for LN invoice") if not self.wallet.lnworker.can_pay_invoice(invoice): diff --git a/electrum/gui/text.py b/electrum/gui/text.py index e3a29d66a..4316fa20a 100644 --- a/electrum/gui/text.py +++ b/electrum/gui/text.py @@ -267,7 +267,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): fmt = self.format_column_width(x, [-20, '*', 15, 25]) headers = fmt % ("Date", "Description", "Amount", "Status") for req in self.wallet.get_unpaid_invoices(): - key = self.wallet.get_key_for_outgoing_invoice(req) + key = req.get_id() status = self.wallet.get_invoice_status(req) status_str = req.get_status_str(status) timestamp = req.get_time() @@ -287,7 +287,7 @@ class ElectrumGui(BaseElectrumGui, EventListener): fmt = self.format_column_width(x, [-20, '*', 15, 25]) headers = fmt % ("Date", "Description", "Amount", "Status") for req in self.wallet.get_unpaid_requests(): - key = self.wallet.get_key_for_receive_request(req) + key = req.get_id() status = self.wallet.get_invoice_status(req) status_str = req.get_status_str(status) timestamp = req.get_time() diff --git a/electrum/invoices.py b/electrum/invoices.py index afd9773c3..03bea6e9b 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -137,6 +137,10 @@ class Invoice(StoredObject): # 0 means never return self.exp + self.time if self.exp else 0 + def has_expired(self) -> bool: + exp = self.get_expiration_date() + return bool(exp) and exp < time.time() + def get_amount_msat(self) -> Union[int, str, None]: return self.amount_msat diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 20a67e5da..c8e42e94f 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1934,13 +1934,12 @@ class LNWallet(LNWorker): return info.status if info else PR_UNPAID def get_invoice_status(self, invoice: Invoice) -> int: - key = invoice.rhash - log = self.logs[key] - if key in self.inflight_payments: + invoice_id = invoice.rhash + if invoice_id in self.inflight_payments: return PR_INFLIGHT # status may be PR_FAILED - status = self.get_payment_status(bfh(key)) - if status == PR_UNPAID and log: + status = self.get_payment_status(bytes.fromhex(invoice_id)) + if status == PR_UNPAID and invoice_id in self.logs: status = PR_FAILED return status @@ -1957,11 +1956,11 @@ class LNWallet(LNWorker): if self.get_payment_status(payment_hash) == status: return self.set_payment_status(payment_hash, status) - key = payment_hash.hex() - req = self.wallet.get_request(key) + request_id = payment_hash.hex() + req = self.wallet.get_request(request_id) if req is None: return - util.trigger_callback('request_status', self.wallet, key, status) + util.trigger_callback('request_status', self.wallet, request_id, status) def set_payment_status(self, payment_hash: bytes, status: int) -> None: info = self.get_payment_info(payment_hash) diff --git a/electrum/wallet.py b/electrum/wallet.py index 6ddb7bce2..2d85cc2d1 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -987,7 +987,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): return invoice def save_invoice(self, invoice: Invoice, *, write_to_disk: bool = True) -> None: - key = self.get_key_for_outgoing_invoice(invoice) + key = invoice.get_id() if not invoice.is_lightning(): if self.is_onchain_invoice_paid(invoice)[0]: _logger.info("saving invoice... but it is already paid!") @@ -1004,7 +1004,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def clear_requests(self): self._receive_requests.clear() - self._requests_addr_to_rhash.clear() + self._requests_addr_to_key.clear() self.save_db() def get_invoices(self): @@ -1016,8 +1016,8 @@ class Abstract_Wallet(ABC, Logger, EventListener): invoices = self.get_invoices() return [x for x in invoices if self.get_invoice_status(x) != PR_PAID] - def get_invoice(self, key): - return self._invoices.get(key) + def get_invoice(self, invoice_id): + return self._invoices.get(invoice_id) def import_requests(self, path): data = read_json_file(path) @@ -1054,10 +1054,10 @@ class Abstract_Wallet(ABC, Logger, EventListener): return invoices def _init_requests_rhash_index(self): - self._requests_addr_to_rhash = {} - for key, req in self._receive_requests.items(): - if req.is_lightning() and (addr:=req.get_address()): - self._requests_addr_to_rhash[addr] = req.rhash + self._requests_addr_to_key = {} + for req in self._receive_requests.values(): + if req.is_lightning() and not req.has_expired() and (addr:=req.get_address()): + self._requests_addr_to_key[addr] = req.get_id() def _prepare_onchain_invoice_paid_detection(self): self._invoices_from_txid_map = defaultdict(set) # type: Dict[str, Set[str]] @@ -1366,7 +1366,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def get_label_for_address(self, addr: str) -> str: label = self._labels.get(addr) or '' - if not label and (request := self.get_request(addr)): + if not label and (request := self.get_request_by_addr(addr)): label = request.get_message() return label @@ -2278,7 +2278,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def get_unused_addresses(self) -> Sequence[str]: domain = self.get_receiving_addresses() - in_use_by_request = set(req.get_address() for req in self.get_unpaid_requests()) + in_use_by_request = set(req.get_address() for req in self.get_unpaid_requests() if not req.has_expired()) return [addr for addr in domain if not self.adb.is_used(addr) and addr not in in_use_by_request] @@ -2303,7 +2303,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): choice = domain[0] for addr in domain: if not self.adb.is_used(addr): - if self.get_request(addr) is None: + if self.get_request_by_addr(addr) is None: return addr else: choice = addr @@ -2329,7 +2329,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def check_expired_status(self, r: Invoice, status): #if r.is_lightning() and r.exp == 0: # status = PR_EXPIRED # for BOLT-11 invoices, exp==0 means 0 seconds - if status == PR_UNPAID and r.get_expiration_date() and r.get_expiration_date() < time.time(): + if status == PR_UNPAID and r.has_expired(): status = PR_EXPIRED return status @@ -2350,15 +2350,15 @@ class Abstract_Wallet(ABC, Logger, EventListener): status = PR_PAID return self.check_expired_status(invoice, status) - def get_request(self, key: str) -> Optional[Invoice]: - if req := self._receive_requests.get(key): - return req - # try 'key' as a fallback address for lightning invoices - if (rhash := self._requests_addr_to_rhash.get(key)) and (req := self._receive_requests.get(rhash)): - return req + def get_request_by_addr(self, addr: str) -> Optional[Invoice]: + key = self._requests_addr_to_key.get(addr) + return self._receive_requests.get(key) - def get_formatted_request(self, key): - x = self.get_request(key) + def get_request(self, request_id: str) -> Optional[Invoice]: + return self._receive_requests.get(request_id) + + def get_formatted_request(self, request_id): + x = self.get_request(request_id) if x: return self.export_request(x) @@ -2376,6 +2376,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): 'expiration': x.get_expiration_date(), 'status': status, 'status_str': status_str, + 'request_id': key, } if is_lightning: d['rhash'] = x.rhash @@ -2404,6 +2405,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): return d def export_invoice(self, x: Invoice) -> Dict[str, Any]: + key = x.get_id() status = self.get_invoice_status(x) status_str = x.get_status_str(status) is_lightning = x.is_lightning() @@ -2415,6 +2417,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): 'expiration': x.exp, 'status': status, 'status_str': status_str, + 'invoice_id': key, } if is_lightning: d['lightning_invoice'] = x.lightning_invoice @@ -2441,9 +2444,8 @@ class Abstract_Wallet(ABC, Logger, EventListener): with self.transaction_lock: for txo in tx.outputs(): addr = txo.address - if self.get_request(addr): - req = self.get_request(addr) - status = self.get_invoice_status(req) + if request:=self.get_request_by_addr(addr): + status = self.get_invoice_status(request) util.trigger_callback('request_status', self, addr, status) for invoice_key in self._invoices_from_scriptpubkey_map.get(txo.scriptpubkey, set()): relevant_invoice_keys.add(invoice_key) @@ -2482,52 +2484,31 @@ class Abstract_Wallet(ABC, Logger, EventListener): key = self.add_payment_request(req) return key - @classmethod - def get_key_for_outgoing_invoice(cls, invoice: Invoice) -> str: - """Return the key to use for this invoice in self.invoices.""" - return invoice.get_id() - - 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() or "" - if sanity_checks: - if not bitcoin.is_address(addr): - raise Exception(_('Invalid Bitcoin address.')) - if not self.is_mine(addr): - raise Exception(_('Address not in wallet.')) - key = addr - else: - key = req.rhash - return key - def add_payment_request(self, req: Invoice, *, write_to_disk: bool = True): - key = self.get_key_for_receive_request(req, sanity_checks=True) - self._receive_requests[key] = req - if req.is_lightning() and (addr:=req.get_address()): - self._requests_addr_to_rhash[addr] = req.rhash + request_id = req.get_id() + self._receive_requests[request_id] = req + if addr:=req.get_address(): + self._requests_addr_to_key[addr] = request_id if write_to_disk: self.save_db() - return key + return request_id - def delete_request(self, key, *, write_to_disk: bool = True): + def delete_request(self, request_id, *, write_to_disk: bool = True): """ lightning or on-chain """ - req = self.get_request(key) + req = self.get_request(request_id) if req is None: return - key = self.get_key_for_receive_request(req) - self._receive_requests.pop(key, None) - if req.is_lightning() and (addr:=req.get_address()): - self._requests_addr_to_rhash.pop(addr) + self._receive_requests.pop(request_id, None) + if addr:=req.get_address(): + self._requests_addr_to_key.pop(addr) if req.is_lightning() and self.lnworker: self.lnworker.delete_payment_info(req.rhash) if write_to_disk: self.save_db() - def delete_invoice(self, key, *, write_to_disk: bool = True): + def delete_invoice(self, invoice_id, *, write_to_disk: bool = True): """ lightning or on-chain """ - inv = self._invoices.pop(key, None) + inv = self._invoices.pop(invoice_id, None) if inv is None: return if inv.is_lightning() and self.lnworker: @@ -2810,7 +2791,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): 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) + key = req.get_id() addr = req.get_address() or '' amount_sat = req.get_amount_sat() or 0 address_help = '' @@ -3013,7 +2994,8 @@ class Imported_Wallet(Simple_Wallet): for tx_hash in transactions_to_remove: self.adb._remove_transaction(tx_hash) self.set_label(address, None) - self.delete_request(address) + if req:= self.get_request_by_addr(address): + self.delete_request(req.get_id()) self.set_frozen_state_of_addresses([address], False) pubkey = self.get_public_key(address) self.db.remove_imported_address(address) diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index f861f9dee..02b4a1674 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: OLD_SEED_VERSION = 4 # electrum versions < 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0 -FINAL_SEED_VERSION = 49 # electrum >= 2.7 will set this to prevent +FINAL_SEED_VERSION = 50 # electrum >= 2.7 will set this to prevent # old versions from overwriting new format @@ -198,6 +198,7 @@ class WalletDB(JsonDB): self._convert_version_47() self._convert_version_48() self._convert_version_49() + self._convert_version_50() self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self._after_upgrade_tasks() @@ -904,17 +905,13 @@ class WalletDB(JsonDB): } self.data['seed_version'] = 45 - def _convert_version_46(self): - from .crypto import sha256d - if not self._is_upgrade_method_needed(45, 45): - return + def _convert_invoices_keys(self, invoices): # recalc keys of outgoing on-chain invoices + from .crypto import sha256d def get_id_from_onchain_outputs(raw_outputs, timestamp): outputs = [PartialTxOutput.from_legacy_tuple(*output) for output in raw_outputs] outputs_str = "\n".join(f"{txout.scriptpubkey.hex()}, {txout.value}" for txout in outputs) return sha256d(outputs_str + "%d" % timestamp).hex()[0:10] - - invoices = self.data.get('invoices', {}) for key, item in list(invoices.items()): is_lightning = item['lightning_invoice'] is not None if is_lightning: @@ -926,6 +923,12 @@ class WalletDB(JsonDB): if newkey != key: invoices[newkey] = item del invoices[key] + + def _convert_version_46(self): + if not self._is_upgrade_method_needed(45, 45): + return + invoices = self.data.get('invoices', {}) + self._convert_invoices_keys(invoices) self.data['seed_version'] = 46 def _convert_version_47(self): @@ -970,6 +973,13 @@ class WalletDB(JsonDB): ) self.data['seed_version'] = 49 + def _convert_version_50(self): + if not self._is_upgrade_method_needed(49, 49): + return + requests = self.data.get('payment_requests', {}) + self._convert_invoices_keys(requests) + self.data['seed_version'] = 50 + def _convert_imported(self): if not self._is_upgrade_method_needed(0, 13): return