From 56e685feaa56045b2e219c2f20e2a4af2706e645 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 31 Mar 2023 14:25:40 +0200 Subject: [PATCH] invoices: Use the same base method to export invoices and requests. This fixes an inconsistency where the 'expiration' field was relative for invoices, and absolute timestamp for requests. This in turn fixes QML the timer refreshing the request list. In order to prevent any API using that field from being silently broken, the 'expiration' field is renamed as 'expiry'. --- electrum/commands.py | 8 ++--- electrum/gui/qml/qeinvoicelistmodel.py | 6 ++-- electrum/invoices.py | 19 +++++++++++- electrum/wallet.py | 42 +++++--------------------- 4 files changed, 33 insertions(+), 42 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index a475fdc36..46cc6ad11 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -975,7 +975,7 @@ class Commands: return wallet.get_unused_address() @command('w') - async def add_request(self, amount, memo='', expiration=3600, force=False, wallet: Abstract_Wallet = None): + async def add_request(self, amount, memo='', expiry=3600, force=False, wallet: Abstract_Wallet = None): """Create a payment request, using the first unused address of the wallet. The address will be considered as used after this operation. If no payment is received, the address will be considered as unused if the payment request is deleted from the wallet.""" @@ -986,8 +986,8 @@ class Commands: else: return False amount = satoshis(amount) - expiration = int(expiration) if expiration else None - key = wallet.create_request(amount, memo, expiration, addr) + expiry = int(expiry) if expiry else None + key = wallet.create_request(amount, memo, expiry, addr) req = wallet.get_request(key) return wallet.export_request(req) @@ -1429,7 +1429,7 @@ command_options = { 'addtransaction': (None,'Whether transaction is to be used for broadcasting afterwards. Adds transaction to the wallet'), 'domain': ("-D", "List of addresses"), 'memo': ("-m", "Description of the request"), - 'expiration': (None, "Time in seconds"), + 'expiry': (None, "Time in seconds"), 'timeout': (None, "Timeout in seconds"), 'force': (None, "Create new address beyond gap limit, if no more addresses are available."), 'pending': (None, "Show only pending requests."), diff --git a/electrum/gui/qml/qeinvoicelistmodel.py b/electrum/gui/qml/qeinvoicelistmodel.py index 1bd39dd4d..a983708ab 100644 --- a/electrum/gui/qml/qeinvoicelistmodel.py +++ b/electrum/gui/qml/qeinvoicelistmodel.py @@ -15,7 +15,7 @@ class QEAbstractInvoiceListModel(QAbstractListModel): # define listmodel rolemap _ROLE_NAMES=('key', 'is_lightning', 'timestamp', 'date', 'message', 'amount', - 'status', 'status_str', 'address', 'expiration', 'type', 'onchain_fallback', + 'status', 'status_str', 'address', 'expiry', 'type', 'onchain_fallback', 'lightning_invoice') _ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) _ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) @@ -132,8 +132,8 @@ class QEAbstractInvoiceListModel(QAbstractListModel): nearest_interval = LN_EXPIRY_NEVER for invoice in self.invoices: if invoice['status'] != PR_EXPIRED: - if invoice['expiration'] > 0 and invoice['expiration'] != LN_EXPIRY_NEVER: - interval = status_update_timer_interval(invoice['timestamp'] + invoice['expiration']) + if invoice['expiry'] > 0 and invoice['expiry'] != LN_EXPIRY_NEVER: + interval = status_update_timer_interval(invoice['timestamp'] + invoice['expiry']) if interval > 0: nearest_interval = nearest_interval if nearest_interval < interval else interval diff --git a/electrum/invoices.py b/electrum/invoices.py index 7aa2e0488..49819807c 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -6,7 +6,7 @@ import attr from .json_db import StoredObject from .i18n import _ -from .util import age, InvoiceError +from .util import age, InvoiceError, format_satoshis from .lnutil import hex_to_bytes from .lnaddr import lndecode, LnAddr from . import constants @@ -222,6 +222,23 @@ class BaseInvoice(StoredObject): else: # on-chain return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time) + def as_dict(self, status): + d = { + 'is_lightning': self.is_lightning(), + 'amount_BTC': format_satoshis(self.get_amount_sat()), + 'message': self.message, + 'timestamp': self.get_time(), + 'expiry': self.exp, + 'status': status, + 'status_str': self.get_status_str(status), + 'id': self.get_id(), + 'amount_sat': int(self.get_amount_sat()), + } + if self.is_lightning(): + d['amount_msat'] = self.get_amount_msat() + return d + + @attr.s class Invoice(BaseInvoice): lightning_invoice = attr.ib(type=str, kw_only=True) # type: Optional[str] diff --git a/electrum/wallet.py b/electrum/wallet.py index 99cf5ceef..d246c4caa 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2459,34 +2459,20 @@ class Abstract_Wallet(ABC, Logger, EventListener): def export_request(self, x: Request) -> 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() - address = x.get_address() - d = { - 'is_lightning': is_lightning, - 'amount_BTC': format_satoshis(x.get_amount_sat()), - 'message': x.message, - 'timestamp': x.get_time(), - 'expiration': x.get_expiration_date(), - 'status': status, - 'status_str': status_str, - 'request_id': key, - "tx_hashes": [] - } - if is_lightning: + d = x.as_dict(status) + d['request_id'] = d.pop('id') + if x.is_lightning(): d['rhash'] = x.rhash d['lightning_invoice'] = self.get_bolt11_invoice(x) - d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_receive'] = self.lnworker.can_receive_invoice(x) - if address: - d['amount_sat'] = int(x.get_amount_sat()) + if address := x.get_address(): d['address'] = address d['URI'] = self.get_request_URI(x) # if request was paid onchain, add relevant fields # note: addr is reused when getting paid on LN! so we check for that. _, conf, tx_hashes = self._is_onchain_invoice_paid(x) - if not is_lightning or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID: + if not x.is_lightning() or not self.lnworker or self.lnworker.get_invoice_status(x) != PR_PAID: if conf is not None: d['confirmations'] = conf d['tx_hashes'] = tx_hashes @@ -2496,27 +2482,15 @@ class Abstract_Wallet(ABC, Logger, EventListener): 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() - d = { - 'is_lightning': is_lightning, - 'amount_BTC': format_satoshis(x.get_amount_sat()), - 'message': x.message, - 'timestamp': x.time, - 'expiration': x.exp, - 'status': status, - 'status_str': status_str, - 'invoice_id': key, - } - if is_lightning: + d = x.as_dict(status) + d['invoice_id'] = d.pop('id') + if x.is_lightning(): d['lightning_invoice'] = x.lightning_invoice - d['amount_msat'] = x.get_amount_msat() if self.lnworker and status == PR_UNPAID: d['can_pay'] = self.lnworker.can_pay_invoice(x) else: amount_sat = x.get_amount_sat() assert isinstance(amount_sat, (int, str, type(None))) - d['amount_sat'] = amount_sat d['outputs'] = [y.to_legacy_tuple() for y in x.get_outputs()] if x.bip70: d['bip70'] = x.bip70