|
|
@ -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) |
|
|
|