commit 6058829870fde0ef17b2e08a567110ecc381ab94
parent 5f527720cf2ae4c7aef1cfdcf4244dbceb54a5bc
Author: ThomasV <thomasv@electrum.org>
Date: Sun, 31 May 2020 12:49:49 +0200
Use attr.s classes for invoices and requests:
- storage upgrade
- fixes #6192
- add can_pay_invoice, can_receive_invoice to lnworker
Diffstat:
20 files changed, 489 insertions(+), 370 deletions(-)
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -47,7 +47,7 @@ from .bip32 import BIP32Node
from .i18n import _
from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
tx_from_any, PartialTxInput, TxOutpoint)
-from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
+from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .synchronizer import Notifier
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet
from .address_synchronizer import TX_HEIGHT_LOCAL
@@ -59,7 +59,7 @@ from .lnpeer import channel_id_from_funding_tx
from .plugin import run_hook
from .version import ELECTRUM_VERSION
from .simple_config import SimpleConfig
-from .lnaddr import parse_lightning_invoice
+from .invoices import LNInvoice
if TYPE_CHECKING:
@@ -761,19 +761,13 @@ class Commands:
decrypted = wallet.decrypt_message(pubkey, encrypted, password)
return decrypted.decode('utf-8')
- def _format_request(self, out):
- from .util import get_request_status
- out['amount_BTC'] = format_satoshis(out.get('amount'))
- out['status'], out['status_str'] = get_request_status(out)
- return out
-
@command('w')
async def getrequest(self, key, wallet: Abstract_Wallet = None):
"""Return a payment request"""
r = wallet.get_request(key)
if not r:
raise Exception("Request not found")
- return self._format_request(r)
+ return wallet.export_request(r)
#@command('w')
#async def ackrequest(self, serialized):
@@ -783,8 +777,6 @@ class Commands:
@command('w')
async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
"""List the payment requests you made."""
- out = wallet.get_sorted_requests()
- out = list(map(self._format_request, out))
if pending:
f = PR_UNPAID
elif expired:
@@ -793,9 +785,10 @@ class Commands:
f = PR_PAID
else:
f = None
+ out = wallet.get_sorted_requests()
if f is not None:
- out = list(filter(lambda x: x.get('status')==f, out))
- return out
+ out = list(filter(lambda x: x.status==f, out))
+ return [wallet.export_request(x) for x in out]
@command('w')
async def createnewaddress(self, wallet: Abstract_Wallet = None):
@@ -847,14 +840,13 @@ class Commands:
expiration = int(expiration) if expiration else None
req = wallet.make_payment_request(addr, amount, memo, expiration)
wallet.add_payment_request(req)
- out = wallet.get_request(addr)
- return self._format_request(out)
+ return wallet.export_request(req)
@command('wn')
async def add_lightning_request(self, amount, memo='', expiration=3600, wallet: Abstract_Wallet = None):
amount_sat = int(satoshis(amount))
key = await wallet.lnworker._add_request_coro(amount_sat, memo, expiration)
- return wallet.get_request(key)
+ return wallet.get_formatted_request(key)
@command('w')
async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
@@ -996,14 +988,24 @@ class Commands:
@command('')
async def decode_invoice(self, invoice):
- return parse_lightning_invoice(invoice)
+ from .lnaddr import lndecode
+ lnaddr = lndecode(invoice)
+ 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')
async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None):
lnworker = wallet.lnworker
lnaddr = lnworker._check_invoice(invoice, None)
payment_hash = lnaddr.paymenthash
- wallet.save_invoice(parse_lightning_invoice(invoice))
+ wallet.save_invoice(LNInvoice.from_bech32(invoice))
success, log = await lnworker._pay(invoice, attempts=attempts)
return {
'payment_hash': payment_hash.hex(),
@@ -1061,7 +1063,8 @@ class Commands:
@command('w')
async def list_invoices(self, wallet: Abstract_Wallet = None):
- return wallet.get_invoices()
+ l = wallet.get_invoices()
+ return [wallet.export_invoice(x) for x in l]
@command('wn')
async def close_channel(self, channel_point, force=False, wallet: Abstract_Wallet = None):
diff --git a/electrum/daemon.py b/electrum/daemon.py
@@ -46,7 +46,7 @@ from aiorpcx import TaskGroup
from . import util
from .network import Network
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare)
-from .util import PR_PAID, PR_EXPIRED, get_request_status
+from .invoices import PR_PAID, PR_EXPIRED
from .util import log_exceptions, ignore_exceptions, randrange
from .wallet import Wallet, Abstract_Wallet
from .storage import WalletStorage
@@ -344,13 +344,13 @@ class PayServer(Logger):
async def get_request(self, r):
key = r.query_string
- request = self.wallet.get_request(key)
+ request = self.wallet.get_formatted_request(key)
return web.json_response(request)
async def get_bip70_request(self, r):
from .paymentrequest import make_request
key = r.match_info['key']
- request = self.wallet.get_request(key)
+ request = self.wallet.get_formatted_request(key)
if not request:
return web.HTTPNotFound()
pr = make_request(self.config, request)
@@ -360,7 +360,7 @@ class PayServer(Logger):
ws = web.WebSocketResponse()
await ws.prepare(request)
key = request.query_string
- info = self.wallet.get_request(key)
+ info = self.wallet.get_formatted_request(key)
if not info:
await ws.send_str('unknown invoice')
await ws.close()
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
@@ -16,7 +16,8 @@ from electrum.plugin import run_hook
from electrum import util
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
format_satoshis, format_satoshis_plain, format_fee_satoshis,
- PR_PAID, PR_FAILED, maybe_extract_bolt11_invoice)
+ maybe_extract_bolt11_invoice)
+from electrum.invoices import PR_PAID, PR_FAILED
from electrum import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr
@@ -242,7 +243,7 @@ class ElectrumWindow(App):
req = self.wallet.get_invoice(key)
if req is None:
return
- status = req['status']
+ status = self.wallet.get_invoice_status(req)
# todo: update single item
self.update_tab('send')
if self.invoice_popup and self.invoice_popup.key == key:
@@ -393,7 +394,7 @@ class ElectrumWindow(App):
if pr.verify(self.wallet.contacts):
key = pr.get_id()
invoice = self.wallet.get_invoice(key) # FIXME wrong key...
- if invoice and invoice['status'] == PR_PAID:
+ if invoice and self.wallet.get_invoice_status(invoice) == PR_PAID:
self.show_error("invoice already paid")
self.send_screen.do_clear()
elif pr.has_expired():
@@ -451,9 +452,7 @@ class ElectrumWindow(App):
def show_request(self, is_lightning, key):
from .uix.dialogs.request_dialog import RequestDialog
- request = self.wallet.get_request(key)
- data = request['invoice'] if is_lightning else request['URI']
- self.request_popup = RequestDialog('Request', data, key, is_lightning=is_lightning)
+ self.request_popup = RequestDialog('Request', key)
self.request_popup.open()
def show_invoice(self, is_lightning, key):
@@ -461,7 +460,7 @@ class ElectrumWindow(App):
invoice = self.wallet.get_invoice(key)
if not invoice:
return
- data = invoice['invoice'] if is_lightning else key
+ data = invoice.invoice if is_lightning else key
self.invoice_popup = InvoiceDialog('Invoice', data, key)
self.invoice_popup.open()
diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py
@@ -7,8 +7,8 @@ from kivy.app import App
from kivy.clock import Clock
from electrum.gui.kivy.i18n import _
-from electrum.util import pr_tooltips, pr_color, get_request_status
-from electrum.util import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
+from electrum.invoices import pr_tooltips, pr_color
+from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
if TYPE_CHECKING:
from electrum.gui.kivy.main_window import ElectrumWindow
@@ -92,16 +92,17 @@ class InvoiceDialog(Factory.Popup):
self.title = title
self.data = data
self.key = key
- r = self.app.wallet.get_invoice(key)
- self.amount = r.get('amount')
- self.description = r.get('message') or r.get('memo','')
- self.is_lightning = r.get('type') == PR_TYPE_LN
+ invoice = self.app.wallet.get_invoice(key)
+ self.amount = invoice.amount
+ self.description = invoice.message
+ self.is_lightning = invoice.type == PR_TYPE_LN
self.update_status()
self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else []
def update_status(self):
- req = self.app.wallet.get_invoice(self.key)
- self.status, self.status_str = get_request_status(req)
+ invoice = self.app.wallet.get_invoice(self.key)
+ self.status = self.app.wallet.get_invoice_status(invoice)
+ self.status_str = invoice.get_status_str(self.status)
self.status_color = pr_color[self.status]
self.can_pay = self.status in [PR_UNPAID, PR_FAILED]
if self.can_pay and self.is_lightning and self.app.wallet.lnworker:
diff --git a/electrum/gui/kivy/uix/dialogs/request_dialog.py b/electrum/gui/kivy/uix/dialogs/request_dialog.py
@@ -7,8 +7,8 @@ from kivy.app import App
from kivy.clock import Clock
from electrum.gui.kivy.i18n import _
-from electrum.util import pr_tooltips, pr_color, get_request_status
-from electrum.util import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
+from electrum.invoices import pr_tooltips, pr_color
+from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
if TYPE_CHECKING:
from ...main_window import ElectrumWindow
@@ -86,17 +86,17 @@ Builder.load_string('''
class RequestDialog(Factory.Popup):
- def __init__(self, title, data, key, *, is_lightning=False):
+ def __init__(self, title, key):
self.status = PR_UNKNOWN
Factory.Popup.__init__(self)
self.app = App.get_running_app() # type: ElectrumWindow
self.title = title
- self.data = data
self.key = key
r = self.app.wallet.get_request(key)
- self.amount = r.get('amount')
- self.description = r.get('message', '')
- self.is_lightning = r.get('type') == PR_TYPE_LN
+ self.is_lightning = r.is_lightning()
+ self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r)
+ self.amount = r.amount
+ self.description = r.message
self.update_status()
def on_open(self):
@@ -109,7 +109,8 @@ class RequestDialog(Factory.Popup):
def update_status(self):
req = self.app.wallet.get_request(self.key)
- self.status, self.status_str = get_request_status(req)
+ self.status = self.app.wallet.get_request_status(self.key)
+ self.status_str = req.get_status_str(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.amount and self.amount > self.app.wallet.lnworker.num_sats_can_receive():
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
@@ -24,17 +24,17 @@ from kivy.utils import platform
from kivy.logger import Logger
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
-from electrum.util 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,
+ LNInvoice, pr_expiration_values)
from electrum import bitcoin, constants
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
-from electrum.util import (parse_URI, InvalidBitcoinURI, PR_PAID, PR_UNKNOWN, PR_EXPIRED,
- PR_INFLIGHT, TxMinedInfo, get_request_status, pr_expiration_values,
- maybe_extract_bolt11_invoice)
+from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice
from electrum.plugin import run_hook
from electrum.wallet import InternalAddressCorruption
from electrum import simple_config
from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING
-from electrum.lnaddr import lndecode, parse_lightning_invoice
+from electrum.lnaddr import lndecode
from electrum.lnutil import RECEIVED, SENT, PaymentFailure
from .dialogs.question import Question
@@ -225,26 +225,27 @@ class SendScreen(CScreen):
self.app.show_invoice(obj.is_lightning, obj.key)
def get_card(self, item):
- invoice_type = item['type']
- status, status_str = get_request_status(item) # convert to str
- if invoice_type == PR_TYPE_LN:
- key = item['rhash']
+ status = self.app.wallet.get_invoice_status(item)
+ status_str = item.get_status_str(status)
+ is_lightning = item.type == PR_TYPE_LN
+ if is_lightning:
+ key = item.rhash
log = self.app.wallet.lnworker.logs.get(key)
- if item['status'] == PR_INFLIGHT and log:
+ if status == PR_INFLIGHT and log:
status_str += '... (%d)'%len(log)
- elif invoice_type == PR_TYPE_ONCHAIN:
- key = item['id']
+ is_bip70 = False
else:
- raise Exception('unknown invoice type')
+ key = item.id
+ is_bip70 = bool(item.bip70)
return {
- 'is_lightning': invoice_type == PR_TYPE_LN,
- 'is_bip70': 'bip70' in item,
+ 'is_lightning': is_lightning,
+ 'is_bip70': is_bip70,
'screen': self,
'status': status,
'status_str': status_str,
'key': key,
- 'memo': item['message'],
- 'amount': self.app.format_amount_and_units(item['amount'] or 0),
+ 'memo': item.message,
+ 'amount': self.app.format_amount_and_units(item.amount or 0),
}
def do_clear(self):
@@ -300,7 +301,7 @@ class SendScreen(CScreen):
return
message = self.message
if self.is_lightning:
- return parse_lightning_invoice(address)
+ return LNInvoice.from_bech32(address)
else: # on-chain
if self.payment_request:
outputs = self.payment_request.get_outputs()
@@ -329,26 +330,27 @@ class SendScreen(CScreen):
self.do_pay_invoice(invoice)
def do_pay_invoice(self, invoice):
- if invoice['type'] == PR_TYPE_LN:
+ if invoice.is_lightning():
self._do_pay_lightning(invoice)
return
- elif invoice['type'] == PR_TYPE_ONCHAIN:
+ else:
do_pay = lambda rbf: self._do_pay_onchain(invoice, rbf)
if self.app.electrum_config.get('use_rbf'):
d = Question(_('Should this transaction be replaceable?'), do_pay)
d.open()
else:
do_pay(False)
- else:
- raise Exception('unknown invoice type')
def _do_pay_lightning(self, invoice):
attempts = 10
- threading.Thread(target=self.app.wallet.lnworker.pay, args=(invoice['invoice'], invoice['amount'], attempts)).start()
+ threading.Thread(
+ target=self.app.wallet.lnworker.pay,
+ args=(invoice.invoice, invoice.amount),
+ kwargs={'attempts':10}).start()
def _do_pay_onchain(self, invoice, rbf):
# make unsigned transaction
- outputs = invoice['outputs'] # type: List[PartialTxOutput]
+ outputs = invoice.outputs # type: List[PartialTxOutput]
coins = self.app.wallet.get_spendable_coins(None)
try:
tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
@@ -405,7 +407,7 @@ class SendScreen(CScreen):
def callback(c):
if c:
for req in invoices:
- key = req['key']
+ key = req.rhash if req.is_lightning() else req.get_address()
self.app.wallet.delete_invoice(key)
self.update()
n = len(invoices)
@@ -477,16 +479,17 @@ class ReceiveScreen(CScreen):
self.app.show_request(lightning, key)
def get_card(self, req):
- is_lightning = req.get('type') == PR_TYPE_LN
+ is_lightning = req.is_lightning()
if not is_lightning:
- address = req['address']
+ address = req.get_address()
key = address
else:
- key = req['rhash']
- address = req['invoice']
- amount = req.get('amount')
- description = req.get('message') or req.get('memo', '') # TODO: a db upgrade would be needed to simplify that.
- status, status_str = get_request_status(req)
+ key = req.rhash
+ address = req.invoice
+ amount = req.amount
+ description = req.message
+ status = self.app.wallet.get_request_status(key)
+ status_str = req.get_status_str(status)
ci = {}
ci['screen'] = self
ci['address'] = address
diff --git a/electrum/gui/kivy/uix/ui_screens/receive.kv b/electrum/gui/kivy/uix/ui_screens/receive.kv
@@ -1,6 +1,6 @@
#:import _ electrum.gui.kivy.i18n._
-#:import pr_color electrum.util.pr_color
-#:import PR_UNKNOWN electrum.util.PR_UNKNOWN
+#:import pr_color electrum.invoices.pr_color
+#:import PR_UNKNOWN electrum.invoices.PR_UNKNOWN
#:import Factory kivy.factory.Factory
#:import Decimal decimal.Decimal
#:set btc_symbol chr(171)
diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
@@ -1,6 +1,6 @@
#:import _ electrum.gui.kivy.i18n._
-#:import pr_color electrum.util.pr_color
-#:import PR_UNKNOWN electrum.util.PR_UNKNOWN
+#:import pr_color electrum.invoices.pr_color
+#:import PR_UNKNOWN electrum.invoices.PR_UNKNOWN
#:import Factory kivy.factory.Factory
#:import Decimal decimal.Decimal
#:set btc_symbol chr(171)
diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
@@ -32,9 +32,8 @@ from PyQt5.QtWidgets import QAbstractItemView
from PyQt5.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QHeaderView
from electrum.i18n import _
-from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED
-from electrum.util import get_request_status
-from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
+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.lnutil import PaymentAttemptLog
from .util import (MyTreeView, read_QIcon, MySortModel,
@@ -77,7 +76,7 @@ class InvoiceList(MyTreeView):
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.update()
- def update_item(self, key, req):
+ def update_item(self, key, invoice: Invoice):
model = self.std_model
for row in range(0, model.rowCount()):
item = model.item(row, 0)
@@ -86,7 +85,8 @@ class InvoiceList(MyTreeView):
else:
return
status_item = model.item(row, self.Columns.STATUS)
- status, status_str = get_request_status(req)
+ status = self.parent.wallet.get_invoice_status(invoice)
+ status_str = invoice.get_status_str(status)
if self.parent.wallet.lnworker:
log = self.parent.wallet.lnworker.logs.get(key)
if log and status == PR_INFLIGHT:
@@ -100,21 +100,21 @@ class InvoiceList(MyTreeView):
self.std_model.clear()
self.update_headers(self.__class__.headers)
for idx, item in enumerate(self.parent.wallet.get_invoices()):
- invoice_type = item['type']
- if invoice_type == PR_TYPE_LN:
- key = item['rhash']
+ if item.type == PR_TYPE_LN:
+ key = item.rhash
icon_name = 'lightning.png'
- elif invoice_type == PR_TYPE_ONCHAIN:
- key = item['id']
+ elif item.type == PR_TYPE_ONCHAIN:
+ key = item.id
icon_name = 'bitcoin.png'
- if item.get('bip70'):
+ if item.bip70:
icon_name = 'seal.png'
else:
raise Exception('Unsupported type')
- status, status_str = get_request_status(item)
- message = item['message']
- amount = item['amount']
- timestamp = item.get('time', 0)
+ status = self.parent.wallet.get_invoice_status(item)
+ status_str = item.get_status_str(status)
+ message = item.message
+ amount = item.amount
+ timestamp = item.time or 0
date_str = format_time(timestamp) if timestamp else _('Unknown')
amount_str = self.parent.format_amount(amount, whitespaces=True)
labels = [date_str, message, amount_str, status_str]
@@ -123,7 +123,7 @@ class InvoiceList(MyTreeView):
items[self.Columns.DATE].setIcon(read_QIcon(icon_name))
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(invoice_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)
self.std_model.insertRow(idx, items)
self.filter()
@@ -143,11 +143,12 @@ class InvoiceList(MyTreeView):
export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
def create_menu(self, position):
+ wallet = self.parent.wallet
items = self.selected_in_column(0)
if len(items)>1:
keys = [ item.data(ROLE_REQUEST_ID) for item in items]
- invoices = [ self.parent.wallet.get_invoice(key) for key in keys]
- can_batch_pay = all([ invoice['status'] == PR_UNPAID and invoice['type'] == PR_TYPE_ONCHAIN for invoice in invoices])
+ 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])
menu = QMenu(self)
if can_batch_pay:
menu.addAction(_("Batch pay invoices"), lambda: self.parent.pay_multiple_invoices(invoices))
@@ -164,9 +165,10 @@ class InvoiceList(MyTreeView):
self.add_copy_menu(menu, idx)
invoice = self.parent.wallet.get_invoice(key)
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
- if invoice['status'] == PR_UNPAID:
+ status = wallet.get_invoice_status(invoice)
+ if status == PR_UNPAID:
menu.addAction(_("Pay"), lambda: self.parent.do_pay_invoice(invoice))
- if invoice['status'] == PR_FAILED:
+ if status == PR_FAILED:
menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice))
if self.parent.wallet.lnworker:
log = self.parent.wallet.lnworker.logs.get(key)
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -62,7 +62,8 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
get_new_wallet_name, send_exception_to_crash_reporter,
InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs)
-from electrum.util 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
+from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice
from electrum.transaction import (Transaction, PartialTxInput,
PartialTransaction, PartialTxOutput)
from electrum.address_synchronizer import AddTransactionException
@@ -73,10 +74,7 @@ from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed,
from electrum.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig
from electrum.logging import Logger
-from electrum.util import PR_PAID, PR_FAILED
-from electrum.util import pr_expiration_values
from electrum.lnutil import ln_dummy_address
-from electrum.lnaddr import parse_lightning_invoice
from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit
@@ -1192,7 +1190,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.receive_message_e.setText('')
# copy to clipboard
r = self.wallet.get_request(key)
- content = r.get('invoice', '') if is_lightning else r.get('address', '')
+ content = r.invoice if r.is_lightning() else r.get_address()
title = _('Invoice') if is_lightning else _('Address')
self.do_copy(content, title=title)
@@ -1231,7 +1229,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def export_payment_request(self, addr):
r = self.wallet.receive_requests.get(addr)
pr = paymentrequest.serialize_request(r).SerializeToString()
- name = r['id'] + '.bip70'
+ name = r.id + '.bip70'
fileName = self.getSaveFileName(_("Select where to save your payment request"), name, "*.bip70")
if fileName:
with open(fileName, "wb+") as f:
@@ -1505,21 +1503,21 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if self.check_send_tab_payto_line_and_show_errors():
return
if not self._is_onchain:
- invoice = self.payto_e.lightning_invoice
- if not invoice:
+ invoice_str = self.payto_e.lightning_invoice
+ if not invoice_str:
return
if not self.wallet.lnworker:
self.show_error(_('Lightning is disabled'))
return
- invoice_dict = parse_lightning_invoice(invoice)
- if invoice_dict.get('amount') is None:
+ invoice = LNInvoice.from_bech32(invoice_str)
+ if invoice.amount is None:
amount = self.amount_e.get_amount()
if amount:
- invoice_dict['amount'] = amount
+ invoice.amount = amount
else:
self.show_error(_('No amount'))
return
- return invoice_dict
+ return invoice
else:
outputs = self.read_outputs()
if self.check_send_tab_onchain_outputs_and_show_errors(outputs):
@@ -1547,15 +1545,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def pay_multiple_invoices(self, invoices):
outputs = []
for invoice in invoices:
- outputs += invoice['outputs']
+ outputs += invoice.outputs
self.pay_onchain_dialog(self.get_coins(), outputs)
def do_pay_invoice(self, invoice):
- if invoice['type'] == PR_TYPE_LN:
- self.pay_lightning_invoice(invoice['invoice'], invoice['amount'])
- elif invoice['type'] == PR_TYPE_ONCHAIN:
- outputs = invoice['outputs']
- self.pay_onchain_dialog(self.get_coins(), outputs)
+ if invoice.type == PR_TYPE_LN:
+ self.pay_lightning_invoice(invoice.invoice, invoice.amount)
+ elif invoice.type == PR_TYPE_ONCHAIN:
+ self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
else:
raise Exception('unknown invoice type')
@@ -1775,7 +1772,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return
key = pr.get_id()
invoice = self.wallet.get_invoice(key)
- if invoice and invoice['status'] == PR_PAID:
+ if invoice and self.wallet.get_invoice_status() == PR_PAID:
self.show_message("invoice already paid")
self.do_clear()
self.payment_request = None
@@ -1970,7 +1967,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if invoice is None:
self.show_error('Cannot find payment request in wallet.')
return
- bip70 = invoice.get('bip70')
+ bip70 = invoice.bip70
if bip70:
pr = paymentrequest.PaymentRequest(bytes.fromhex(bip70))
pr.verify(self.contacts)
diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
@@ -31,8 +31,8 @@ from PyQt5.QtWidgets import QMenu, QAbstractItemView
from PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex
from electrum.i18n import _
-from electrum.util import format_time, get_request_status
-from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
+from electrum.util import format_time
+from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.plugin import run_hook
from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
@@ -90,18 +90,17 @@ class RequestList(MyTreeView):
return
# TODO use siblingAtColumn when min Qt version is >=5.11
item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))
- request_type = item.data(ROLE_REQUEST_TYPE)
key = item.data(ROLE_KEY)
req = self.wallet.get_request(key)
if req is None:
self.update()
return
- if request_type == PR_TYPE_LN:
- self.parent.receive_payreq_e.setText(req.get('invoice'))
- self.parent.receive_address_e.setText(req.get('invoice'))
+ if req.is_lightning():
+ self.parent.receive_payreq_e.setText(req.invoice)
+ self.parent.receive_address_e.setText(req.invoice)
else:
- self.parent.receive_payreq_e.setText(req.get('URI'))
- self.parent.receive_address_e.setText(req['address'])
+ 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_payreq_e.repaint() # macOS hack (similar to #4777)
self.parent.receive_address_e.repaint() # macOS hack (similar to #4777)
@@ -119,7 +118,8 @@ class RequestList(MyTreeView):
key = date_item.data(ROLE_KEY)
req = self.wallet.get_request(key)
if req:
- status, status_str = get_request_status(req)
+ status = self.parent.wallet.get_request_status(key)
+ status_str = req.get_status_str(status)
status_item.setText(status_str)
status_item.setIcon(read_QIcon(pr_icons.get(status)))
@@ -130,20 +130,22 @@ class RequestList(MyTreeView):
self.std_model.clear()
self.update_headers(self.__class__.headers)
for req in self.wallet.get_sorted_requests():
- status, status_str = get_request_status(req)
- request_type = req['type']
- timestamp = req.get('time', 0)
- amount = req.get('amount')
- message = req.get('message') or req.get('memo')
+ key = req.rhash if req.is_lightning() else req.id
+ status = self.parent.wallet.get_request_status(key)
+ status_str = req.get_status_str(status)
+ request_type = req.type
+ timestamp = req.time
+ amount = req.amount
+ message = req.message
date = format_time(timestamp)
amount_str = self.parent.format_amount(amount) if amount else ""
labels = [date, message, amount_str, status_str]
- if request_type == PR_TYPE_LN:
- key = req['rhash']
+ if req.is_lightning():
+ key = req.rhash
icon = read_QIcon("lightning.png")
tooltip = 'lightning request'
- elif request_type == PR_TYPE_ONCHAIN:
- key = req['address']
+ else:
+ key = req.get_address()
icon = read_QIcon("bitcoin.png")
tooltip = 'onchain request'
items = [QStandardItem(e) for e in labels]
@@ -182,20 +184,20 @@ class RequestList(MyTreeView):
if not item:
return
key = item.data(ROLE_KEY)
- request_type = item.data(ROLE_REQUEST_TYPE)
req = self.wallet.get_request(key)
if req is None:
self.update()
return
menu = QMenu(self)
self.add_copy_menu(menu, idx)
- if request_type == PR_TYPE_LN:
- menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req['invoice'], title='Lightning Request'))
+ if req.is_lightning():
+ menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req.invoice, title='Lightning Request'))
else:
- menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req['URI'], title='Bitcoin URI'))
- menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req['address'], title='Bitcoin Address'))
- if 'view_url' in req:
- menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
+ URI = self.wallet.get_request_URI(req)
+ menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(URI, title='Bitcoin URI'))
+ menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address'))
+ #if 'view_url' in req:
+ # menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
menu.addAction(_("Delete"), lambda: self.parent.delete_requests([key]))
run_hook('receive_list_menu', menu, key)
menu.exec_(self.viewport().mapToGlobal(position))
diff --git a/electrum/gui/qt/util.py b/electrum/gui/qt/util.py
@@ -26,7 +26,7 @@ from PyQt5.QtWidgets import (QPushButton, QLabel, QMessageBox, QHBoxLayout,
from electrum.i18n import _, languages
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path
-from electrum.util import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING
+from electrum.invoices import PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING
if TYPE_CHECKING:
from .main_window import ElectrumWindow
diff --git a/electrum/invoices.py b/electrum/invoices.py
@@ -0,0 +1,115 @@
+import attr
+import time
+
+from .json_db import StoredObject
+from .i18n import _
+from .util import age
+from .lnaddr import lndecode
+from . import constants
+from .bitcoin import COIN
+from .transaction import PartialTxOutput
+
+# convention: 'invoices' = outgoing , 'request' = incoming
+
+# types of payment requests
+PR_TYPE_ONCHAIN = 0
+PR_TYPE_LN = 2
+
+# status of payment requests
+PR_UNPAID = 0
+PR_EXPIRED = 1
+PR_UNKNOWN = 2 # sent but not propagated
+PR_PAID = 3 # send and propagated
+PR_INFLIGHT = 4 # unconfirmed
+PR_FAILED = 5
+PR_ROUTING = 6
+
+pr_color = {
+ PR_UNPAID: (.7, .7, .7, 1),
+ PR_PAID: (.2, .9, .2, 1),
+ PR_UNKNOWN: (.7, .7, .7, 1),
+ PR_EXPIRED: (.9, .2, .2, 1),
+ PR_INFLIGHT: (.9, .6, .3, 1),
+ PR_FAILED: (.9, .2, .2, 1),
+ PR_ROUTING: (.9, .6, .3, 1),
+}
+
+pr_tooltips = {
+ PR_UNPAID:_('Pending'),
+ PR_PAID:_('Paid'),
+ PR_UNKNOWN:_('Unknown'),
+ PR_EXPIRED:_('Expired'),
+ PR_INFLIGHT:_('In progress'),
+ PR_FAILED:_('Failed'),
+ PR_ROUTING: _('Computing route...'),
+}
+
+PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60 # 1 day
+pr_expiration_values = {
+ 0: _('Never'),
+ 10*60: _('10 minutes'),
+ 60*60: _('1 hour'),
+ 24*60*60: _('1 day'),
+ 7*24*60*60: _('1 week'),
+}
+assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
+
+outputs_decoder = lambda _list: [PartialTxOutput.from_legacy_tuple(*x) for x in _list]
+
+@attr.s
+class Invoice(StoredObject):
+ type = attr.ib(type=int)
+ message = attr.ib(type=str)
+ amount = attr.ib(type=int)
+ exp = attr.ib(type=int)
+ time = attr.ib(type=int)
+
+ def is_lightning(self):
+ return self.type == PR_TYPE_LN
+
+ def get_status_str(self, status):
+ status_str = pr_tooltips[status]
+ if status == PR_UNPAID:
+ if self.exp > 0:
+ expiration = self.exp + self.time
+ status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
+ else:
+ status_str = _('Pending')
+ return status_str
+
+@attr.s
+class OnchainInvoice(Invoice):
+ id = attr.ib(type=str)
+ outputs = attr.ib(type=list, converter=outputs_decoder)
+ bip70 = attr.ib(type=str) # may be None
+ requestor = attr.ib(type=str) # may be None
+
+ def get_address(self):
+ assert len(self.outputs) == 1
+ return self.outputs[0].address
+
+@attr.s
+class LNInvoice(Invoice):
+ rhash = attr.ib(type=str)
+ invoice = attr.ib(type=str)
+
+ @classmethod
+ def from_bech32(klass, invoice: str):
+ lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
+ amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
+ return LNInvoice(
+ type = PR_TYPE_LN,
+ amount = amount,
+ message = lnaddr.get_description(),
+ time = lnaddr.date,
+ exp = lnaddr.get_expiry(),
+ rhash = lnaddr.paymenthash.hex(),
+ invoice = invoice,
+ )
+
+
+def invoice_from_json(x: dict) -> Invoice:
+ if x.get('type') == PR_TYPE_LN:
+ return LNInvoice(**x)
+ else:
+ return OnchainInvoice(**x)
diff --git a/electrum/lnaddr.py b/electrum/lnaddr.py
@@ -13,7 +13,6 @@ from .bitcoin import hash160_to_b58_address, b58_address_to_hash160
from .segwit_addr import bech32_encode, bech32_decode, CHARSET
from . import constants
from . import ecc
-from .util import PR_TYPE_LN
from .bitcoin import COIN
@@ -470,20 +469,6 @@ def lndecode(invoice: str, *, verbose=False, expected_hrp=None) -> LnAddr:
-def parse_lightning_invoice(invoice):
- lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
- amount = int(lnaddr.amount * COIN) if lnaddr.amount else None
- return {
- 'type': PR_TYPE_LN,
- 'invoice': invoice,
- 'amount': amount,
- 'message': lnaddr.get_description(),
- 'time': lnaddr.date,
- 'exp': lnaddr.get_expiry(),
- 'pubkey': lnaddr.pubkey.serialize().hex(),
- 'rhash': lnaddr.paymenthash.hex(),
- }
-
if __name__ == '__main__':
# run using
# python3 -m electrum.lnaddr <invoice> <expected hrp>
diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
@@ -35,7 +35,8 @@ import attr
from . import ecc
from . import constants, util
-from .util import bfh, bh2u, chunks, TxMinedInfo, PR_PAID
+from .util import bfh, bh2u, chunks, TxMinedInfo
+from .invoices import PR_PAID
from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
from .transaction import Transaction, PartialTransaction, TxInput
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -24,8 +24,8 @@ from aiorpcx import run_in_thread
from . import constants, util
from . import keystore
from .util import profiler
-from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING
-from .util import PR_TYPE_LN, NetworkRetryManager
+from .invoices import PR_TYPE_LN, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LNInvoice
+from .util import NetworkRetryManager
from .lnutil import LN_MAX_FUNDING_SAT
from .keystore import BIP32_KeyStore
from .bitcoin import COIN
@@ -1102,15 +1102,7 @@ class LNWallet(LNWorker):
payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
invoice = lnencode(lnaddr, self.node_keypair.privkey)
key = bh2u(lnaddr.paymenthash)
- req = {
- 'type': PR_TYPE_LN,
- 'amount': amount_sat,
- 'time': lnaddr.date,
- 'exp': expiry,
- 'message': message,
- 'rhash': key,
- 'invoice': invoice
- }
+ req = LNInvoice.from_bech32(invoice)
self.save_preimage(payment_hash, payment_preimage)
self.save_payment_info(info)
self.wallet.add_payment_request(req)
@@ -1145,7 +1137,8 @@ class LNWallet(LNWorker):
info = self.get_payment_info(payment_hash)
return info.status if info else PR_UNPAID
- def get_invoice_status(self, key):
+ def get_invoice_status(self, invoice):
+ key = invoice.rhash
log = self.logs[key]
if key in self.is_routing:
return PR_ROUTING
@@ -1285,6 +1278,12 @@ class LNWallet(LNWorker):
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
+ def can_pay_invoice(self, invoice):
+ return invoice.amount <= self.num_sats_can_send()
+
+ def can_receive_invoice(self, invoice):
+ return invoice.amount <= self.num_sats_can_receive()
+
async def close_channel(self, chan_id):
chan = self._channels[chan_id]
peer = self._peers[chan.node_id]
diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
@@ -41,7 +41,7 @@ except ImportError:
from . import bitcoin, ecc, util, transaction, x509, rsakey
from .util import bh2u, bfh, export_meta, import_meta, make_aiohttp_session
-from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
+from .invoices import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
from .crypto import sha256
from .bitcoin import address_to_script
from .transaction import PartialTxOutput
diff --git a/electrum/util.py b/electrum/util.py
@@ -43,6 +43,7 @@ from typing import NamedTuple, Optional
import ssl
import ipaddress
import random
+import attr
import aiohttp
from aiohttp_socks import ProxyConnector, ProxyType
@@ -77,66 +78,6 @@ base_units_list = ['BTC', 'mBTC', 'bits', 'sat'] # list(dict) does not guarante
DECIMAL_POINT_DEFAULT = 5 # mBTC
-# types of payment requests
-PR_TYPE_ONCHAIN = 0
-PR_TYPE_LN = 2
-
-# status of payment requests
-PR_UNPAID = 0
-PR_EXPIRED = 1
-PR_UNKNOWN = 2 # sent but not propagated
-PR_PAID = 3 # send and propagated
-PR_INFLIGHT = 4 # unconfirmed
-PR_FAILED = 5
-PR_ROUTING = 6
-
-pr_color = {
- PR_UNPAID: (.7, .7, .7, 1),
- PR_PAID: (.2, .9, .2, 1),
- PR_UNKNOWN: (.7, .7, .7, 1),
- PR_EXPIRED: (.9, .2, .2, 1),
- PR_INFLIGHT: (.9, .6, .3, 1),
- PR_FAILED: (.9, .2, .2, 1),
- PR_ROUTING: (.9, .6, .3, 1),
-}
-
-pr_tooltips = {
- PR_UNPAID:_('Pending'),
- PR_PAID:_('Paid'),
- PR_UNKNOWN:_('Unknown'),
- PR_EXPIRED:_('Expired'),
- PR_INFLIGHT:_('In progress'),
- PR_FAILED:_('Failed'),
- PR_ROUTING: _('Computing route...'),
-}
-
-PR_DEFAULT_EXPIRATION_WHEN_CREATING = 24*60*60 # 1 day
-pr_expiration_values = {
- 0: _('Never'),
- 10*60: _('10 minutes'),
- 60*60: _('1 hour'),
- 24*60*60: _('1 day'),
- 7*24*60*60: _('1 week'),
-}
-assert PR_DEFAULT_EXPIRATION_WHEN_CREATING in pr_expiration_values
-
-
-def get_request_status(req):
- status = req['status']
- exp = req.get('exp', 0) or 0
- if req.get('type') == PR_TYPE_LN and exp == 0:
- status = PR_EXPIRED # for BOLT-11 invoices, exp==0 means 0 seconds
- if req['status'] == PR_UNPAID and exp > 0 and req['time'] + req['exp'] < time.time():
- status = PR_EXPIRED
- status_str = pr_tooltips[status]
- if status == PR_UNPAID:
- if exp > 0:
- expiration = exp + req['time']
- status_str = _('Expires') + ' ' + age(expiration, include_seconds=True)
- else:
- status_str = _('Pending')
- return status, status_str
-
class UnknownBaseUnit(Exception): pass
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -52,10 +52,10 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
-from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN, get_backup_dir
+from .util import get_backup_dir
from .simple_config import SimpleConfig
-from .bitcoin import (COIN, is_address, address_to_script,
- is_minikey, relayfee, dust_threshold)
+from .bitcoin import COIN, TYPE_ADDRESS
+from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
from .crypto import sha256d
from . import keystore
from .keystore import load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric
@@ -68,7 +68,8 @@ from .transaction import (Transaction, TxInput, UnknownTxinType, TxOutput,
from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
-from .util import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT
+from .invoices import Invoice, OnchainInvoice, invoice_from_json
+from .invoices import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT, PR_TYPE_ONCHAIN, PR_TYPE_LN
from .contacts import Contacts
from .interface import NetworkException
from .mnemonic import Mnemonic
@@ -660,39 +661,43 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
amount = '!'
else:
amount = sum(x.value for x in outputs)
- invoice = {
- 'type': PR_TYPE_ONCHAIN,
- 'message': message,
- 'outputs': outputs,
- 'amount': amount,
- }
+ outputs = [x.to_legacy_tuple() for x in outputs]
if pr:
- invoice['bip70'] = pr.raw.hex()
- invoice['time'] = pr.get_time()
- invoice['exp'] = pr.get_expiration_date() - pr.get_time()
- invoice['requestor'] = pr.get_requestor()
- invoice['message'] = pr.get_memo()
- elif URI:
- timestamp = URI.get('time')
- if timestamp: invoice['time'] = timestamp
- exp = URI.get('exp')
- if exp: invoice['exp'] = exp
- if 'time' not in invoice:
- invoice['time'] = int(time.time())
+ invoice = OnchainInvoice(
+ type = PR_TYPE_ONCHAIN,
+ amount = amount,
+ outputs = 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() if pr else None,
+ requestor = pr.get_requestor(),
+ )
+ else:
+ invoice = OnchainInvoice(
+ type = PR_TYPE_ONCHAIN,
+ amount = amount,
+ outputs = outputs,
+ message = message,
+ id = bh2u(sha256(repr(outputs))[0:16]),
+ time = URI.get('time') if URI else int(time.time()),
+ exp = URI.get('exp') if URI else 0,
+ bip70 = None,
+ requestor = None,
+ )
return invoice
- def save_invoice(self, invoice):
- invoice_type = invoice['type']
+ def save_invoice(self, invoice: Invoice):
+ invoice_type = invoice.type
if invoice_type == PR_TYPE_LN:
- key = invoice['rhash']
+ key = invoice.rhash
elif invoice_type == PR_TYPE_ONCHAIN:
+ key = invoice.id
if self.is_onchain_invoice_paid(invoice):
self.logger.info("saving invoice... but it is already paid!")
- key = bh2u(sha256(repr(invoice))[0:16])
- invoice['id'] = key
- outputs = invoice['outputs'] # type: List[PartialTxOutput]
with self.transaction_lock:
- for txout in outputs:
+ for txout in invoice.outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
else:
raise Exception('Unsupported invoice type')
@@ -704,26 +709,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.save_db()
def get_invoices(self):
- out = [self.get_invoice(key) for key in self.invoices.keys()]
- out = list(filter(None, out))
- out.sort(key=operator.itemgetter('time'))
+ out = list(self.invoices.values())
+ #out = list(filter(None, out)) filter out ln
+ out.sort(key=lambda x:x.time)
return out
def get_invoice(self, key):
- if key not in self.invoices:
- return
- # convert StoredDict to dict
- item = dict(self.invoices[key])
- request_type = item.get('type')
- if request_type == PR_TYPE_ONCHAIN:
- item['status'] = PR_PAID if self.is_onchain_invoice_paid(item) else PR_UNPAID
- elif self.lnworker and request_type == PR_TYPE_LN:
- item['status'] = self.lnworker.get_invoice_status(key)
- else:
- return
- # unique handle
- item['key'] = key
- return item
+ return self.invoices.get(key)
def _get_relevant_invoice_keys_for_tx(self, tx: Transaction) -> Set[str]:
relevant_invoice_keys = set()
@@ -736,16 +728,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# scriptpubkey -> list(invoice_keys)
self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]]
for invoice_key, invoice in self.invoices.items():
- if invoice.get('type') == PR_TYPE_ONCHAIN:
- outputs = invoice['outputs'] # type: List[PartialTxOutput]
- for txout in outputs:
+ if invoice.type == PR_TYPE_ONCHAIN:
+ for txout in invoice.outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
- def _is_onchain_invoice_paid(self, invoice: dict) -> Tuple[bool, Sequence[str]]:
+ def _is_onchain_invoice_paid(self, invoice: Invoice) -> Tuple[bool, Sequence[str]]:
"""Returns whether on-chain invoice is satisfied, and list of relevant TXIDs."""
- assert invoice.get('type') == PR_TYPE_ONCHAIN
+ assert invoice.type == PR_TYPE_ONCHAIN
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 txo.value == '!' else txo.value
relevant_txs = []
with self.transaction_lock:
@@ -762,7 +753,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return False, []
return True, relevant_txs
- def is_onchain_invoice_paid(self, invoice: dict) -> bool:
+ def is_onchain_invoice_paid(self, invoice: Invoice) -> bool:
return self._is_onchain_invoice_paid(invoice)[0]
def _maybe_set_tx_label_based_on_invoices(self, tx: Transaction) -> bool:
@@ -1550,7 +1541,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
def get_unused_addresses(self) -> Sequence[str]:
domain = self.get_receiving_addresses()
- in_use_by_request = [k for k in self.receive_requests.keys() if self.get_request_status(k)[0] != PR_EXPIRED]
+ in_use_by_request = [k for k in self.receive_requests.keys() if self.get_request_status(k) != PR_EXPIRED] # we should index receive_requests by id
return [addr for addr in domain if not self.is_used(addr)
and addr not in in_use_by_request]
@@ -1608,60 +1599,84 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return True, conf
return False, None
- def get_request_URI(self, addr):
- req = self.receive_requests[addr]
+ def get_request_URI(self, req: Invoice):
+ addr = req.get_address()
message = self.labels.get(addr, '')
- amount = req['amount']
+ amount = req.amount
extra_query_params = {}
- if req.get('time'):
- extra_query_params['time'] = str(int(req.get('time')))
- if req.get('exp'):
- extra_query_params['exp'] = str(int(req.get('exp')))
- if req.get('name') and req.get('sig'):
- sig = bfh(req.get('sig'))
- sig = bitcoin.base_encode(sig, base=58)
- extra_query_params['name'] = req['name']
- extra_query_params['sig'] = sig
+ if req.time:
+ extra_query_params['time'] = str(int(req.time))
+ if req.exp:
+ extra_query_params['exp'] = str(int(req.exp))
+ #if req.get('name') and req.get('sig'):
+ # sig = bfh(req.get('sig'))
+ # sig = bitcoin.base_encode(sig, base=58)
+ # extra_query_params['name'] = req['name']
+ # extra_query_params['sig'] = sig
uri = create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params)
return str(uri)
- def get_request_status(self, address):
- r = self.receive_requests.get(address)
+ def check_expired_status(self, r, status):
+ if r.is_lightning() and r.exp == 0:
+ status = PR_EXPIRED # for BOLT-11 invoices, exp==0 means 0 seconds
+ if status == PR_UNPAID and r.exp > 0 and r.time + r.exp < time.time():
+ status = PR_EXPIRED
+ return status
+
+ def get_invoice_status(self, invoice):
+ if invoice.is_lightning():
+ status = self.lnworker.get_invoice_status(invoice)
+ else:
+ status = PR_PAID if self.is_onchain_invoice_paid(invoice) else PR_UNPAID
+ return self.check_expired_status(invoice, status)
+
+ def get_request_status(self, key):
+ r = self.get_request(key)
if r is None:
return PR_UNKNOWN
- amount = r.get('amount', 0) or 0
- timestamp = r.get('time', 0)
- if timestamp and type(timestamp) != int:
- timestamp = 0
- exp = r.get('exp', 0) or 0
- paid, conf = self.get_payment_status(address, amount)
- if not paid:
- if exp > 0 and time.time() > timestamp + exp:
- status = PR_EXPIRED
- else:
- status = PR_UNPAID
+ if r.is_lightning():
+ status = self.lnworker.get_payment_status(bfh(r.rhash))
else:
- status = PR_PAID
- return status, conf
+ paid, conf = self.get_payment_status(r.get_address(), r.amount)
+ status = PR_PAID if paid else PR_UNPAID
+ return self.check_expired_status(r, status)
def get_request(self, key):
- req = self.receive_requests.get(key)
- if not req:
- return
- # convert StoredDict to dict
- req = dict(req)
- _type = req.get('type')
- if _type == PR_TYPE_ONCHAIN:
- addr = req['address']
- req['URI'] = self.get_request_URI(addr)
- status, conf = self.get_request_status(addr)
- req['status'] = status
- if conf is not None:
- req['confirmations'] = conf
- elif self.lnworker and _type == PR_TYPE_LN:
- req['status'] = self.lnworker.get_payment_status(bfh(key))
+ return self.receive_requests.get(key)
+
+ def get_formatted_request(self, key):
+ x = self.receive_requests.get(key)
+ if x:
+ return self.export_request(x)
+
+ def export_request(self, x):
+ key = x.rhash if x.is_lightning() else x.get_address()
+ status = self.get_request_status(key)
+ status_str = x.get_status_str(status)
+ is_lightning = x.is_lightning()
+ d = {
+ 'is_lightning': is_lightning,
+ 'amount': x.amount,
+ 'amount_BTC': format_satoshis(x.amount),
+ 'message': x.message,
+ 'timestamp': x.time,
+ 'expiration': x.exp,
+ 'status': status,
+ 'status_str': status_str,
+ }
+ if is_lightning:
+ d['rhash'] = x.rhash
+ d['invoice'] = x.invoice
+ if self.lnworker and status == PR_UNPAID:
+ d['can_receive'] = self.lnworker.can_receive_invoice(x)
else:
- return
+ #key = x.id
+ addr = x.get_address()
+ paid, conf = self.get_payment_status(addr, x.amount)
+ d['address'] = addr
+ d['URI'] = self.get_request_URI(x)
+ if conf is not None:
+ d['confirmations'] = conf
# add URL if we are running a payserver
payserver = self.config.get_netaddress('payserver_address')
if payserver:
@@ -1669,32 +1684,58 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
use_ssl = bool(self.config.get('ssl_keyfile'))
protocol = 'https' if use_ssl else 'http'
base = '%s://%s:%d'%(protocol, payserver.host, payserver.port)
- req['view_url'] = base + root + '/pay?id=' + key
- if use_ssl and 'URI' in req:
+ d['view_url'] = base + root + '/pay?id=' + key
+ if use_ssl and 'URI' in d:
request_url = base + '/bip70/' + key + '.bip70'
- req['bip70_url'] = request_url
- return req
+ d['bip70_url'] = request_url
+ return d
+
+ def export_invoice(self, x):
+ status = self.get_invoice_status(x)
+ status_str = x.get_status_str(status)
+ is_lightning = x.is_lightning()
+ d = {
+ 'is_lightning': is_lightning,
+ 'amount': x.amount,
+ 'amount_BTC': format_satoshis(x.amount),
+ 'message': x.message,
+ 'timestamp': x.time,
+ 'expiration': x.exp,
+ 'status': status,
+ 'status_str': status_str,
+ }
+ if is_lightning:
+ d['invoice'] = x.invoice
+ if status == PR_UNPAID:
+ d['can_pay'] = self.lnworker.can_pay_invoice(x)
+ else:
+ d['outputs'] = [y.to_legacy_tuple() for y in x.outputs]
+ if x.bip70:
+ d['bip70'] = x.bip70
+ d['requestor'] = x.requestor
+ return d
def receive_tx_callback(self, tx_hash, tx, tx_height):
super().receive_tx_callback(tx_hash, tx, tx_height)
for txo in tx.outputs():
addr = self.get_txout_address(txo)
if addr in self.receive_requests:
- status, conf = self.get_request_status(addr)
+ status = self.get_request_status(addr)
util.trigger_callback('request_status', addr, status)
- def make_payment_request(self, addr, amount, message, expiration):
+ def make_payment_request(self, address, amount, message, expiration):
timestamp = int(time.time())
- _id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
- return {
- 'type': PR_TYPE_ONCHAIN,
- 'time':timestamp,
- 'amount':amount,
- 'exp':expiration,
- 'address':addr,
- 'memo':message,
- 'id':_id,
- }
+ _id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
+ return OnchainInvoice(
+ type = PR_TYPE_ONCHAIN,
+ outputs = [(TYPE_ADDRESS, address, amount)],
+ message = message,
+ time = timestamp,
+ amount = amount,
+ exp = expiration,
+ id = _id,
+ bip70 = None,
+ requestor = None)
def sign_payment_request(self, key, alias, alias_addr, password):
req = self.receive_requests.get(key)
@@ -1706,20 +1747,17 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.receive_requests[key] = req
def add_payment_request(self, req):
- if req['type'] == PR_TYPE_ONCHAIN:
- addr = req['address']
+ if not req.is_lightning():
+ addr = req.get_address()
if not bitcoin.is_address(addr):
raise Exception(_('Invalid Bitcoin address.'))
if not self.is_mine(addr):
raise Exception(_('Address not in wallet.'))
key = addr
- message = req['memo']
- elif req['type'] == PR_TYPE_LN:
- key = req['rhash']
- message = req['message']
+ message = req.message
else:
- raise Exception('Unknown request type')
- amount = req.get('amount')
+ key = req.rhash
+ message = req.message
self.receive_requests[key] = req
self.set_label(key, message) # should be a default label
return req
@@ -1748,7 +1786,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
""" sorted by timestamp """
out = [self.get_request(x) for x in self.receive_requests.keys()]
out = [x for x in out if x is not None]
- out.sort(key=operator.itemgetter('time'))
+ out.sort(key=lambda x: x.time)
return out
@abstractmethod
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
@@ -32,7 +32,8 @@ from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Seque
import binascii
from . import util, bitcoin
-from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh, PR_TYPE_ONCHAIN
+from .util import profiler, WalletFileException, multisig_type, TxMinedInfo, bfh
+from .invoices import PR_TYPE_ONCHAIN, invoice_from_json
from .keystore import bip44_derivation
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
from .logging import Logger
@@ -50,7 +51,7 @@ if TYPE_CHECKING:
OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
-FINAL_SEED_VERSION = 28 # electrum >= 2.7 will set this to prevent
+FINAL_SEED_VERSION = 29 # electrum >= 2.7 will set this to prevent
# old versions from overwriting new format
@@ -174,6 +175,7 @@ class WalletDB(JsonDB):
self._convert_version_26()
self._convert_version_27()
self._convert_version_28()
+ self._convert_version_29()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks()
@@ -605,6 +607,41 @@ class WalletDB(JsonDB):
c['local_config']['channel_seed'] = None
self.data['seed_version'] = 28
+ def _convert_version_29(self):
+ if not self._is_upgrade_method_needed(28, 28):
+ return
+ requests = self.data.get('payment_requests', {})
+ invoices = self.data.get('invoices', {})
+ for d in [invoices, requests]:
+ for key, r in list(d.items()):
+ _type = r.get('type', 0)
+ item = {
+ 'type': _type,
+ 'message': r.get('message') or r.get('memo', ''),
+ 'amount': r.get('amount'),
+ 'exp': r.get('exp', 0),
+ 'time': r.get('time', 0),
+ }
+ if _type == PR_TYPE_ONCHAIN:
+ address = r.pop('address', None)
+ if address:
+ outputs = [(0, address, r.get('amount'))]
+ else:
+ outputs = r.get('outputs')
+ item.update({
+ 'outputs': outputs,
+ 'id': r.get('id'),
+ 'bip70': r.get('bip70'),
+ 'requestor': r.get('requestor'),
+ })
+ else:
+ item.update({
+ 'rhash': r['rhash'],
+ 'invoice': r['invoice'],
+ })
+ d[key] = item
+ self.data['seed_version'] = 29
+
def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13):
return
@@ -1072,15 +1109,6 @@ class WalletDB(JsonDB):
if spending_txid not in self.transactions:
self.logger.info("removing unreferenced spent outpoint")
d.pop(prevout_n)
- # convert invoices
- # TODO invoices being these contextual dicts even internally,
- # where certain keys are only present depending on values of other keys...
- # it's horrible. we need to change this, at least for the internal representation,
- # to something that can be typed.
- self.invoices = self.get_dict('invoices')
- for invoice_key, invoice in self.invoices.items():
- if invoice.get('type') == PR_TYPE_ONCHAIN:
- invoice['outputs'] = [PartialTxOutput.from_legacy_tuple(*output) for output in invoice.get('outputs')]
@modifier
def clear_history(self):
@@ -1097,6 +1125,10 @@ class WalletDB(JsonDB):
if key == 'transactions':
# 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())
+ if key == 'invoices':
+ v = dict((k, invoice_from_json(x)) for k, x in v.items())
+ if key == 'payment_requests':
+ v = dict((k, invoice_from_json(x)) for k, x in v.items())
elif key == 'adds':
v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
elif key == 'fee_updates':