Browse Source

Merge pull request #6256 from SomberNight/202006_invoices_need_msat_precision_2

LN invoices: support msat precision (alt 2nd approach)
bip39-recovery
ThomasV 5 years ago
committed by GitHub
parent
commit
0d156bc3e9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      electrum/commands.py
  2. 8
      electrum/gui/kivy/uix/dialogs/invoice_dialog.py
  3. 2
      electrum/gui/kivy/uix/dialogs/lightning_open_channel.py
  4. 6
      electrum/gui/kivy/uix/dialogs/request_dialog.py
  5. 30
      electrum/gui/kivy/uix/screens.py
  6. 9
      electrum/gui/qt/amountedit.py
  7. 2
      electrum/gui/qt/invoice_list.py
  8. 36
      electrum/gui/qt/main_window.py
  9. 13
      electrum/gui/qt/request_list.py
  10. 118
      electrum/invoices.py
  11. 3
      electrum/json_db.py
  12. 15
      electrum/lnaddr.py
  13. 2
      electrum/lnchannel.py
  14. 44
      electrum/lnworker.py
  15. 2
      electrum/paymentrequest.py
  16. 2
      electrum/tests/test_lnpeer.py
  17. 1
      electrum/util.py
  18. 59
      electrum/wallet.py
  19. 32
      electrum/wallet_db.py

18
electrum/commands.py

@ -990,23 +990,14 @@ class Commands:
return chan.funding_outpoint.to_str() return chan.funding_outpoint.to_str()
@command('') @command('')
async def decode_invoice(self, invoice): async def decode_invoice(self, invoice: str):
from .lnaddr import lndecode invoice = LNInvoice.from_bech32(invoice)
lnaddr = lndecode(invoice) return invoice.to_debug_json()
return {
'pubkey': lnaddr.pubkey.serialize().hex(),
'amount_BTC': lnaddr.amount,
'rhash': lnaddr.paymenthash.hex(),
'description': lnaddr.get_description(),
'exp': lnaddr.get_expiry(),
'time': lnaddr.date,
#'tags': str(lnaddr.tags),
}
@command('wn') @command('wn')
async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None): async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None):
lnworker = wallet.lnworker lnworker = wallet.lnworker
lnaddr = lnworker._check_invoice(invoice, None) lnaddr = lnworker._check_invoice(invoice)
payment_hash = lnaddr.paymenthash payment_hash = lnaddr.paymenthash
wallet.save_invoice(LNInvoice.from_bech32(invoice)) wallet.save_invoice(LNInvoice.from_bech32(invoice))
success, log = await lnworker._pay(invoice, attempts=attempts) success, log = await lnworker._pay(invoice, attempts=attempts)
@ -1026,7 +1017,6 @@ class Commands:
async def list_channels(self, wallet: Abstract_Wallet = None): async def list_channels(self, wallet: Abstract_Wallet = None):
# we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels # we output the funding_outpoint instead of the channel_id because lnd uses channel_point (funding outpoint) to identify channels
from .lnutil import LOCAL, REMOTE, format_short_channel_id from .lnutil import LOCAL, REMOTE, format_short_channel_id
encoder = util.MyEncoder()
l = list(wallet.lnworker.channels.items()) l = list(wallet.lnworker.channels.items())
return [ return [
{ {

8
electrum/gui/kivy/uix/dialogs/invoice_dialog.py

@ -44,7 +44,7 @@ Builder.load_string('''
RefLabel: RefLabel:
data: root.description or _('No description') data: root.description or _('No description')
TopLabel: TopLabel:
text: _('Amount') + ': ' + app.format_amount_and_units(root.amount) text: _('Amount') + ': ' + app.format_amount_and_units(root.amount_sat)
TopLabel: TopLabel:
text: _('Status') + ': ' + root.status_str text: _('Status') + ': ' + root.status_str
color: root.status_color color: root.status_color
@ -93,9 +93,9 @@ class InvoiceDialog(Factory.Popup):
self.data = data self.data = data
self.key = key self.key = key
invoice = self.app.wallet.get_invoice(key) invoice = self.app.wallet.get_invoice(key)
self.amount = invoice.amount self.amount_sat = invoice.get_amount_sat()
self.description = invoice.message self.description = invoice.message
self.is_lightning = invoice.type == PR_TYPE_LN self.is_lightning = invoice.is_lightning()
self.update_status() self.update_status()
self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else [] self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else []
@ -106,7 +106,7 @@ class InvoiceDialog(Factory.Popup):
self.status_color = pr_color[self.status] self.status_color = pr_color[self.status]
self.can_pay = self.status in [PR_UNPAID, PR_FAILED] self.can_pay = self.status in [PR_UNPAID, PR_FAILED]
if self.can_pay and self.is_lightning and self.app.wallet.lnworker: if self.can_pay and self.is_lightning and self.app.wallet.lnworker:
if self.amount and self.amount > self.app.wallet.lnworker.num_sats_can_send(): if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_send():
self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently send with your channels') self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently send with your channels')
def on_dismiss(self): def on_dismiss(self):

2
electrum/gui/kivy/uix/dialogs/lightning_open_channel.py

@ -118,7 +118,7 @@ class LightningOpenChannelDialog(Factory.Popup):
fee = self.app.electrum_config.fee_per_kb() fee = self.app.electrum_config.fee_per_kb()
if not fee: if not fee:
fee = config.FEERATE_FALLBACK_STATIC_FEE fee = config.FEERATE_FALLBACK_STATIC_FEE
self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2) self.amount = self.app.format_amount_and_units(self.lnaddr.amount * COIN + fee * 2) # FIXME magic number?!
self.pubkey = bh2u(self.lnaddr.pubkey.serialize()) self.pubkey = bh2u(self.lnaddr.pubkey.serialize())
if self.msg: if self.msg:
self.app.show_info(self.msg) self.app.show_info(self.msg)

6
electrum/gui/kivy/uix/dialogs/request_dialog.py

@ -44,7 +44,7 @@ Builder.load_string('''
TopLabel: TopLabel:
text: _('Description') + ': ' + root.description or _('None') text: _('Description') + ': ' + root.description or _('None')
TopLabel: TopLabel:
text: _('Amount') + ': ' + app.format_amount_and_units(root.amount) text: _('Amount') + ': ' + app.format_amount_and_units(root.amount_sat)
TopLabel: TopLabel:
text: (_('Address') if not root.is_lightning else _('Payment hash')) + ': ' text: (_('Address') if not root.is_lightning else _('Payment hash')) + ': '
RefLabel: RefLabel:
@ -93,7 +93,7 @@ class RequestDialog(Factory.Popup):
r = self.app.wallet.get_request(key) r = self.app.wallet.get_request(key)
self.is_lightning = r.is_lightning() self.is_lightning = r.is_lightning()
self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r) self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r)
self.amount = r.amount or 0 self.amount_sat = r.get_amount_sat() or 0
self.description = r.message self.description = r.message
self.update_status() self.update_status()
@ -111,7 +111,7 @@ class RequestDialog(Factory.Popup):
self.status_str = req.get_status_str(self.status) self.status_str = req.get_status_str(self.status)
self.status_color = pr_color[self.status] self.status_color = pr_color[self.status]
if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker: if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker:
if self.amount and self.amount > self.app.wallet.lnworker.num_sats_can_receive(): if self.amount_sat and self.amount_sat > self.app.wallet.lnworker.num_sats_can_receive():
self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels') self.warning = _('Warning') + ': ' + _('This amount exceeds the maximum you can currently receive with your channels')
def on_dismiss(self): def on_dismiss(self):

30
electrum/gui/kivy/uix/screens.py

@ -4,7 +4,7 @@ from decimal import Decimal
import re import re
import threading import threading
import traceback, sys import traceback, sys
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, List, Optional, Dict, Any
from kivy.app import App from kivy.app import App
from kivy.cache import Cache from kivy.cache import Cache
@ -26,7 +26,7 @@ from kivy.logger import Logger
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, from electrum.invoices import (PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING,
PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_PAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
LNInvoice, pr_expiration_values) LNInvoice, pr_expiration_values, Invoice, OnchainInvoice)
from electrum import bitcoin, constants from electrum import bitcoin, constants
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice
@ -224,17 +224,19 @@ class SendScreen(CScreen):
def show_item(self, obj): def show_item(self, obj):
self.app.show_invoice(obj.is_lightning, obj.key) self.app.show_invoice(obj.is_lightning, obj.key)
def get_card(self, item): def get_card(self, item: Invoice):
status = self.app.wallet.get_invoice_status(item) status = self.app.wallet.get_invoice_status(item)
status_str = item.get_status_str(status) status_str = item.get_status_str(status)
is_lightning = item.type == PR_TYPE_LN is_lightning = item.type == PR_TYPE_LN
if is_lightning: if is_lightning:
assert isinstance(item, LNInvoice)
key = item.rhash key = item.rhash
log = self.app.wallet.lnworker.logs.get(key) log = self.app.wallet.lnworker.logs.get(key)
if status == PR_INFLIGHT and log: if status == PR_INFLIGHT and log:
status_str += '... (%d)'%len(log) status_str += '... (%d)'%len(log)
is_bip70 = False is_bip70 = False
else: else:
assert isinstance(item, OnchainInvoice)
key = item.id key = item.id
is_bip70 = bool(item.bip70) is_bip70 = bool(item.bip70)
return { return {
@ -245,7 +247,7 @@ class SendScreen(CScreen):
'status_str': status_str, 'status_str': status_str,
'key': key, 'key': key,
'memo': item.message, 'memo': item.message,
'amount': self.app.format_amount_and_units(item.amount or 0), 'amount': self.app.format_amount_and_units(item.get_amount_sat() or 0),
} }
def do_clear(self): def do_clear(self):
@ -345,16 +347,18 @@ class SendScreen(CScreen):
else: else:
do_pay(False) do_pay(False)
def _do_pay_lightning(self, invoice): def _do_pay_lightning(self, invoice: LNInvoice) -> None:
attempts = 10
threading.Thread( threading.Thread(
target=self.app.wallet.lnworker.pay, target=self.app.wallet.lnworker.pay,
args=(invoice.invoice, invoice.amount), args=(invoice.invoice,),
kwargs={'attempts':10}).start() kwargs={
'attempts': 10,
},
).start()
def _do_pay_onchain(self, invoice, rbf): def _do_pay_onchain(self, invoice: OnchainInvoice, rbf: bool) -> None:
# make unsigned transaction # make unsigned transaction
outputs = invoice.outputs # type: List[PartialTxOutput] outputs = invoice.outputs
coins = self.app.wallet.get_spendable_coins(None) coins = self.app.wallet.get_spendable_coins(None)
try: try:
tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs) tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
@ -482,15 +486,17 @@ class ReceiveScreen(CScreen):
self.update() self.update()
self.app.show_request(lightning, key) self.app.show_request(lightning, key)
def get_card(self, req): def get_card(self, req: Invoice) -> Dict[str, Any]:
is_lightning = req.is_lightning() is_lightning = req.is_lightning()
if not is_lightning: if not is_lightning:
assert isinstance(req, OnchainInvoice)
address = req.get_address() address = req.get_address()
key = address key = address
else: else:
assert isinstance(req, LNInvoice)
key = req.rhash key = req.rhash
address = req.invoice address = req.invoice
amount = req.amount amount = req.get_amount_sat()
description = req.message description = req.message
status = self.app.wallet.get_request_status(key) status = self.app.wallet.get_request_status(key)
status_str = req.get_status_str(status) status_str = req.get_status_str(status)

9
electrum/gui/qt/amountedit.py

@ -92,6 +92,7 @@ class BTCAmountEdit(AmountEdit):
return decimal_point_to_base_unit_name(self.decimal_point()) return decimal_point_to_base_unit_name(self.decimal_point())
def get_amount(self): def get_amount(self):
# returns amt in satoshis
try: try:
x = Decimal(str(self.text())) x = Decimal(str(self.text()))
except: except:
@ -106,11 +107,11 @@ class BTCAmountEdit(AmountEdit):
amount = Decimal(max_prec_amount) / pow(10, self.max_precision()-self.decimal_point()) amount = Decimal(max_prec_amount) / pow(10, self.max_precision()-self.decimal_point())
return Decimal(amount) if not self.is_int else int(amount) return Decimal(amount) if not self.is_int else int(amount)
def setAmount(self, amount): def setAmount(self, amount_sat):
if amount is None: if amount_sat is None:
self.setText(" ") # Space forces repaint in case units changed self.setText(" ") # Space forces repaint in case units changed
else: else:
self.setText(format_satoshis_plain(amount, decimal_point=self.decimal_point())) self.setText(format_satoshis_plain(amount_sat, decimal_point=self.decimal_point()))
class FeerateEdit(BTCAmountEdit): class FeerateEdit(BTCAmountEdit):

2
electrum/gui/qt/invoice_list.py

@ -110,7 +110,7 @@ class InvoiceList(MyTreeView):
status = self.parent.wallet.get_invoice_status(item) status = self.parent.wallet.get_invoice_status(item)
status_str = item.get_status_str(status) status_str = item.get_status_str(status)
message = item.message message = item.message
amount = item.amount amount = item.get_amount_sat()
timestamp = item.time or 0 timestamp = item.time or 0
date_str = format_time(timestamp) if timestamp else _('Unknown') date_str = format_time(timestamp) if timestamp else _('Unknown')
amount_str = self.parent.format_amount(amount, whitespaces=True) amount_str = self.parent.format_amount(amount, whitespaces=True)

36
electrum/gui/qt/main_window.py

@ -61,7 +61,7 @@ from electrum.util import (format_time,
get_new_wallet_name, send_exception_to_crash_reporter, get_new_wallet_name, send_exception_to_crash_reporter,
InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds, InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs) NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs)
from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, 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, LNInvoice, OnchainInvoice
from electrum.transaction import (Transaction, PartialTxInput, from electrum.transaction import (Transaction, PartialTxInput,
PartialTransaction, PartialTxOutput) PartialTransaction, PartialTxOutput)
@ -159,6 +159,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
show_privkeys_signal = pyqtSignal() show_privkeys_signal = pyqtSignal()
show_error_signal = pyqtSignal(str) show_error_signal = pyqtSignal(str)
payment_request: Optional[paymentrequest.PaymentRequest]
def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):
QMainWindow.__init__(self) QMainWindow.__init__(self)
@ -877,9 +879,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.notify_transactions() self.notify_transactions()
def format_amount(self, x, is_diff=False, whitespaces=False): def format_amount(self, x, is_diff=False, whitespaces=False):
# x is in sats
return self.config.format_amount(x, is_diff=is_diff, whitespaces=whitespaces) return self.config.format_amount(x, is_diff=is_diff, whitespaces=whitespaces)
def format_amount_and_units(self, amount): def format_amount_and_units(self, amount):
# amount is in sats
text = self.config.format_amount_and_units(amount) text = self.config.format_amount_and_units(amount)
x = self.fx.format_amount_and_units(amount) if self.fx else None x = self.fx.format_amount_and_units(amount) if self.fx else None
if text and x: if text and x:
@ -1480,13 +1484,17 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return False # no errors return False # no errors
def pay_lightning_invoice(self, invoice: str, amount_sat: int): def pay_lightning_invoice(self, invoice: str, *, amount_msat: Optional[int]):
if amount_msat is None:
raise Exception("missing amount for LN invoice")
amount_sat = Decimal(amount_msat) / 1000
# FIXME this is currently lying to user as we truncate to satoshis
msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(amount_sat)) msg = _("Pay lightning invoice?") + '\n\n' + _("This will send {}?").format(self.format_amount_and_units(amount_sat))
if not self.question(msg): if not self.question(msg):
return return
attempts = LN_NUM_PAYMENT_ATTEMPTS attempts = LN_NUM_PAYMENT_ATTEMPTS
def task(): def task():
self.wallet.lnworker.pay(invoice, amount_sat, attempts=attempts) self.wallet.lnworker.pay(invoice, amount_msat=amount_msat, attempts=attempts)
self.do_clear() self.do_clear()
self.wallet.thread.add(task) self.wallet.thread.add(task)
self.invoice_list.update() self.invoice_list.update()
@ -1523,10 +1531,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.show_error(_('Lightning is disabled')) self.show_error(_('Lightning is disabled'))
return return
invoice = LNInvoice.from_bech32(invoice_str) invoice = LNInvoice.from_bech32(invoice_str)
if invoice.amount is None: if invoice.get_amount_msat() is None:
amount = self.amount_e.get_amount() amount_sat = self.amount_e.get_amount()
if amount: if amount_sat:
invoice.amount = amount invoice.amount_msat = int(amount_sat * 1000)
else: else:
self.show_error(_('No amount')) self.show_error(_('No amount'))
return return
@ -1565,10 +1573,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
outputs += invoice.outputs outputs += invoice.outputs
self.pay_onchain_dialog(self.get_coins(), outputs) self.pay_onchain_dialog(self.get_coins(), outputs)
def do_pay_invoice(self, invoice): def do_pay_invoice(self, invoice: 'Invoice'):
if invoice.type == PR_TYPE_LN: if invoice.type == PR_TYPE_LN:
self.pay_lightning_invoice(invoice.invoice, invoice.amount) assert isinstance(invoice, LNInvoice)
self.pay_lightning_invoice(invoice.invoice, amount_msat=invoice.get_amount_msat())
elif invoice.type == PR_TYPE_ONCHAIN: elif invoice.type == PR_TYPE_ONCHAIN:
assert isinstance(invoice, OnchainInvoice)
self.pay_onchain_dialog(self.get_coins(), invoice.outputs) self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
else: else:
raise Exception('unknown invoice type') raise Exception('unknown invoice type')
@ -1837,8 +1847,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.payto_e.setFrozen(True) self.payto_e.setFrozen(True)
self.payto_e.setText(pubkey) self.payto_e.setText(pubkey)
self.message_e.setText(description) self.message_e.setText(description)
if lnaddr.amount is not None: if lnaddr.get_amount_sat() is not None:
self.amount_e.setAmount(lnaddr.amount * COIN) self.amount_e.setAmount(lnaddr.get_amount_sat())
#self.amount_e.textEdited.emit("") #self.amount_e.textEdited.emit("")
self.set_onchain(False) self.set_onchain(False)
@ -1979,7 +1989,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.update_completions() self.update_completions()
def show_onchain_invoice(self, invoice: OnchainInvoice): def show_onchain_invoice(self, invoice: OnchainInvoice):
amount_str = self.format_amount(invoice.amount) + ' ' + self.base_unit() amount_str = self.format_amount(invoice.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()
@ -2029,7 +2039,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
grid.addWidget(QLabel(_("Node ID") + ':'), 0, 0) grid.addWidget(QLabel(_("Node ID") + ':'), 0, 0)
grid.addWidget(QLabel(lnaddr.pubkey.serialize().hex()), 0, 1) grid.addWidget(QLabel(lnaddr.pubkey.serialize().hex()), 0, 1)
grid.addWidget(QLabel(_("Amount") + ':'), 1, 0) grid.addWidget(QLabel(_("Amount") + ':'), 1, 0)
amount_str = self.format_amount(invoice.amount) + ' ' + self.base_unit() amount_str = self.format_amount(invoice.get_amount_sat()) + ' ' + self.base_unit()
grid.addWidget(QLabel(amount_str), 1, 1) grid.addWidget(QLabel(amount_str), 1, 1)
grid.addWidget(QLabel(_("Description") + ':'), 2, 0) grid.addWidget(QLabel(_("Description") + ':'), 2, 0)
grid.addWidget(QLabel(invoice.message), 2, 1) grid.addWidget(QLabel(invoice.message), 2, 1)

13
electrum/gui/qt/request_list.py

@ -32,7 +32,7 @@ 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 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 .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
@ -130,21 +130,28 @@ class RequestList(MyTreeView):
self.std_model.clear() self.std_model.clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for req in self.wallet.get_sorted_requests(): for req in self.wallet.get_sorted_requests():
key = req.rhash if req.is_lightning() else req.id if req.is_lightning():
assert isinstance(req, LNInvoice)
key = req.rhash
else:
assert isinstance(req, OnchainInvoice)
key = req.id
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 request_type = req.type
timestamp = req.time timestamp = req.time
amount = req.amount amount = req.get_amount_sat()
message = req.message message = req.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]
if req.is_lightning(): if req.is_lightning():
assert isinstance(req, LNInvoice)
key = req.rhash key = req.rhash
icon = read_QIcon("lightning.png") icon = read_QIcon("lightning.png")
tooltip = 'lightning request' tooltip = 'lightning request'
else: else:
assert isinstance(req, OnchainInvoice)
key = req.get_address() key = req.get_address()
icon = read_QIcon("bitcoin.png") icon = read_QIcon("bitcoin.png")
tooltip = 'onchain request' tooltip = 'onchain request'

118
electrum/invoices.py

@ -1,11 +1,13 @@
import attr
import time import time
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, List, Optional, Union, Dict, Any
from decimal import Decimal
import attr
from .json_db import StoredObject from .json_db import StoredObject
from .i18n import _ from .i18n import _
from .util import age from .util import age
from .lnaddr import lndecode from .lnaddr import lndecode, LnAddr
from . import constants from . import constants
from .bitcoin import COIN from .bitcoin import COIN
from .transaction import PartialTxOutput from .transaction import PartialTxOutput
@ -67,6 +69,7 @@ def _decode_outputs(outputs) -> List[PartialTxOutput]:
ret.append(output) ret.append(output)
return ret return ret
# hack: BOLT-11 is not really clear on what an expiry of 0 means. # hack: BOLT-11 is not really clear on what an expiry of 0 means.
# It probably interprets it as 0 seconds, so already expired... # It probably interprets it as 0 seconds, so already expired...
# Our higher level invoices code however uses 0 for "never". # Our higher level invoices code however uses 0 for "never".
@ -75,11 +78,11 @@ 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) type = attr.ib(type=int, kw_only=True)
message = attr.ib(type=str)
amount = attr.ib(type=int) message: str
exp = attr.ib(type=int) exp: int
time = attr.ib(type=int) time: int
def is_lightning(self): def is_lightning(self):
return self.type == PR_TYPE_LN return self.type == PR_TYPE_LN
@ -94,22 +97,42 @@ class Invoice(StoredObject):
status_str = _('Pending') status_str = _('Pending')
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 @attr.s
class OnchainInvoice(Invoice): class OnchainInvoice(Invoice):
id = attr.ib(type=str) message = attr.ib(type=str, kw_only=True)
outputs = attr.ib(type=list, converter=_decode_outputs) amount_sat = attr.ib(kw_only=True) # type: Union[None, int, str] # in satoshis. can be '!'
bip70 = attr.ib(type=str) # may be None exp = attr.ib(type=int, kw_only=True)
requestor = attr.ib(type=str) # may be None time = attr.ib(type=int, kw_only=True)
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]
def get_address(self) -> str: def get_address(self) -> str:
assert len(self.outputs) == 1 assert len(self.outputs) == 1
return self.outputs[0].address return self.outputs[0].address
def get_amount_sat(self) -> Union[int, str, None]:
return self.amount_sat
@classmethod @classmethod
def from_bip70_payreq(cls, pr: 'PaymentRequest') -> 'OnchainInvoice': def from_bip70_payreq(cls, pr: 'PaymentRequest') -> 'OnchainInvoice':
return OnchainInvoice( return OnchainInvoice(
type=PR_TYPE_ONCHAIN, type=PR_TYPE_ONCHAIN,
amount=pr.get_amount(), amount_sat=pr.get_amount(),
outputs=pr.get_outputs(), outputs=pr.get_outputs(),
message=pr.get_memo(), message=pr.get_memo(),
id=pr.get_id(), id=pr.get_id(),
@ -121,26 +144,63 @@ class OnchainInvoice(Invoice):
@attr.s @attr.s
class LNInvoice(Invoice): class LNInvoice(Invoice):
rhash = attr.ib(type=str)
invoice = attr.ib(type=str) invoice = attr.ib(type=str)
amount_msat = attr.ib(kw_only=True) # type: Optional[int] # needed for zero amt invoices
__lnaddr = None
@property
def _lnaddr(self) -> LnAddr:
if self.__lnaddr is None:
self.__lnaddr = lndecode(self.invoice)
return self.__lnaddr
@property
def rhash(self) -> str:
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(klass, invoice: str) -> 'LNInvoice': def from_bech32(cls, invoice: str) -> 'LNInvoice':
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) amount_msat = lndecode(invoice).get_amount_msat()
amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
return LNInvoice( return LNInvoice(
type = PR_TYPE_LN, type=PR_TYPE_LN,
amount = amount, invoice=invoice,
message = lnaddr.get_description(), amount_msat=amount_msat,
time = lnaddr.date,
exp = lnaddr.get_expiry(),
rhash = lnaddr.paymenthash.hex(),
invoice = invoice,
) )
def to_debug_json(self) -> Dict[str, Any]:
d = self.to_json()
d.update({
'pubkey': self._lnaddr.pubkey.serialize().hex(),
'amount_BTC': self._lnaddr.amount,
'rhash': self._lnaddr.paymenthash.hex(),
'description': self._lnaddr.get_description(),
'exp': self._lnaddr.get_expiry(),
'time': self._lnaddr.date,
# 'tags': str(lnaddr.tags),
})
return d
def invoice_from_json(x: dict) -> Invoice:
if x.get('type') == PR_TYPE_LN:
return LNInvoice(**x)
else:
return OnchainInvoice(**x)

3
electrum/json_db.py

@ -60,6 +60,9 @@ class StoredObject:
def to_json(self): def to_json(self):
d = dict(vars(self)) d = dict(vars(self))
d.pop('db', None) d.pop('db', None)
# don't expose/store private stuff
d = {k: v for k, v in d.items()
if not k.startswith('_')}
return d return d

15
electrum/lnaddr.py

@ -6,6 +6,7 @@ import time
from hashlib import sha256 from hashlib import sha256
from binascii import hexlify from binascii import hexlify
from decimal import Decimal from decimal import Decimal
from typing import Optional
import bitstring import bitstring
@ -33,7 +34,7 @@ def shorten_amount(amount):
break break
return str(amount) + unit return str(amount) + unit
def unshorten_amount(amount): def unshorten_amount(amount) -> Decimal:
""" Given a shortened amount, convert it into a decimal """ Given a shortened amount, convert it into a decimal
""" """
# BOLT #11: # BOLT #11:
@ -271,12 +272,20 @@ class LnAddr(object):
self.signature = None self.signature = None
self.pubkey = None self.pubkey = None
self.currency = constants.net.SEGWIT_HRP if currency is None else currency self.currency = constants.net.SEGWIT_HRP if currency is None else currency
self.amount = amount # in bitcoins self.amount = amount # type: Optional[Decimal] # in bitcoins
self._min_final_cltv_expiry = 9 self._min_final_cltv_expiry = 9
def get_amount_sat(self): def get_amount_sat(self) -> Optional[Decimal]:
# note that this has msat resolution potentially
if self.amount is None:
return None
return self.amount * COIN return self.amount * COIN
def get_amount_msat(self) -> Optional[int]:
if self.amount is None:
return None
return int(self.amount * COIN * 1000)
def __str__(self): def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format( return "LnAddr[{}, amount={}{} tags=[{}]]".format(
hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None, hexlify(self.pubkey.serialize()).decode('utf-8') if self.pubkey else None,

2
electrum/lnchannel.py

@ -136,7 +136,7 @@ class RevokeAndAck(NamedTuple):
class RemoteCtnTooFarInFuture(Exception): pass class RemoteCtnTooFarInFuture(Exception): pass
def htlcsum(htlcs): def htlcsum(htlcs: Iterable[UpdateAddHtlc]):
return sum([x.amount_msat for x in htlcs]) return sum([x.amount_msat for x in htlcs])

44
electrum/lnworker.py

@ -133,7 +133,7 @@ FALLBACK_NODE_LIST_MAINNET = [
class PaymentInfo(NamedTuple): class PaymentInfo(NamedTuple):
payment_hash: bytes payment_hash: bytes
amount: int # in satoshis amount: Optional[int] # in satoshis # TODO make it msat and rename to amount_msat
direction: int direction: int
status: int status: int
@ -491,7 +491,7 @@ class LNWallet(LNWorker):
self.lnwatcher = None self.lnwatcher = None
self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ self.features |= LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid self.payments = self.db.get_dict('lightning_payments') # RHASH -> amount, direction, is_paid # FIXME amt should be msat
self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage self.preimages = self.db.get_dict('lightning_preimages') # RHASH -> preimage
self.sweep_address = wallet.get_new_sweep_address_for_channel() # TODO possible address-reuse self.sweep_address = wallet.get_new_sweep_address_for_channel() # TODO possible address-reuse
self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH # (not persisted) self.logs = defaultdict(list) # type: Dict[str, List[PaymentAttemptLog]] # key is RHASH # (not persisted)
@ -597,7 +597,7 @@ class LNWallet(LNWorker):
out[k] += v out[k] += v
return out return out
def get_payment_value(self, info, plist): def get_payment_value(self, info: Optional['PaymentInfo'], plist):
amount_msat = 0 amount_msat = 0
fee_msat = None fee_msat = None
for chan_id, htlc, _direction in plist: for chan_id, htlc, _direction in plist:
@ -832,11 +832,11 @@ class LNWallet(LNWorker):
raise Exception(_("open_channel timed out")) raise Exception(_("open_channel timed out"))
return chan, funding_tx return chan, funding_tx
def pay(self, invoice: str, amount_sat: int = None, *, attempts: int = 1) -> Tuple[bool, List[PaymentAttemptLog]]: def pay(self, invoice: str, *, amount_msat: int = None, attempts: int = 1) -> Tuple[bool, List[PaymentAttemptLog]]:
""" """
Can be called from other threads Can be called from other threads
""" """
coro = self._pay(invoice, amount_sat, attempts=attempts) coro = self._pay(invoice, amount_msat=amount_msat, attempts=attempts)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
return fut.result() return fut.result()
@ -846,10 +846,15 @@ class LNWallet(LNWorker):
return chan return chan
@log_exceptions @log_exceptions
async def _pay(self, invoice: str, amount_sat: int = None, *, async def _pay(
attempts: int = 1, self,
full_path: LNPaymentPath = None) -> Tuple[bool, List[PaymentAttemptLog]]: invoice: str,
lnaddr = self._check_invoice(invoice, amount_sat) *,
amount_msat: int = None,
attempts: int = 1,
full_path: LNPaymentPath = None,
) -> Tuple[bool, List[PaymentAttemptLog]]:
lnaddr = self._check_invoice(invoice, amount_msat=amount_msat)
payment_hash = lnaddr.paymenthash payment_hash = lnaddr.paymenthash
key = payment_hash.hex() key = payment_hash.hex()
amount = int(lnaddr.amount * COIN) amount = int(lnaddr.amount * COIN)
@ -901,7 +906,7 @@ class LNWallet(LNWorker):
await peer.initialized await peer.initialized
htlc = peer.pay(route=route, htlc = peer.pay(route=route,
chan=chan, chan=chan,
amount_msat=int(lnaddr.amount * COIN * 1000), amount_msat=lnaddr.get_amount_msat(),
payment_hash=lnaddr.paymenthash, payment_hash=lnaddr.paymenthash,
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(), min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
payment_secret=lnaddr.payment_secret) payment_secret=lnaddr.payment_secret)
@ -993,12 +998,15 @@ class LNWallet(LNWorker):
return blacklist return blacklist
@staticmethod @staticmethod
def _check_invoice(invoice: str, amount_sat: int = None) -> LnAddr: def _check_invoice(invoice: str, *, amount_msat: int = None) -> LnAddr:
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP) addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
if addr.is_expired(): if addr.is_expired():
raise InvoiceError(_("This invoice has expired")) raise InvoiceError(_("This invoice has expired"))
if amount_sat: if amount_msat: # replace amt in invoice. main usecase is paying zero amt invoices
addr.amount = Decimal(amount_sat) / COIN existing_amt_msat = addr.get_amount_msat()
if existing_amt_msat and amount_msat < existing_amt_msat:
raise Exception("cannot pay lower amt than what is originally in LN invoice")
addr.amount = Decimal(amount_msat) / COIN / 1000
if addr.amount is None: if addr.amount is None:
raise InvoiceError(_("Missing amount")) raise InvoiceError(_("Missing amount"))
if addr.get_min_final_cltv_expiry() > lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE: if addr.get_min_final_cltv_expiry() > lnutil.NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE:
@ -1010,7 +1018,7 @@ class LNWallet(LNWorker):
@profiler @profiler
def _create_route_from_invoice(self, decoded_invoice: 'LnAddr', def _create_route_from_invoice(self, decoded_invoice: 'LnAddr',
*, full_path: LNPaymentPath = None) -> LNPaymentRoute: *, full_path: LNPaymentPath = None) -> LNPaymentRoute:
amount_msat = int(decoded_invoice.amount * COIN * 1000) amount_msat = decoded_invoice.get_amount_msat()
invoice_pubkey = decoded_invoice.pubkey.serialize() invoice_pubkey = decoded_invoice.pubkey.serialize()
# use 'r' field from invoice # use 'r' field from invoice
route = None # type: Optional[LNPaymentRoute] route = None # type: Optional[LNPaymentRoute]
@ -1310,11 +1318,11 @@ class LNWallet(LNWorker):
return Decimal(max(chan.available_to_spend(REMOTE) if chan.is_open() else 0 return Decimal(max(chan.available_to_spend(REMOTE) if chan.is_open() else 0
for chan in self.channels.values()))/1000 if self.channels else 0 for chan in self.channels.values()))/1000 if self.channels else 0
def can_pay_invoice(self, invoice): def can_pay_invoice(self, invoice: LNInvoice) -> bool:
return invoice.amount <= self.num_sats_can_send() return invoice.get_amount_sat() <= self.num_sats_can_send()
def can_receive_invoice(self, invoice): def can_receive_invoice(self, invoice: LNInvoice) -> bool:
return invoice.amount <= 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):
chan = self._channels[chan_id] chan = self._channels[chan_id]

2
electrum/paymentrequest.py

@ -326,7 +326,7 @@ def make_unsigned_request(req: 'OnchainInvoice'):
time = 0 time = 0
if exp and type(exp) != int: if exp and type(exp) != int:
exp = 0 exp = 0
amount = req.amount amount = req.amount_sat
if amount is None: if amount is None:
amount = 0 amount = 0
memo = req.message memo = req.message

2
electrum/tests/test_lnpeer.py

@ -586,7 +586,7 @@ class TestPeer(ElectrumTestCase):
route = w1._create_route_from_invoice(decoded_invoice=lnaddr) route = w1._create_route_from_invoice(decoded_invoice=lnaddr)
htlc = p1.pay(route=route, htlc = p1.pay(route=route,
chan=alice_channel, chan=alice_channel,
amount_msat=int(lnaddr.amount * COIN * 1000), amount_msat=lnaddr.get_amount_msat(),
payment_hash=lnaddr.paymenthash, payment_hash=lnaddr.paymenthash,
min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(), min_final_cltv_expiry=lnaddr.get_min_final_cltv_expiry(),
payment_secret=lnaddr.payment_secret) payment_secret=lnaddr.payment_secret)

1
electrum/util.py

@ -793,6 +793,7 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional
class InvalidBitcoinURI(Exception): pass class InvalidBitcoinURI(Exception): pass
# TODO rename to parse_bip21_uri or similar
def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict: def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict:
"""Raises InvalidBitcoinURI on malformed URI.""" """Raises InvalidBitcoinURI on malformed URI."""
from . import bitcoin from . import bitcoin

59
electrum/wallet.py

@ -70,7 +70,7 @@ 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, invoice_from_json, LNInvoice from .invoices import Invoice, OnchainInvoice, LNInvoice
from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_TYPE_ONCHAIN, PR_TYPE_LN from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_TYPE_ONCHAIN, PR_TYPE_LN
from .contacts import Contacts from .contacts import Contacts
from .interface import NetworkException from .interface import NetworkException
@ -693,7 +693,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
amount = sum(x.value for x in outputs) amount = sum(x.value for x in outputs)
invoice = OnchainInvoice( invoice = OnchainInvoice(
type=PR_TYPE_ONCHAIN, type=PR_TYPE_ONCHAIN,
amount=amount, amount_sat=amount,
outputs=outputs, outputs=outputs,
message=message, message=message,
id=bh2u(sha256(repr(outputs))[0:16]), id=bh2u(sha256(repr(outputs))[0:16]),
@ -738,7 +738,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.from_json(x)
self.add_payment_request(req) self.add_payment_request(req)
def export_requests(self, path): def export_requests(self, path):
@ -747,7 +747,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.from_json(x)
self.save_invoice(invoice) self.save_invoice(invoice)
def export_invoices(self, path): def export_invoices(self, path):
@ -1630,7 +1630,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def get_request_URI(self, req: OnchainInvoice) -> str: def get_request_URI(self, req: OnchainInvoice) -> str:
addr = req.get_address() addr = req.get_address()
message = self.labels.get(addr, '') message = self.labels.get(addr, '')
amount = req.amount amount = req.amount_sat
extra_query_params = {} extra_query_params = {}
if req.time: if req.time:
extra_query_params['time'] = str(int(req.time)) extra_query_params['time'] = str(int(req.time))
@ -1663,9 +1663,11 @@ 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:
paid, conf = self.get_payment_status(r.get_address(), r.amount) assert isinstance(r, OnchainInvoice)
paid, conf = self.get_payment_status(r.get_address(), r.amount_sat)
status = PR_PAID if paid else PR_UNPAID status = PR_PAID if paid else PR_UNPAID
return self.check_expired_status(r, status) return self.check_expired_status(r, status)
@ -1689,8 +1691,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
is_lightning = x.is_lightning() is_lightning = x.is_lightning()
d = { d = {
'is_lightning': is_lightning, 'is_lightning': is_lightning,
'amount': x.amount, 'amount_BTC': format_satoshis(x.get_amount_sat()),
'amount_BTC': format_satoshis(x.amount),
'message': x.message, 'message': x.message,
'timestamp': x.time, 'timestamp': x.time,
'expiration': x.exp, 'expiration': x.exp,
@ -1698,13 +1699,19 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
'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()
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)
amount_sat = x.get_amount_sat()
assert isinstance(amount_sat, (int, str, type(None)))
d['amount_sat'] = amount_sat
addr = x.get_address() addr = x.get_address()
paid, conf = self.get_payment_status(addr, x.amount) paid, conf = self.get_payment_status(addr, x.amount_sat)
d['address'] = addr d['address'] = addr
d['URI'] = self.get_request_URI(x) d['URI'] = self.get_request_URI(x)
if conf is not None: if conf is not None:
@ -1728,8 +1735,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
is_lightning = x.is_lightning() is_lightning = x.is_lightning()
d = { d = {
'is_lightning': is_lightning, 'is_lightning': is_lightning,
'amount': x.amount, 'amount_BTC': format_satoshis(x.get_amount_sat()),
'amount_BTC': format_satoshis(x.amount),
'message': x.message, 'message': x.message,
'timestamp': x.time, 'timestamp': x.time,
'expiration': x.exp, 'expiration': x.exp,
@ -1739,10 +1745,14 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
if is_lightning: if is_lightning:
assert isinstance(x, LNInvoice) assert isinstance(x, LNInvoice)
d['invoice'] = x.invoice d['invoice'] = x.invoice
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) assert isinstance(x, OnchainInvoice)
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.outputs] d['outputs'] = [y.to_legacy_tuple() for y in x.outputs]
if x.bip70: if x.bip70:
d['bip70'] = x.bip70 d['bip70'] = x.bip70
@ -1757,20 +1767,23 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
status = self.get_request_status(addr) status = self.get_request_status(addr)
util.trigger_callback('request_status', addr, status) util.trigger_callback('request_status', addr, status)
def make_payment_request(self, address, amount, message, expiration): def make_payment_request(self, address, amount_sat, message, expiration):
amount = amount or 0 # TODO maybe merge with wallet.create_invoice()...
# note that they use incompatible "id"
amount_sat = amount_sat or 0
timestamp = int(time.time()) timestamp = int(time.time())
_id = bh2u(sha256d(address + "%d"%timestamp))[0:10] _id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
return OnchainInvoice( return OnchainInvoice(
type = PR_TYPE_ONCHAIN, type=PR_TYPE_ONCHAIN,
outputs = [(TYPE_ADDRESS, address, amount)], outputs=[(TYPE_ADDRESS, address, amount_sat)],
message = message, message=message,
time = timestamp, time=timestamp,
amount = amount, amount_sat=amount_sat,
exp = expiration, exp=expiration,
id = _id, id=_id,
bip70 = None, bip70=None,
requestor = None) requestor=None,
)
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
req = self.receive_requests.get(key) req = self.receive_requests.get(key)
@ -1820,7 +1833,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.receive_requests.pop(addr) self.receive_requests.pop(addr)
return True return True
def get_sorted_requests(self): def get_sorted_requests(self) -> List[Invoice]:
""" sorted by timestamp """ """ sorted by timestamp """
out = [self.get_request(x) for x in self.receive_requests.keys()] out = [self.get_request(x) for x in self.receive_requests.keys()]
out = [x for x in out if x is not None] out = [x for x in out if x is not None]

32
electrum/wallet_db.py

@ -33,7 +33,7 @@ import binascii
from . import util, bitcoin from . import util, bitcoin
from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
from .invoices import PR_TYPE_ONCHAIN, invoice_from_json from .invoices import PR_TYPE_ONCHAIN, Invoice
from .keystore import bip44_derivation from .keystore import bip44_derivation
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
from .logging import Logger from .logging import Logger
@ -52,7 +52,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 = 29 # electrum >= 2.7 will set this to prevent FINAL_SEED_VERSION = 30 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format # old versions from overwriting new format
@ -177,6 +177,7 @@ class WalletDB(JsonDB):
self._convert_version_27() self._convert_version_27()
self._convert_version_28() self._convert_version_28()
self._convert_version_29() self._convert_version_29()
self._convert_version_30()
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()
@ -643,6 +644,29 @@ class WalletDB(JsonDB):
d[key] = item d[key] = item
self.data['seed_version'] = 29 self.data['seed_version'] = 29
def _convert_version_30(self):
if not self._is_upgrade_method_needed(29, 29):
return
from .invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN
requests = self.data.get('payment_requests', {})
invoices = self.data.get('invoices', {})
for d in [invoices, requests]:
for key, item in list(d.items()):
_type = item['type']
if _type == PR_TYPE_ONCHAIN:
item['amount_sat'] = item.pop('amount')
elif _type == PR_TYPE_LN:
amount_sat = item.pop('amount')
item['amount_msat'] = 1000 * amount_sat if amount_sat is not None else None
item.pop('exp')
item.pop('message')
item.pop('rhash')
item.pop('time')
else:
raise Exception(f"unknown invoice type: {_type}")
self.data['seed_version'] = 30
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
@ -1127,9 +1151,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.from_json(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.from_json(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