Browse Source

wallet_db upgrade:

- unify lightning and onchain invoices, with optional fields for bip70 and lightning
 - add receive_address fields to submarine swaps
patch-4
ThomasV 3 years ago
parent
commit
e392197ab9
  1. 6
      electrum/commands.py
  2. 6
      electrum/gui/qt/invoice_list.py
  3. 5
      electrum/gui/qt/lightning_tx_dialog.py
  4. 33
      electrum/gui/qt/main_window.py
  5. 14
      electrum/gui/qt/request_list.py
  6. 172
      electrum/invoices.py
  7. 24
      electrum/lnworker.py
  8. 6
      electrum/paymentrequest.py
  9. 9
      electrum/submarine_swaps.py
  10. 86
      electrum/wallet.py
  11. 50
      electrum/wallet_db.py

6
electrum/commands.py

@ -60,7 +60,7 @@ from .lnpeer import channel_id_from_funding_tx
from .plugin import run_hook from .plugin import run_hook
from .version import ELECTRUM_VERSION from .version import ELECTRUM_VERSION
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
from .invoices import LNInvoice from .invoices import Invoice
from . import submarine_swaps from . import submarine_swaps
@ -1066,7 +1066,7 @@ class Commands:
@command('') @command('')
async def decode_invoice(self, invoice: str): async def decode_invoice(self, invoice: str):
invoice = LNInvoice.from_bech32(invoice) invoice = Invoice.from_bech32(invoice)
return invoice.to_debug_json() return invoice.to_debug_json()
@command('wnl') @command('wnl')
@ -1074,7 +1074,7 @@ class Commands:
lnworker = wallet.lnworker lnworker = wallet.lnworker
lnaddr = lnworker._check_invoice(invoice) lnaddr = lnworker._check_invoice(invoice)
payment_hash = lnaddr.paymenthash payment_hash = lnaddr.paymenthash
wallet.save_invoice(LNInvoice.from_bech32(invoice)) wallet.save_invoice(Invoice.from_bech32(invoice))
success, log = await lnworker.pay_invoice(invoice, attempts=attempts) success, log = await lnworker.pay_invoice(invoice, attempts=attempts)
return { return {
'payment_hash': payment_hash.hex(), 'payment_hash': payment_hash.hex(),

6
electrum/gui/qt/invoice_list.py

@ -33,7 +33,7 @@ from PyQt5.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QH
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time from electrum.util import format_time
from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED
from electrum.lnutil import HtlcLog from electrum.lnutil import HtlcLog
from .util import MyTreeView, read_QIcon, MySortModel, pr_icons from .util import MyTreeView, read_QIcon, MySortModel, pr_icons
@ -116,7 +116,7 @@ class InvoiceList(MyTreeView):
items[self.Columns.DATE].setIcon(read_QIcon(icon_name)) items[self.Columns.DATE].setIcon(read_QIcon(icon_name))
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID) items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID)
items[self.Columns.DATE].setData(item.type, role=ROLE_REQUEST_TYPE) #items[self.Columns.DATE].setData(item.type, role=ROLE_REQUEST_TYPE)
items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER) items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER)
self.std_model.insertRow(idx, items) self.std_model.insertRow(idx, items)
self.filter() self.filter()
@ -136,7 +136,7 @@ class InvoiceList(MyTreeView):
if len(items)>1: if len(items)>1:
keys = [item.data(ROLE_REQUEST_ID) for item in items] keys = [item.data(ROLE_REQUEST_ID) for item in items]
invoices = [wallet.invoices.get(key) for key in keys] invoices = [wallet.invoices.get(key) for key in keys]
can_batch_pay = all([i.type == PR_TYPE_ONCHAIN and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices]) can_batch_pay = all([not i.is_lightning() and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices])
menu = QMenu(self) menu = QMenu(self)
if can_batch_pay: if can_batch_pay:
menu.addAction(_("Batch pay invoices") + "...", lambda: self.parent.pay_multiple_invoices(invoices)) menu.addAction(_("Batch pay invoices") + "...", lambda: self.parent.pay_multiple_invoices(invoices))

5
electrum/gui/qt/lightning_tx_dialog.py

@ -31,7 +31,6 @@ from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import QVBoxLayout, QLabel, QGridLayout from PyQt5.QtWidgets import QVBoxLayout, QLabel, QGridLayout
from electrum.i18n import _ from electrum.i18n import _
from electrum.invoices import LNInvoice
from .util import WindowModalDialog, ButtonsLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT from .util import WindowModalDialog, ButtonsLineEdit, ColorScheme, Buttons, CloseButton, MONOSPACE_FONT
from .qrtextedit import ShowQRTextEdit from .qrtextedit import ShowQRTextEdit
@ -55,8 +54,8 @@ class LightningTxDialog(WindowModalDialog):
invoice = (self.parent.wallet.get_invoice(self.payment_hash) invoice = (self.parent.wallet.get_invoice(self.payment_hash)
or self.parent.wallet.get_request(self.payment_hash)) or self.parent.wallet.get_request(self.payment_hash))
if invoice: if invoice:
assert isinstance(invoice, LNInvoice), f"{self.invoice!r}" assert invoice.is_lightning(), f"{self.invoice!r}"
self.invoice = invoice.invoice self.invoice = invoice.lightning_invoice
else: else:
self.invoice = '' self.invoice = ''

33
electrum/gui/qt/main_window.py

@ -66,8 +66,8 @@ from electrum.util import (format_time,
NoDynamicFeeEstimates, NoDynamicFeeEstimates,
AddTransactionException, BITCOIN_BIP21_URI_SCHEME, AddTransactionException, BITCOIN_BIP21_URI_SCHEME,
InvoiceError, parse_max_spend) InvoiceError, parse_max_spend)
from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice from electrum.invoices import PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice
from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, Invoice
from electrum.transaction import (Transaction, PartialTxInput, from electrum.transaction import (Transaction, PartialTxInput,
PartialTransaction, PartialTxOutput) PartialTransaction, PartialTxOutput)
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
@ -1290,7 +1290,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.receive_message_e.setText('') self.receive_message_e.setText('')
# copy to clipboard # copy to clipboard
r = self.wallet.get_request(key) r = self.wallet.get_request(key)
content = r.invoice if r.is_lightning() else r.get_address() content = r.lightning_invoice if r.is_lightning() else r.get_address()
title = _('Invoice') if is_lightning else _('Address') title = _('Invoice') if is_lightning else _('Address')
self.do_copy(content, title=title) self.do_copy(content, title=title)
@ -1311,7 +1311,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")): if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
return return
addr = self.wallet.create_new_address(False) addr = self.wallet.create_new_address(False)
req = self.wallet.make_payment_request(addr, amount, message, expiration) timestamp = int(time.time())
req = self.wallet.make_payment_request(amount, message, timestamp, expiration, address=addr)
try: try:
self.wallet.add_payment_request(req) self.wallet.add_payment_request(req)
except Exception as e: except Exception as e:
@ -1649,8 +1650,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if not self.wallet.has_lightning(): if not self.wallet.has_lightning():
self.show_error(_('Lightning is disabled')) self.show_error(_('Lightning is disabled'))
return return
invoice = LNInvoice.from_bech32(invoice_str) invoice = Invoice.from_bech32(invoice_str)
if invoice.get_amount_msat() is None: if invoice.amount_msat is None:
amount_sat = self.amount_e.get_amount() amount_sat = self.amount_e.get_amount()
if amount_sat: if amount_sat:
invoice.amount_msat = int(amount_sat * 1000) invoice.amount_msat = int(amount_sat * 1000)
@ -1698,14 +1699,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.pay_onchain_dialog(self.get_coins(), outputs) self.pay_onchain_dialog(self.get_coins(), outputs)
def do_pay_invoice(self, invoice: 'Invoice'): def do_pay_invoice(self, invoice: 'Invoice'):
if invoice.type == PR_TYPE_LN: if invoice.is_lightning():
assert isinstance(invoice, LNInvoice) self.pay_lightning_invoice(invoice.lightning_invoice, amount_msat=invoice.get_amount_msat())
self.pay_lightning_invoice(invoice.invoice, amount_msat=invoice.get_amount_msat())
elif invoice.type == PR_TYPE_ONCHAIN:
assert isinstance(invoice, OnchainInvoice)
self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
else: else:
raise Exception('unknown invoice type') self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]: def get_coins(self, *, nonlocal_only=False) -> Sequence[PartialTxInput]:
coins = self.get_manually_selected_coins() coins = self.get_manually_selected_coins()
@ -2177,8 +2174,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.contact_list.update() self.contact_list.update()
self.update_completions() self.update_completions()
def show_onchain_invoice(self, invoice: OnchainInvoice): def show_onchain_invoice(self, invoice: Invoice):
amount_str = self.format_amount(invoice.amount_sat) + ' ' + self.base_unit() amount_str = self.format_amount(invoice.get_amount_sat()) + ' ' + self.base_unit()
d = WindowModalDialog(self, _("Onchain Invoice")) d = WindowModalDialog(self, _("Onchain Invoice"))
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
grid = QGridLayout() grid = QGridLayout()
@ -2226,8 +2223,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
vbox.addLayout(buttons) vbox.addLayout(buttons)
d.exec_() d.exec_()
def show_lightning_invoice(self, invoice: LNInvoice): def show_lightning_invoice(self, invoice: Invoice):
lnaddr = lndecode(invoice.invoice) lnaddr = lndecode(invoice.lightning_invoice)
d = WindowModalDialog(self, _("Lightning Invoice")) d = WindowModalDialog(self, _("Lightning Invoice"))
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
grid = QGridLayout() grid = QGridLayout()
@ -2250,7 +2247,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
vbox.addLayout(grid) vbox.addLayout(grid)
invoice_e = ShowQRTextEdit(config=self.config) invoice_e = ShowQRTextEdit(config=self.config)
invoice_e.addCopyButton(self.app) invoice_e.addCopyButton(self.app)
invoice_e.setText(invoice.invoice) invoice_e.setText(invoice.lightning_invoice)
vbox.addWidget(invoice_e) vbox.addWidget(invoice_e)
vbox.addLayout(Buttons(CloseButton(d),)) vbox.addLayout(Buttons(CloseButton(d),))
d.exec_() d.exec_()

14
electrum/gui/qt/request_list.py

@ -32,7 +32,6 @@ from PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time from electrum.util import format_time
from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, LNInvoice, OnchainInvoice
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.invoices import Invoice from electrum.invoices import Invoice
@ -100,8 +99,8 @@ class RequestList(MyTreeView):
self.update() self.update()
return return
if req.is_lightning(): if req.is_lightning():
self.parent.receive_payreq_e.setText(req.invoice) # TODO maybe prepend "lightning:" ?? self.parent.receive_payreq_e.setText(req.lightning_invoice) # TODO maybe prepend "lightning:" ??
self.parent.receive_address_e.setText(req.invoice) self.parent.receive_address_e.setText(req.lightning_invoice)
else: else:
self.parent.receive_payreq_e.setText(self.parent.wallet.get_request_URI(req)) self.parent.receive_payreq_e.setText(self.parent.wallet.get_request_URI(req))
self.parent.receive_address_e.setText(req.get_address()) self.parent.receive_address_e.setText(req.get_address())
@ -133,10 +132,9 @@ class RequestList(MyTreeView):
key = self.wallet.get_key_for_receive_request(req) key = self.wallet.get_key_for_receive_request(req)
status = self.parent.wallet.get_request_status(key) status = self.parent.wallet.get_request_status(key)
status_str = req.get_status_str(status) status_str = req.get_status_str(status)
request_type = req.type timestamp = req.get_time()
timestamp = req.time
amount = req.get_amount_sat() amount = req.get_amount_sat()
message = req.message message = req.get_message()
date = format_time(timestamp) date = format_time(timestamp)
amount_str = self.parent.format_amount(amount) if amount else "" amount_str = self.parent.format_amount(amount) if amount else ""
labels = [date, message, amount_str, status_str] labels = [date, message, amount_str, status_str]
@ -148,7 +146,7 @@ class RequestList(MyTreeView):
tooltip = 'onchain request' tooltip = 'onchain request'
items = [QStandardItem(e) for e in labels] items = [QStandardItem(e) for e in labels]
self.set_editability(items) self.set_editability(items)
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) #items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
items[self.Columns.DATE].setData(key, ROLE_KEY) items[self.Columns.DATE].setData(key, ROLE_KEY)
items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER) items[self.Columns.DATE].setData(timestamp, ROLE_SORT_ORDER)
items[self.Columns.DATE].setIcon(icon) items[self.Columns.DATE].setIcon(icon)
@ -190,7 +188,7 @@ class RequestList(MyTreeView):
menu = QMenu(self) menu = QMenu(self)
self.add_copy_menu(menu, idx) self.add_copy_menu(menu, idx)
if req.is_lightning(): if req.is_lightning():
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req.invoice, title='Lightning Request')) menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req.lightning_invoice, title='Lightning Request'))
else: else:
URI = self.wallet.get_request_URI(req) URI = self.wallet.get_request_URI(req)
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(URI, title='Bitcoin URI')) menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(URI, title='Bitcoin URI'))

172
electrum/invoices.py

@ -17,10 +17,6 @@ if TYPE_CHECKING:
# convention: 'invoices' = outgoing , 'request' = incoming # convention: 'invoices' = outgoing , 'request' = incoming
# types of payment requests
PR_TYPE_ONCHAIN = 0
PR_TYPE_LN = 2
# status of payment requests # status of payment requests
PR_UNPAID = 0 # if onchain: invoice amt not reached by txs in mempool+chain. if LN: invoice not paid. PR_UNPAID = 0 # if onchain: invoice amt not reached by txs in mempool+chain. if LN: invoice not paid.
PR_EXPIRED = 1 # invoice is unpaid and expiry time reached PR_EXPIRED = 1 # invoice is unpaid and expiry time reached
@ -65,6 +61,8 @@ assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
def _decode_outputs(outputs) -> List[PartialTxOutput]: def _decode_outputs(outputs) -> List[PartialTxOutput]:
if outputs is None:
return None
ret = [] ret = []
for output in outputs: for output in outputs:
if not isinstance(output, PartialTxOutput): if not isinstance(output, PartialTxOutput):
@ -79,92 +77,73 @@ def _decode_outputs(outputs) -> List[PartialTxOutput]:
# Hence set some high expiration here # Hence set some high expiration here
LN_EXPIRY_NEVER = 100 * 365 * 24 * 60 * 60 # 100 years LN_EXPIRY_NEVER = 100 * 365 * 24 * 60 * 60 # 100 years
@attr.s @attr.s
class Invoice(StoredObject): class Invoice(StoredObject):
type = attr.ib(type=int, kw_only=True)
message: str # mandatory fields
exp: int amount_msat = attr.ib(kw_only=True) # type: Optional[Union[int, str]] # can be '!' or None
time: int message = attr.ib(type=str, kw_only=True)
time = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # timestamp of the invoice
exp = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # expiration delay (relative). 0 means never
# optional fields.
# an request (incoming) can be satisfied onchain, using lightning or using a swap
# an invoice (outgoing) is constructed from a source: bip21, bip70, lnaddr
# onchain only
outputs = attr.ib(kw_only=True, converter=_decode_outputs) # type: List[PartialTxOutput]
height = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int)) # only for receiving
bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str]
#bip70_requestor = attr.ib(type=str, kw_only=True) # type: Optional[str]
# lightning only
lightning_invoice = attr.ib(type=str, kw_only=True)
__lnaddr = None
def is_lightning(self): def is_lightning(self):
return self.type == PR_TYPE_LN return self.lightning_invoice is not None
def get_status_str(self, status): def get_status_str(self, status):
status_str = pr_tooltips[status] status_str = pr_tooltips[status]
if status == PR_UNPAID: if status == PR_UNPAID:
if self.exp > 0 and self.exp != LN_EXPIRY_NEVER: if self.exp > 0 and self.exp != LN_EXPIRY_NEVER:
expiration = self.exp + self.time expiration = self.get_expiration_date()
status_str = _('Expires') + ' ' + age(expiration, include_seconds=True) status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
return status_str return status_str
def get_amount_sat(self) -> Union[int, Decimal, str, None]:
"""Returns a decimal satoshi amount, or '!' or None."""
raise NotImplementedError()
@classmethod
def from_json(cls, x: dict) -> 'Invoice':
# note: these raise if x has extra fields
if x.get('type') == PR_TYPE_LN:
return LNInvoice(**x)
else:
return OnchainInvoice(**x)
@attr.s
class OnchainInvoice(Invoice):
message = attr.ib(type=str, kw_only=True)
amount_sat = attr.ib(kw_only=True) # type: Union[int, str] # in satoshis. can be '!'
exp = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int))
time = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int))
id = attr.ib(type=str, kw_only=True)
outputs = attr.ib(kw_only=True, converter=_decode_outputs) # type: List[PartialTxOutput]
bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str]
requestor = attr.ib(type=str, kw_only=True) # type: Optional[str]
height = attr.ib(type=int, kw_only=True, validator=attr.validators.instance_of(int))
def get_address(self) -> str: def get_address(self) -> str:
"""returns the first address, to be displayed in GUI""" """returns the first address, to be displayed in GUI"""
return self.outputs[0].address return self.outputs[0].address
def get_amount_sat(self) -> Union[int, str]: def get_expiration_date(self):
return self.amount_sat or 0 # 0 means never
return self.exp + self.time if self.exp else 0
@amount_sat.validator def get_amount_msat(self):
def _validate_amount(self, attribute, value): return self.amount_msat
if isinstance(value, int):
if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN):
raise InvoiceError(f"amount is out-of-bounds: {value!r} sat")
elif isinstance(value, str):
if value != '!':
raise InvoiceError(f"unexpected amount: {value!r}")
else:
raise InvoiceError(f"unexpected amount: {value!r}")
@classmethod def get_time(self):
def from_bip70_payreq(cls, pr: 'PaymentRequest', height:int) -> 'OnchainInvoice': return self.time
return OnchainInvoice(
type=PR_TYPE_ONCHAIN,
amount_sat=pr.get_amount(),
outputs=pr.get_outputs(),
message=pr.get_memo(),
id=pr.get_id(),
time=pr.get_time(),
exp=pr.get_expiration_date() - pr.get_time(),
bip70=pr.raw.hex(),
requestor=pr.get_requestor(),
height=height,
)
@attr.s def get_message(self):
class LNInvoice(Invoice): return self.message
invoice = attr.ib(type=str)
amount_msat = attr.ib(kw_only=True) # type: Optional[int] # needed for zero amt invoices
__lnaddr = None def get_amount_sat(self) -> Union[int, str]:
"""
Returns an integer satoshi amount, or '!' or None.
Callers who need msat precision should call get_amount_msat()
"""
amount_msat = self.amount_msat
if amount_msat is None:
return None
return int(amount_msat / 1000)
@invoice.validator @lightning_invoice.validator
def _validate_invoice_str(self, attribute, value): def _validate_invoice_str(self, attribute, value):
if value is not None:
lndecode(value) # this checks the str can be decoded lndecode(value) # this checks the str can be decoded
@amount_msat.validator @amount_msat.validator
@ -174,45 +153,25 @@ class LNInvoice(Invoice):
if isinstance(value, int): if isinstance(value, int):
if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN * 1000): if not (0 <= value <= TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN * 1000):
raise InvoiceError(f"amount is out-of-bounds: {value!r} msat") raise InvoiceError(f"amount is out-of-bounds: {value!r} msat")
elif isinstance(value, str):
if value != '!':
raise InvoiceError(f"unexpected amount: {value!r}")
else: else:
raise InvoiceError(f"unexpected amount: {value!r}") raise InvoiceError(f"unexpected amount: {value!r}")
@property @property
def _lnaddr(self) -> LnAddr: def _lnaddr(self) -> LnAddr:
if self.__lnaddr is None: if self.__lnaddr is None:
self.__lnaddr = lndecode(self.invoice) self.__lnaddr = lndecode(self.lightning_invoice)
return self.__lnaddr return self.__lnaddr
@property @property
def rhash(self) -> str: def rhash(self) -> str:
return self._lnaddr.paymenthash.hex() return self._lnaddr.paymenthash.hex()
def get_amount_msat(self) -> Optional[int]:
amount_btc = self._lnaddr.amount
amount = int(amount_btc * COIN * 1000) if amount_btc else None
return amount or self.amount_msat
def get_amount_sat(self) -> Union[Decimal, None]:
amount_msat = self.get_amount_msat()
if amount_msat is None:
return None
return Decimal(amount_msat) / 1000
@property
def exp(self) -> int:
return self._lnaddr.get_expiry()
@property
def time(self) -> int:
return self._lnaddr.date
@property
def message(self) -> str:
return self._lnaddr.get_description()
@classmethod @classmethod
def from_bech32(cls, invoice: str) -> 'LNInvoice': def from_bech32(cls, invoice: str) -> 'Invoice':
"""Constructs LNInvoice object from BOLT-11 string. """Constructs Invoice object from BOLT-11 string.
Might raise InvoiceError. Might raise InvoiceError.
""" """
try: try:
@ -220,10 +179,31 @@ class LNInvoice(Invoice):
except Exception as e: except Exception as e:
raise InvoiceError(e) from e raise InvoiceError(e) from e
amount_msat = lnaddr.get_amount_msat() amount_msat = lnaddr.get_amount_msat()
return LNInvoice( timestamp = lnaddr.date
type=PR_TYPE_LN, exp_delay = lnaddr.get_expiry()
invoice=invoice, message = lnaddr.get_description()
return Invoice(
message=message,
amount_msat=amount_msat, amount_msat=amount_msat,
time=timestamp,
exp=exp_delay,
outputs=None,
bip70=None,
height=0,
lightning_invoice=invoice,
)
@classmethod
def from_bip70_payreq(cls, pr: 'PaymentRequest', height:int) -> 'Invoice':
return Invoice(
amount_msat=pr.get_amount()*1000,
message=pr.get_memo(),
time=pr.get_time(),
exp=pr.get_expiration_date() - pr.get_time(),
outputs=pr.get_outputs(),
bip70=pr.raw.hex(),
height=height,
lightning_invoice=None,
) )
def to_debug_json(self) -> Dict[str, Any]: def to_debug_json(self) -> Dict[str, Any]:

24
electrum/lnworker.py

@ -27,7 +27,7 @@ from aiorpcx import run_in_thread, NetAddress, ignore_after
from . import constants, util from . import constants, util
from . import keystore from . import keystore
from .util import profiler, chunks, OldTaskGroup from .util import profiler, chunks, OldTaskGroup
from .invoices import PR_TYPE_LN, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LNInvoice, LN_EXPIRY_NEVER from .invoices import Invoice, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LN_EXPIRY_NEVER
from .util import NetworkRetryManager, JsonRPCClient from .util import NetworkRetryManager, JsonRPCClient
from .lnutil import LN_MAX_FUNDING_SAT from .lnutil import LN_MAX_FUNDING_SAT
from .keystore import BIP32_KeyStore from .keystore import BIP32_KeyStore
@ -1784,16 +1784,24 @@ class LNWallet(LNWorker):
return lnaddr, invoice return lnaddr, invoice
def add_request(self, amount_sat: Optional[int], message, expiry: int) -> str: def add_request(self, amount_sat: Optional[int], message, expiry: int) -> str:
# passed expiry is relative, it is absolute in the lightning invoice
amount_msat = amount_sat * 1000 if amount_sat is not None else None amount_msat = amount_sat * 1000 if amount_sat is not None else None
timestamp = int(time.time())
lnaddr, invoice = self.create_invoice( lnaddr, invoice = self.create_invoice(
amount_msat=amount_msat, amount_msat=amount_msat,
message=message, message=message,
expiry=expiry, expiry=expiry,
write_to_disk=False, write_to_disk=False,
) )
key = bh2u(lnaddr.paymenthash) req = self.wallet.make_payment_request(
req = LNInvoice.from_bech32(invoice) amount_sat,
self.wallet.add_payment_request(req, write_to_disk=False) message,
timestamp,
expiry,
address=None,
lightning_invoice=invoice
)
key = self.wallet.add_payment_request(req, write_to_disk=False)
self.wallet.set_label(key, message) self.wallet.set_label(key, message)
self.wallet.save_db() self.wallet.save_db()
return key return key
@ -1856,7 +1864,7 @@ class LNWallet(LNWorker):
info = self.get_payment_info(payment_hash) info = self.get_payment_info(payment_hash)
return info.status if info else PR_UNPAID return info.status if info else PR_UNPAID
def get_invoice_status(self, invoice: LNInvoice) -> int: def get_invoice_status(self, invoice: Invoice) -> int:
key = invoice.rhash key = invoice.rhash
log = self.logs[key] log = self.logs[key]
if key in self.inflight_payments: if key in self.inflight_payments:
@ -2073,10 +2081,12 @@ class LNWallet(LNWorker):
can_receive = max([c.available_to_spend(REMOTE) for c in channels]) if channels else 0 can_receive = max([c.available_to_spend(REMOTE) for c in channels]) if channels else 0
return Decimal(can_receive) / 1000 return Decimal(can_receive) / 1000
def can_pay_invoice(self, invoice: LNInvoice) -> bool: def can_pay_invoice(self, invoice: Invoice) -> bool:
assert invoice.is_lightning()
return invoice.get_amount_sat() <= self.num_sats_can_send() return invoice.get_amount_sat() <= self.num_sats_can_send()
def can_receive_invoice(self, invoice: LNInvoice) -> bool: def can_receive_invoice(self, invoice: Invoice) -> bool:
assert invoice.is_lightning()
return invoice.get_amount_sat() <= self.num_sats_can_receive() return invoice.get_amount_sat() <= self.num_sats_can_receive()
async def close_channel(self, chan_id): async def close_channel(self, chan_id):

6
electrum/paymentrequest.py

@ -41,7 +41,7 @@ except ImportError:
from . import bitcoin, constants, ecc, util, transaction, x509, rsakey from . import bitcoin, constants, ecc, util, transaction, x509, rsakey
from .util import bh2u, bfh, make_aiohttp_session from .util import bh2u, bfh, make_aiohttp_session
from .invoices import OnchainInvoice from .invoices import Invoice
from .crypto import sha256 from .crypto import sha256
from .bitcoin import address_to_script from .bitcoin import address_to_script
from .transaction import PartialTxOutput from .transaction import PartialTxOutput
@ -324,7 +324,7 @@ class PaymentRequest:
return False, error return False, error
def make_unsigned_request(req: 'OnchainInvoice'): def make_unsigned_request(req: 'Invoice'):
addr = req.get_address() addr = req.get_address()
time = req.time time = req.time
exp = req.exp exp = req.exp
@ -465,7 +465,7 @@ def serialize_request(req): # FIXME this is broken
return pr return pr
def make_request(config: 'SimpleConfig', req: 'OnchainInvoice'): def make_request(config: 'SimpleConfig', req: 'Invoice'):
pr = make_unsigned_request(req) pr = make_unsigned_request(req)
key_path = config.get('ssl_keyfile') key_path = config.get('ssl_keyfile')
cert_path = config.get('ssl_certfile') cert_path = config.get('ssl_certfile')

9
electrum/submarine_swaps.py

@ -90,6 +90,7 @@ class SwapData(StoredObject):
prepay_hash = attr.ib(type=Optional[bytes], converter=hex_to_bytes) prepay_hash = attr.ib(type=Optional[bytes], converter=hex_to_bytes)
privkey = attr.ib(type=bytes, converter=hex_to_bytes) privkey = attr.ib(type=bytes, converter=hex_to_bytes)
lockup_address = attr.ib(type=str) lockup_address = attr.ib(type=str)
receive_address = attr.ib(type=str)
funding_txid = attr.ib(type=Optional[str]) funding_txid = attr.ib(type=Optional[str])
spending_txid = attr.ib(type=Optional[str]) spending_txid = attr.ib(type=Optional[str])
is_redeemed = attr.ib(type=bool) is_redeemed = attr.ib(type=bool)
@ -213,7 +214,6 @@ class SwapManager(Logger):
if amount_sat < dust_threshold(): if amount_sat < dust_threshold():
self.logger.info('utxo value below dust threshold') self.logger.info('utxo value below dust threshold')
continue continue
address = self.wallet.get_receiving_address()
if swap.is_reverse: # successful reverse swap if swap.is_reverse: # successful reverse swap
preimage = swap.preimage preimage = swap.preimage
locktime = 0 locktime = 0
@ -224,7 +224,8 @@ class SwapManager(Logger):
txin=txin, txin=txin,
witness_script=swap.redeem_script, witness_script=swap.redeem_script,
preimage=preimage, preimage=preimage,
address=address, privkey=swap.privkey,
address=swap.receive_address,
amount_sat=amount_sat, amount_sat=amount_sat,
locktime=locktime, locktime=locktime,
) )
@ -330,6 +331,7 @@ class SwapManager(Logger):
tx.set_rbf(True) # note: rbf must not decrease payment tx.set_rbf(True) # note: rbf must not decrease payment
self.wallet.sign_transaction(tx, password) self.wallet.sign_transaction(tx, password)
# save swap data in wallet in case we need a refund # save swap data in wallet in case we need a refund
receive_address = self.wallet.get_receiving_address()
swap = SwapData( swap = SwapData(
redeem_script = redeem_script, redeem_script = redeem_script,
locktime = locktime, locktime = locktime,
@ -338,6 +340,7 @@ class SwapManager(Logger):
prepay_hash = None, prepay_hash = None,
lockup_address = lockup_address, lockup_address = lockup_address,
onchain_amount = expected_onchain_amount_sat, onchain_amount = expected_onchain_amount_sat,
receive_address = receive_address,
lightning_amount = lightning_amount_sat, lightning_amount = lightning_amount_sat,
is_reverse = False, is_reverse = False,
is_redeemed = False, is_redeemed = False,
@ -429,6 +432,7 @@ class SwapManager(Logger):
raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) " raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) "
f"not what we requested ({lightning_amount_sat})") f"not what we requested ({lightning_amount_sat})")
# save swap data to wallet file # save swap data to wallet file
receive_address = self.wallet.get_receiving_address()
swap = SwapData( swap = SwapData(
redeem_script = redeem_script, redeem_script = redeem_script,
locktime = locktime, locktime = locktime,
@ -437,6 +441,7 @@ class SwapManager(Logger):
prepay_hash = prepay_hash, prepay_hash = prepay_hash,
lockup_address = lockup_address, lockup_address = lockup_address,
onchain_amount = onchain_amount, onchain_amount = onchain_amount,
receive_address = receive_address,
lightning_amount = lightning_amount_sat, lightning_amount = lightning_amount_sat,
is_reverse = True, is_reverse = True,
is_redeemed = False, is_redeemed = False,

86
electrum/wallet.py

@ -73,8 +73,8 @@ from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
from .plugin import run_hook from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
from .invoices import Invoice, OnchainInvoice, LNInvoice from .invoices import Invoice
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED, PR_TYPE_ONCHAIN, PR_TYPE_LN from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_UNCONFIRMED
from .contacts import Contacts from .contacts import Contacts
from .interface import NetworkException from .interface import NetworkException
from .mnemonic import Mnemonic from .mnemonic import Mnemonic
@ -756,7 +756,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice: def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
height=self.get_local_height() height=self.get_local_height()
if pr: if pr:
return OnchainInvoice.from_bip70_payreq(pr, height) return Invoice.from_bip70_payreq(pr, height)
amount = 0 amount = 0
for x in outputs: for x in outputs:
if parse_max_spend(x.value): if parse_max_spend(x.value):
@ -771,25 +771,21 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
exp = URI.get('exp') exp = URI.get('exp')
timestamp = timestamp or int(time.time()) timestamp = timestamp or int(time.time())
exp = exp or 0 exp = exp or 0
_id = bh2u(sha256d(repr(outputs) + "%d"%timestamp))[0:10] invoice = Invoice(
invoice = OnchainInvoice( amount_msat=amount*1000,
type=PR_TYPE_ONCHAIN,
amount_sat=amount,
outputs=outputs,
message=message, message=message,
id=_id,
time=timestamp, time=timestamp,
exp=exp, exp=exp,
outputs=outputs,
bip70=None, bip70=None,
requestor=None,
height=height, height=height,
lightning_invoice=None,
) )
return invoice return invoice
def save_invoice(self, invoice: Invoice) -> None: def save_invoice(self, invoice: Invoice) -> None:
key = self.get_key_for_outgoing_invoice(invoice) key = self.get_key_for_outgoing_invoice(invoice)
if not invoice.is_lightning(): if not invoice.is_lightning():
assert isinstance(invoice, OnchainInvoice)
if self.is_onchain_invoice_paid(invoice, 0): if self.is_onchain_invoice_paid(invoice, 0):
self.logger.info("saving invoice... but it is already paid!") self.logger.info("saving invoice... but it is already paid!")
with self.transaction_lock: with self.transaction_lock:
@ -821,7 +817,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def import_requests(self, path): def import_requests(self, path):
data = read_json_file(path) data = read_json_file(path)
for x in data: for x in data:
req = Invoice.from_json(x) req = Invoice(**x)
self.add_payment_request(req) self.add_payment_request(req)
def export_requests(self, path): def export_requests(self, path):
@ -830,7 +826,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def import_invoices(self, path): def import_invoices(self, path):
data = read_json_file(path) data = read_json_file(path)
for x in data: for x in data:
invoice = Invoice.from_json(x) invoice = Invoice(**x)
self.save_invoice(invoice) self.save_invoice(invoice)
def export_invoices(self, path): def export_invoices(self, path):
@ -846,27 +842,25 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
relevant_invoice_keys.add(invoice_key) relevant_invoice_keys.add(invoice_key)
return relevant_invoice_keys return relevant_invoice_keys
def get_relevant_invoices_for_tx(self, tx: Transaction) -> Sequence[OnchainInvoice]: def get_relevant_invoices_for_tx(self, tx: Transaction) -> Sequence[Invoice]:
invoice_keys = self._get_relevant_invoice_keys_for_tx(tx) invoice_keys = self._get_relevant_invoice_keys_for_tx(tx)
invoices = [self.get_invoice(key) for key in invoice_keys] invoices = [self.get_invoice(key) for key in invoice_keys]
invoices = [inv for inv in invoices if inv] # filter out None invoices = [inv for inv in invoices if inv] # filter out None
for inv in invoices: for inv in invoices:
assert isinstance(inv, OnchainInvoice), f"unexpected type {type(inv)}" assert isinstance(inv, Invoice), f"unexpected type {type(inv)}"
return invoices return invoices
def _prepare_onchain_invoice_paid_detection(self): def _prepare_onchain_invoice_paid_detection(self):
# scriptpubkey -> list(invoice_keys) # scriptpubkey -> list(invoice_keys)
self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]] self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]]
for invoice_key, invoice in self.invoices.items(): for invoice_key, invoice in self.invoices.items():
if invoice.type == PR_TYPE_ONCHAIN: if not invoice.is_lightning():
assert isinstance(invoice, OnchainInvoice)
for txout in invoice.outputs: for txout in invoice.outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key) self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
def _is_onchain_invoice_paid(self, invoice: Invoice, conf: int) -> Tuple[bool, Sequence[str]]: 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.""" """Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
assert invoice.type == PR_TYPE_ONCHAIN assert not invoice.is_lightning()
assert isinstance(invoice, OnchainInvoice)
invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats invoice_amounts = defaultdict(int) # type: Dict[bytes, int] # scriptpubkey -> value_sats
for txo in invoice.outputs: # type: PartialTxOutput for txo in invoice.outputs: # type: PartialTxOutput
invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value invoice_amounts[txo.scriptpubkey] += 1 if parse_max_spend(txo.value) else txo.value
@ -2100,7 +2094,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def delete_address(self, address: str) -> None: def delete_address(self, address: str) -> None:
raise Exception("this wallet cannot delete addresses") raise Exception("this wallet cannot delete addresses")
def get_onchain_request_status(self, r: OnchainInvoice) -> Tuple[bool, Optional[int]]: def get_onchain_request_status(self, r: Invoice) -> Tuple[bool, Optional[int]]:
address = r.get_address() address = r.get_address()
amount = r.get_amount_sat() amount = r.get_amount_sat()
received, sent = self.get_addr_io(address) received, sent = self.get_addr_io(address)
@ -2121,14 +2115,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return True, conf return True, conf
return False, None return False, None
def get_request_URI(self, req: OnchainInvoice) -> str: def get_request_URI(self, req: Invoice) -> str:
addr = req.get_address() addr = req.get_address()
message = self.get_label(addr) message = self.get_label(addr)
amount = req.amount_sat amount = req.get_amount_sat()
extra_query_params = {} extra_query_params = {}
if req.time: if req.time and req.exp:
extra_query_params['time'] = str(int(req.time)) extra_query_params['time'] = str(int(req.time))
if req.exp:
extra_query_params['exp'] = str(int(req.exp)) extra_query_params['exp'] = str(int(req.exp))
#if req.get('name') and req.get('sig'): #if req.get('name') and req.get('sig'):
# sig = bfh(req.get('sig')) # sig = bfh(req.get('sig'))
@ -2139,9 +2132,9 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return str(uri) return str(uri)
def check_expired_status(self, r: Invoice, status): def check_expired_status(self, r: Invoice, status):
if r.is_lightning() and r.exp == 0: #if r.is_lightning() and r.exp == 0:
status = PR_EXPIRED # for BOLT-11 invoices, exp==0 means 0 seconds # status = PR_EXPIRED # for BOLT-11 invoices, exp==0 means 0 seconds
if status == PR_UNPAID and r.exp > 0 and r.time + r.exp < time.time(): if status == PR_UNPAID and r.get_expiration_date() and r.get_expiration_date() < time.time():
status = PR_EXPIRED status = PR_EXPIRED
return status return status
@ -2162,10 +2155,8 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if r is None: if r is None:
return PR_UNKNOWN return PR_UNKNOWN
if r.is_lightning(): if r.is_lightning():
assert isinstance(r, LNInvoice)
status = self.lnworker.get_payment_status(bfh(r.rhash)) if self.lnworker else PR_UNKNOWN status = self.lnworker.get_payment_status(bfh(r.rhash)) if self.lnworker else PR_UNKNOWN
else: else:
assert isinstance(r, OnchainInvoice)
paid, conf = self.get_onchain_request_status(r) paid, conf = self.get_onchain_request_status(r)
if not paid: if not paid:
status = PR_UNPAID status = PR_UNPAID
@ -2192,20 +2183,18 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
'is_lightning': is_lightning, 'is_lightning': is_lightning,
'amount_BTC': format_satoshis(x.get_amount_sat()), 'amount_BTC': format_satoshis(x.get_amount_sat()),
'message': x.message, 'message': x.message,
'timestamp': x.time, 'timestamp': x.get_time(),
'expiration': x.exp, 'expiration': x.get_expiry(),
'status': status, 'status': status,
'status_str': status_str, 'status_str': status_str,
} }
if is_lightning: if is_lightning:
assert isinstance(x, LNInvoice)
d['rhash'] = x.rhash d['rhash'] = x.rhash
d['invoice'] = x.invoice d['invoice'] = x.invoice
d['amount_msat'] = x.get_amount_msat() d['amount_msat'] = x.get_amount_msat()
if self.lnworker and status == PR_UNPAID: if self.lnworker and status == PR_UNPAID:
d['can_receive'] = self.lnworker.can_receive_invoice(x) d['can_receive'] = self.lnworker.can_receive_invoice(x)
else: else:
assert isinstance(x, OnchainInvoice)
paid, conf = self.get_onchain_request_status(x) paid, conf = self.get_onchain_request_status(x)
d['amount_sat'] = x.get_amount_sat() d['amount_sat'] = x.get_amount_sat()
d['address'] = x.get_address() d['address'] = x.get_address()
@ -2239,13 +2228,11 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
'status_str': status_str, 'status_str': status_str,
} }
if is_lightning: if is_lightning:
assert isinstance(x, LNInvoice)
d['invoice'] = x.invoice d['invoice'] = x.invoice
d['amount_msat'] = x.get_amount_msat() d['amount_msat'] = x.get_amount_msat()
if self.lnworker and status == PR_UNPAID: if self.lnworker and status == PR_UNPAID:
d['can_pay'] = self.lnworker.can_pay_invoice(x) d['can_pay'] = self.lnworker.can_pay_invoice(x)
else: else:
assert isinstance(x, OnchainInvoice)
amount_sat = x.get_amount_sat() amount_sat = x.get_amount_sat()
assert isinstance(amount_sat, (int, str, type(None))) assert isinstance(amount_sat, (int, str, type(None)))
d['amount_sat'] = amount_sat d['amount_sat'] = amount_sat
@ -2281,29 +2268,28 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
status = self.get_request_status(addr) status = self.get_request_status(addr)
util.trigger_callback('request_status', self, addr, status) util.trigger_callback('request_status', self, addr, status)
def make_payment_request(self, address, amount_sat, message, expiration): def make_payment_request(self, amount_sat, message, timestamp, expiration, address=None, lightning_invoice=None):
# TODO maybe merge with wallet.create_invoice()... # TODO maybe merge with wallet.create_invoice()...
# note that they use incompatible "id" # note that they use incompatible "id"
amount_sat = amount_sat or 0 amount_sat = amount_sat or 0
timestamp = int(time.time()) #_id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
_id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
expiration = expiration or 0 expiration = expiration or 0
return OnchainInvoice( outputs=[PartialTxOutput.from_address_and_value(address, amount_sat)] if address else []
type=PR_TYPE_ONCHAIN, return Invoice(
outputs=[PartialTxOutput.from_address_and_value(address, amount_sat)], outputs=outputs,
message=message, message=message,
time=timestamp, time=timestamp,
amount_sat=amount_sat, amount_msat=amount_sat*1000,
exp=expiration, exp=expiration,
id=_id,
bip70=None,
requestor=None,
height=self.get_local_height(), height=self.get_local_height(),
bip70=None,
lightning_invoice=lightning_invoice,
) )
def sign_payment_request(self, key, alias, alias_addr, password): # FIXME this is broken def sign_payment_request(self, key, alias, alias_addr, password): # FIXME this is broken
raise
req = self.receive_requests.get(key) req = self.receive_requests.get(key)
assert isinstance(req, OnchainInvoice) assert not req.is_lightning()
alias_privkey = self.export_private_key(alias_addr, password) alias_privkey = self.export_private_key(alias_addr, password)
pr = paymentrequest.make_unsigned_request(req) pr = paymentrequest.make_unsigned_request(req)
paymentrequest.sign_request_with_alias(pr, alias, alias_privkey) paymentrequest.sign_request_with_alias(pr, alias, alias_privkey)
@ -2316,17 +2302,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def get_key_for_outgoing_invoice(cls, invoice: Invoice) -> str: def get_key_for_outgoing_invoice(cls, invoice: Invoice) -> str:
"""Return the key to use for this invoice in self.invoices.""" """Return the key to use for this invoice in self.invoices."""
if invoice.is_lightning(): if invoice.is_lightning():
assert isinstance(invoice, LNInvoice)
key = invoice.rhash key = invoice.rhash
else: else:
assert isinstance(invoice, OnchainInvoice) key = bh2u(sha256d(repr(invoice.outputs) + "%d"%invoice.time))[0:10]
key = invoice.id
return key return key
def get_key_for_receive_request(self, req: Invoice, *, sanity_checks: bool = False) -> str: 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.""" """Return the key to use for this invoice in self.receive_requests."""
if not req.is_lightning(): if not req.is_lightning():
assert isinstance(req, OnchainInvoice)
addr = req.get_address() addr = req.get_address()
if sanity_checks: if sanity_checks:
if not bitcoin.is_address(addr): if not bitcoin.is_address(addr):
@ -2335,7 +2318,6 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
raise Exception(_('Address not in wallet.')) raise Exception(_('Address not in wallet.'))
key = addr key = addr
else: else:
assert isinstance(req, LNInvoice)
key = req.rhash key = req.rhash
return key return key
@ -2346,7 +2328,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.set_label(key, message) # should be a default label self.set_label(key, message) # should be a default label
if write_to_disk: if write_to_disk:
self.save_db() self.save_db()
return req return key
def delete_request(self, key): def delete_request(self, key):
""" lightning or on-chain """ """ lightning or on-chain """

50
electrum/wallet_db.py

@ -53,7 +53,7 @@ if TYPE_CHECKING:
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 44 # electrum >= 2.7 will set this to prevent FINAL_SEED_VERSION = 45 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format # old versions from overwriting new format
@ -193,6 +193,7 @@ class WalletDB(JsonDB):
self._convert_version_42() self._convert_version_42()
self._convert_version_43() self._convert_version_43()
self._convert_version_44() self._convert_version_44()
self._convert_version_45()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks() self._after_upgrade_tasks()
@ -864,6 +865,49 @@ class WalletDB(JsonDB):
item['channel_type'] = channel_type item['channel_type'] = channel_type
self.data['seed_version'] = 44 self.data['seed_version'] = 44
def _convert_version_45(self):
from .lnaddr import lndecode
if not self._is_upgrade_method_needed(44, 44):
return
swaps = self.data.get('submarine_swaps', {})
for key, item in swaps.items():
item['receive_address'] = None
# note: we set height to zero
# the new key for all requests is a wallet address, not done here
for name in ['invoices', 'payment_requests']:
invoices = self.data.get(name, {})
for key, item in invoices.items():
is_lightning = item['type'] == 2
lightning_invoice = item['invoice'] if is_lightning else None
outputs = item['outputs'] if not is_lightning else None
bip70 = item['bip70'] if not is_lightning else None
if is_lightning:
lnaddr = lndecode(item['invoice'])
amount_msat = lnaddr.get_amount_msat()
timestamp = lnaddr.date
exp_delay = lnaddr.get_expiry()
message = lnaddr.get_description()
height = 0
else:
amount_sat = item['amount_sat']
amount_msat = amount_sat * 1000 if amount_sat not in [None, '!'] else amount_sat
message = item['message']
timestamp = item['time']
exp_delay = item['exp']
height = item['height']
invoices[key] = {
'amount_msat':amount_msat,
'message':message,
'time':timestamp,
'exp':exp_delay,
'height':height,
'outputs':outputs,
'bip70':bip70,
'lightning_invoice':lightning_invoice,
}
self.data['seed_version'] = 45
def _convert_imported(self): def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
return return
@ -1350,9 +1394,9 @@ class WalletDB(JsonDB):
# note: for performance, "deserialize=False" so that we will deserialize these on-demand # note: for performance, "deserialize=False" so that we will deserialize these on-demand
v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items()) v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items())
if key == 'invoices': if key == 'invoices':
v = dict((k, Invoice.from_json(x)) for k, x in v.items()) v = dict((k, Invoice(**x)) for k, x in v.items())
if key == 'payment_requests': if key == 'payment_requests':
v = dict((k, Invoice.from_json(x)) for k, x in v.items()) v = dict((k, Invoice(**x)) for k, x in v.items())
elif key == 'adds': elif key == 'adds':
v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items()) v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
elif key == 'fee_updates': elif key == 'fee_updates':

Loading…
Cancel
Save