diff --git a/electrum/wallet.py b/electrum/wallet.py index e3bcf3502..6754de23d 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -954,7 +954,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): def save_invoice(self, invoice: Invoice) -> None: key = self.get_key_for_outgoing_invoice(invoice) if not invoice.is_lightning(): - if self.is_onchain_invoice_paid(invoice, 0): + if self.is_onchain_invoice_paid(invoice)[0]: _logger.info("saving invoice... but it is already paid!") with self.transaction_lock: for txout in invoice.get_outputs(): @@ -1034,38 +1034,45 @@ class Abstract_Wallet(ABC, Logger, EventListener): 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.""" - if invoice.is_lightning() and not invoice.get_address(): - return False, [] + def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Optional[int], Sequence[str]]: + """Returns whether on-chain invoice/request is satisfied, num confs required txs have, + and list of relevant TXIDs. + """ outputs = invoice.get_outputs() + if not outputs: # e.g. lightning-only + return False, None, [] invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats for txo in outputs: # type: PartialTxOutput invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value - relevant_txs = [] + relevant_txs = set() + is_paid = True + conf_needed = None # type: Optional[int] with self.lock, self.transaction_lock: for invoice_scriptpubkey, invoice_amt in invoice_amounts.items(): scripthash = bitcoin.script_to_scripthash(invoice_scriptpubkey.hex()) prevouts_and_values = self.db.get_prevouts_by_scripthash(scripthash) - total_received = 0 + confs_and_values = [] for prevout, v in prevouts_and_values: + relevant_txs.add(prevout.txid.hex()) tx_height = self.adb.get_tx_height(prevout.txid.hex()) - if tx_height.height > 0 and tx_height.height <= invoice.height: - continue - if tx_height.conf < conf: + if 0 < tx_height.height <= invoice.height: # exclude txs older than invoice continue - total_received += v - relevant_txs.append(prevout.txid.hex()) + confs_and_values.append((tx_height.conf or 0, v)) # check that there is at least one TXO, and that they pay enough. # note: "at least one TXO" check is needed for zero amount invoice (e.g. OP_RETURN) - if len(prevouts_and_values) == 0: - return False, [] - if total_received < invoice_amt: - return False, [] - return True, relevant_txs + vsum = 0 + for conf, v in reversed(sorted(confs_and_values)): + vsum += v + if vsum >= invoice_amt: + conf_needed = min(conf_needed, conf) if conf_needed is not None else conf + break + else: + is_paid = False + return is_paid, conf_needed, list(relevant_txs) - def is_onchain_invoice_paid(self, invoice: Invoice, conf: int) -> bool: - return self._is_onchain_invoice_paid(invoice, conf)[0] + def is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Optional[int]]: + is_paid, conf_needed, relevant_txs = self._is_onchain_invoice_paid(invoice) + return is_paid, conf_needed def _maybe_set_tx_label_based_on_invoices(self, tx: Transaction) -> bool: # note: this is not done in 'get_default_label' as that would require deserializing each tx @@ -2263,27 +2270,6 @@ class Abstract_Wallet(ABC, Logger, EventListener): def delete_address(self, address: str) -> None: raise Exception("this wallet cannot delete addresses") - def get_onchain_request_status(self, r: Invoice) -> Tuple[bool, Optional[int]]: - address = r.get_address() - amount = int(r.get_amount_sat() or 0) - received, sent = self.adb.get_addr_io(address) - l = [] - for txo, x in received.items(): - h, v, is_cb = x - txid, n = txo.split(':') - tx_height = self.adb.get_tx_height(txid) - height = tx_height.height - if height > 0 and height <= r.height: - continue - conf = tx_height.conf - l.append((conf, v)) - vsum = 0 - for conf, v in reversed(sorted(l)): - vsum += v - if vsum >= amount: - return True, conf - return False, None - def get_request_URI(self, req: Invoice) -> str: # todo: should be a method of invoice? addr = req.get_address() @@ -2309,20 +2295,24 @@ class Abstract_Wallet(ABC, Logger, EventListener): return status def get_invoice_status(self, invoice: Invoice): + """Returns status of (outgoing) invoice.""" # 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): + paid, conf = self.is_onchain_invoice_paid(invoice) + if not paid: + status = PR_UNPAID + elif conf == 0: status = PR_UNCONFIRMED else: - status = PR_UNPAID + assert conf >= 1, conf + status = PR_PAID return self.check_expired_status(invoice, status) def get_request_status(self, key): + """Returns status of (incoming) receive request.""" r = self.get_request(key) if r is None: return PR_UNKNOWN @@ -2330,12 +2320,13 @@ class Abstract_Wallet(ABC, Logger, EventListener): 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) + paid, conf = self.is_onchain_invoice_paid(r) if not paid: status = PR_UNPAID elif conf == 0: status = PR_UNCONFIRMED else: + assert conf >= 1, conf status = PR_PAID return self.check_expired_status(r, status) @@ -2373,7 +2364,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): if self.lnworker and status == PR_UNPAID: d['can_receive'] = self.lnworker.can_receive_invoice(x) else: - paid, conf = self.get_onchain_request_status(x) + paid, conf = self.is_onchain_invoice_paid(x) d['amount_sat'] = int(x.get_amount_sat()) d['address'] = x.get_address() d['URI'] = self.get_request_URI(x)