Browse Source

Use attr.s classes for invoices and requests:

- storage upgrade
 - fixes #6192
 - add can_pay_invoice, can_receive_invoice to lnworker
master
ThomasV 4 years ago
parent
commit
6058829870
  1. 41
      electrum/commands.py
  2. 8
      electrum/daemon.py
  3. 13
      electrum/gui/kivy/main_window.py
  4. 17
      electrum/gui/kivy/uix/dialogs/invoice_dialog.py
  5. 17
      electrum/gui/kivy/uix/dialogs/request_dialog.py
  6. 67
      electrum/gui/kivy/uix/screens.py
  7. 4
      electrum/gui/kivy/uix/ui_screens/receive.kv
  8. 4
      electrum/gui/kivy/uix/ui_screens/send.kv
  9. 42
      electrum/gui/qt/invoice_list.py
  10. 37
      electrum/gui/qt/main_window.py
  11. 52
      electrum/gui/qt/request_list.py
  12. 2
      electrum/gui/qt/util.py
  13. 115
      electrum/invoices.py
  14. 15
      electrum/lnaddr.py
  15. 3
      electrum/lnchannel.py
  16. 23
      electrum/lnworker.py
  17. 2
      electrum/paymentrequest.py
  18. 61
      electrum/util.py
  19. 282
      electrum/wallet.py
  20. 54
      electrum/wallet_db.py
  21. 2
      electrum/www

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

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

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

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

17
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():

67
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

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

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

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

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

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

2
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

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

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

3
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

23
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]

2
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

61
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

282
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

54
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':

2
electrum/www

@ -1 +1 @@
Subproject commit 7d902a422a1035258b5b0ad2ce5a655c4a49cf90
Subproject commit e736ae4946fac5ebcfbee35115c7cc6146d31efe
Loading…
Cancel
Save