Browse Source

Index request by ID instead of receiving address.

Replace get_key_for_outgoing_invoice, get_key_for_incoming_request
with Invoice.get_id()

When a new request is created, reuse addresses of expired requests (fixes #7927)

The API is changed for the following commands:
 get_request, get_invoice,
 list_requests, list_invoices,
 delete_request, delete_invoice
patch-4
ThomasV 3 years ago
parent
commit
14e96f4d53
  1. 54
      electrum/commands.py
  2. 6
      electrum/gui/kivy/uix/screens.py
  3. 4
      electrum/gui/qml/qeinvoice.py
  4. 2
      electrum/gui/qt/invoice_list.py
  5. 2
      electrum/gui/qt/request_list.py
  6. 2
      electrum/gui/qt/send_tab.py
  7. 4
      electrum/gui/text.py
  8. 4
      electrum/invoices.py
  9. 15
      electrum/lnworker.py
  10. 98
      electrum/wallet.py
  11. 24
      electrum/wallet_db.py

54
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):
# """<Not implemented>"""
# 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(':')

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

4
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()

2
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:

2
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()

2
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):

4
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()

4
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

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

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

24
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

Loading…
Cancel
Save