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 5 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 .i18n import _
from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput, from .transaction import (Transaction, multisig_script, TxOutput, PartialTransaction, PartialTxOutput,
tx_from_any, PartialTxInput, TxOutpoint) 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 .synchronizer import Notifier
from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text, Deterministic_Wallet
from .address_synchronizer import TX_HEIGHT_LOCAL 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 .plugin import run_hook
from .version import ELECTRUM_VERSION from .version import ELECTRUM_VERSION
from .simple_config import SimpleConfig from .simple_config import SimpleConfig
from .lnaddr import parse_lightning_invoice from .invoices import LNInvoice
if TYPE_CHECKING: if TYPE_CHECKING:
@ -761,19 +761,13 @@ class Commands:
decrypted = wallet.decrypt_message(pubkey, encrypted, password) decrypted = wallet.decrypt_message(pubkey, encrypted, password)
return decrypted.decode('utf-8') 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') @command('w')
async def getrequest(self, key, wallet: Abstract_Wallet = None): async def getrequest(self, key, wallet: Abstract_Wallet = None):
"""Return a payment request""" """Return a payment request"""
r = wallet.get_request(key) r = wallet.get_request(key)
if not r: if not r:
raise Exception("Request not found") raise Exception("Request not found")
return self._format_request(r) return wallet.export_request(r)
#@command('w') #@command('w')
#async def ackrequest(self, serialized): #async def ackrequest(self, serialized):
@ -783,8 +777,6 @@ class Commands:
@command('w') @command('w')
async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None): async def list_requests(self, pending=False, expired=False, paid=False, wallet: Abstract_Wallet = None):
"""List the payment requests you made.""" """List the payment requests you made."""
out = wallet.get_sorted_requests()
out = list(map(self._format_request, out))
if pending: if pending:
f = PR_UNPAID f = PR_UNPAID
elif expired: elif expired:
@ -793,9 +785,10 @@ class Commands:
f = PR_PAID f = PR_PAID
else: else:
f = None f = None
out = wallet.get_sorted_requests()
if f is not None: if f is not None:
out = list(filter(lambda x: x.get('status')==f, out)) out = list(filter(lambda x: x.status==f, out))
return out return [wallet.export_request(x) for x in out]
@command('w') @command('w')
async def createnewaddress(self, wallet: Abstract_Wallet = None): async def createnewaddress(self, wallet: Abstract_Wallet = None):
@ -847,14 +840,13 @@ class Commands:
expiration = int(expiration) if expiration else None expiration = int(expiration) if expiration else None
req = wallet.make_payment_request(addr, amount, memo, expiration) req = wallet.make_payment_request(addr, amount, memo, expiration)
wallet.add_payment_request(req) wallet.add_payment_request(req)
out = wallet.get_request(addr) return wallet.export_request(req)
return self._format_request(out)
@command('wn') @command('wn')
async def add_lightning_request(self, amount, memo='', expiration=3600, wallet: Abstract_Wallet = None): async def add_lightning_request(self, amount, memo='', expiration=3600, wallet: Abstract_Wallet = None):
amount_sat = int(satoshis(amount)) amount_sat = int(satoshis(amount))
key = await wallet.lnworker._add_request_coro(amount_sat, memo, expiration) key = await wallet.lnworker._add_request_coro(amount_sat, memo, expiration)
return wallet.get_request(key) return wallet.get_formatted_request(key)
@command('w') @command('w')
async def addtransaction(self, tx, wallet: Abstract_Wallet = None): async def addtransaction(self, tx, wallet: Abstract_Wallet = None):
@ -996,14 +988,24 @@ class Commands:
@command('') @command('')
async def decode_invoice(self, invoice): 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') @command('wn')
async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None): async def lnpay(self, invoice, attempts=1, timeout=30, wallet: Abstract_Wallet = None):
lnworker = wallet.lnworker lnworker = wallet.lnworker
lnaddr = lnworker._check_invoice(invoice, None) lnaddr = lnworker._check_invoice(invoice, None)
payment_hash = lnaddr.paymenthash 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) success, log = await lnworker._pay(invoice, attempts=attempts)
return { return {
'payment_hash': payment_hash.hex(), 'payment_hash': payment_hash.hex(),
@ -1061,7 +1063,8 @@ class Commands:
@command('w') @command('w')
async def list_invoices(self, wallet: Abstract_Wallet = None): 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') @command('wn')
async def close_channel(self, channel_point, force=False, wallet: Abstract_Wallet = None): 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 . import util
from .network import Network from .network import Network
from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare) 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 .util import log_exceptions, ignore_exceptions, randrange
from .wallet import Wallet, Abstract_Wallet from .wallet import Wallet, Abstract_Wallet
from .storage import WalletStorage from .storage import WalletStorage
@ -344,13 +344,13 @@ class PayServer(Logger):
async def get_request(self, r): async def get_request(self, r):
key = r.query_string key = r.query_string
request = self.wallet.get_request(key) request = self.wallet.get_formatted_request(key)
return web.json_response(request) return web.json_response(request)
async def get_bip70_request(self, r): async def get_bip70_request(self, r):
from .paymentrequest import make_request from .paymentrequest import make_request
key = r.match_info['key'] key = r.match_info['key']
request = self.wallet.get_request(key) request = self.wallet.get_formatted_request(key)
if not request: if not request:
return web.HTTPNotFound() return web.HTTPNotFound()
pr = make_request(self.config, request) pr = make_request(self.config, request)
@ -360,7 +360,7 @@ class PayServer(Logger):
ws = web.WebSocketResponse() ws = web.WebSocketResponse()
await ws.prepare(request) await ws.prepare(request)
key = request.query_string key = request.query_string
info = self.wallet.get_request(key) info = self.wallet.get_formatted_request(key)
if not info: if not info:
await ws.send_str('unknown invoice') await ws.send_str('unknown invoice')
await ws.close() 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 import util
from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter, from electrum.util import (profiler, InvalidPassword, send_exception_to_crash_reporter,
format_satoshis, format_satoshis_plain, format_fee_satoshis, 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 import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr from electrum.interface import PREFERRED_NETWORK_PROTOCOL, ServerAddr
@ -242,7 +243,7 @@ class ElectrumWindow(App):
req = self.wallet.get_invoice(key) req = self.wallet.get_invoice(key)
if req is None: if req is None:
return return
status = req['status'] status = self.wallet.get_invoice_status(req)
# todo: update single item # todo: update single item
self.update_tab('send') self.update_tab('send')
if self.invoice_popup and self.invoice_popup.key == key: if self.invoice_popup and self.invoice_popup.key == key:
@ -393,7 +394,7 @@ class ElectrumWindow(App):
if pr.verify(self.wallet.contacts): if pr.verify(self.wallet.contacts):
key = pr.get_id() key = pr.get_id()
invoice = self.wallet.get_invoice(key) # FIXME wrong key... 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.show_error("invoice already paid")
self.send_screen.do_clear() self.send_screen.do_clear()
elif pr.has_expired(): elif pr.has_expired():
@ -451,9 +452,7 @@ class ElectrumWindow(App):
def show_request(self, is_lightning, key): def show_request(self, is_lightning, key):
from .uix.dialogs.request_dialog import RequestDialog from .uix.dialogs.request_dialog import RequestDialog
request = self.wallet.get_request(key) self.request_popup = RequestDialog('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.open() self.request_popup.open()
def show_invoice(self, is_lightning, key): def show_invoice(self, is_lightning, key):
@ -461,7 +460,7 @@ class ElectrumWindow(App):
invoice = self.wallet.get_invoice(key) invoice = self.wallet.get_invoice(key)
if not invoice: if not invoice:
return 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 = InvoiceDialog('Invoice', data, key)
self.invoice_popup.open() 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 kivy.clock import Clock
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from electrum.util import pr_tooltips, pr_color, get_request_status from electrum.invoices import pr_tooltips, pr_color
from electrum.util import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
if TYPE_CHECKING: if TYPE_CHECKING:
from electrum.gui.kivy.main_window import ElectrumWindow from electrum.gui.kivy.main_window import ElectrumWindow
@ -92,16 +92,17 @@ class InvoiceDialog(Factory.Popup):
self.title = title self.title = title
self.data = data self.data = data
self.key = key self.key = key
r = self.app.wallet.get_invoice(key) invoice = self.app.wallet.get_invoice(key)
self.amount = r.get('amount') self.amount = invoice.amount
self.description = r.get('message') or r.get('memo','') self.description = invoice.message
self.is_lightning = r.get('type') == PR_TYPE_LN self.is_lightning = invoice.type == PR_TYPE_LN
self.update_status() self.update_status()
self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else [] self.log = self.app.wallet.lnworker.logs[self.key] if self.is_lightning else []
def update_status(self): def update_status(self):
req = self.app.wallet.get_invoice(self.key) invoice = self.app.wallet.get_invoice(self.key)
self.status, self.status_str = get_request_status(req) 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.status_color = pr_color[self.status]
self.can_pay = self.status in [PR_UNPAID, PR_FAILED] self.can_pay = self.status in [PR_UNPAID, PR_FAILED]
if self.can_pay and self.is_lightning and self.app.wallet.lnworker: if self.can_pay and self.is_lightning and self.app.wallet.lnworker:

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

@ -7,8 +7,8 @@ from kivy.app import App
from kivy.clock import Clock from kivy.clock import Clock
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from electrum.util import pr_tooltips, pr_color, get_request_status from electrum.invoices import pr_tooltips, pr_color
from electrum.util import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN from electrum.invoices import PR_UNKNOWN, PR_UNPAID, PR_FAILED, PR_TYPE_LN
if TYPE_CHECKING: if TYPE_CHECKING:
from ...main_window import ElectrumWindow from ...main_window import ElectrumWindow
@ -86,17 +86,17 @@ Builder.load_string('''
class RequestDialog(Factory.Popup): class RequestDialog(Factory.Popup):
def __init__(self, title, data, key, *, is_lightning=False): def __init__(self, title, key):
self.status = PR_UNKNOWN self.status = PR_UNKNOWN
Factory.Popup.__init__(self) Factory.Popup.__init__(self)
self.app = App.get_running_app() # type: ElectrumWindow self.app = App.get_running_app() # type: ElectrumWindow
self.title = title self.title = title
self.data = data
self.key = key self.key = key
r = self.app.wallet.get_request(key) r = self.app.wallet.get_request(key)
self.amount = r.get('amount') self.is_lightning = r.is_lightning()
self.description = r.get('message', '') self.data = r.invoice if self.is_lightning else self.app.wallet.get_request_URI(r)
self.is_lightning = r.get('type') == PR_TYPE_LN self.amount = r.amount
self.description = r.message
self.update_status() self.update_status()
def on_open(self): def on_open(self):
@ -109,7 +109,8 @@ class RequestDialog(Factory.Popup):
def update_status(self): def update_status(self):
req = self.app.wallet.get_request(self.key) 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] self.status_color = pr_color[self.status]
if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker: if self.status == PR_UNPAID and self.is_lightning and self.app.wallet.lnworker:
if self.amount and self.amount > self.app.wallet.lnworker.num_sats_can_receive(): if self.amount 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 kivy.logger import Logger
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
from electrum.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 import bitcoin, constants
from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput from electrum.transaction import Transaction, tx_from_any, PartialTransaction, PartialTxOutput
from electrum.util import (parse_URI, InvalidBitcoinURI, PR_PAID, PR_UNKNOWN, PR_EXPIRED, from electrum.util import parse_URI, InvalidBitcoinURI, TxMinedInfo, maybe_extract_bolt11_invoice
PR_INFLIGHT, TxMinedInfo, get_request_status, pr_expiration_values,
maybe_extract_bolt11_invoice)
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum.wallet import InternalAddressCorruption from electrum.wallet import InternalAddressCorruption
from electrum import simple_config from electrum import simple_config
from electrum.simple_config import FEERATE_WARNING_HIGH_FEE, FEE_RATIO_HIGH_WARNING 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 electrum.lnutil import RECEIVED, SENT, PaymentFailure
from .dialogs.question import Question from .dialogs.question import Question
@ -225,26 +225,27 @@ class SendScreen(CScreen):
self.app.show_invoice(obj.is_lightning, obj.key) self.app.show_invoice(obj.is_lightning, obj.key)
def get_card(self, item): def get_card(self, item):
invoice_type = item['type'] status = self.app.wallet.get_invoice_status(item)
status, status_str = get_request_status(item) # convert to str status_str = item.get_status_str(status)
if invoice_type == PR_TYPE_LN: is_lightning = item.type == PR_TYPE_LN
key = item['rhash'] if is_lightning:
key = item.rhash
log = self.app.wallet.lnworker.logs.get(key) 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) status_str += '... (%d)'%len(log)
elif invoice_type == PR_TYPE_ONCHAIN: is_bip70 = False
key = item['id']
else: else:
raise Exception('unknown invoice type') key = item.id
is_bip70 = bool(item.bip70)
return { return {
'is_lightning': invoice_type == PR_TYPE_LN, 'is_lightning': is_lightning,
'is_bip70': 'bip70' in item, 'is_bip70': is_bip70,
'screen': self, 'screen': self,
'status': status, 'status': status,
'status_str': status_str, 'status_str': status_str,
'key': key, 'key': key,
'memo': item['message'], 'memo': item.message,
'amount': self.app.format_amount_and_units(item['amount'] or 0), 'amount': self.app.format_amount_and_units(item.amount or 0),
} }
def do_clear(self): def do_clear(self):
@ -300,7 +301,7 @@ class SendScreen(CScreen):
return return
message = self.message message = self.message
if self.is_lightning: if self.is_lightning:
return parse_lightning_invoice(address) return LNInvoice.from_bech32(address)
else: # on-chain else: # on-chain
if self.payment_request: if self.payment_request:
outputs = self.payment_request.get_outputs() outputs = self.payment_request.get_outputs()
@ -329,26 +330,27 @@ class SendScreen(CScreen):
self.do_pay_invoice(invoice) self.do_pay_invoice(invoice)
def do_pay_invoice(self, invoice): def do_pay_invoice(self, invoice):
if invoice['type'] == PR_TYPE_LN: if invoice.is_lightning():
self._do_pay_lightning(invoice) self._do_pay_lightning(invoice)
return return
elif invoice['type'] == PR_TYPE_ONCHAIN: else:
do_pay = lambda rbf: self._do_pay_onchain(invoice, rbf) do_pay = lambda rbf: self._do_pay_onchain(invoice, rbf)
if self.app.electrum_config.get('use_rbf'): if self.app.electrum_config.get('use_rbf'):
d = Question(_('Should this transaction be replaceable?'), do_pay) d = Question(_('Should this transaction be replaceable?'), do_pay)
d.open() d.open()
else: else:
do_pay(False) do_pay(False)
else:
raise Exception('unknown invoice type')
def _do_pay_lightning(self, invoice): def _do_pay_lightning(self, invoice):
attempts = 10 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): def _do_pay_onchain(self, invoice, rbf):
# make unsigned transaction # make unsigned transaction
outputs = invoice['outputs'] # type: List[PartialTxOutput] outputs = invoice.outputs # type: List[PartialTxOutput]
coins = self.app.wallet.get_spendable_coins(None) coins = self.app.wallet.get_spendable_coins(None)
try: try:
tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs) tx = self.app.wallet.make_unsigned_transaction(coins=coins, outputs=outputs)
@ -405,7 +407,7 @@ class SendScreen(CScreen):
def callback(c): def callback(c):
if c: if c:
for req in invoices: 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.app.wallet.delete_invoice(key)
self.update() self.update()
n = len(invoices) n = len(invoices)
@ -477,16 +479,17 @@ class ReceiveScreen(CScreen):
self.app.show_request(lightning, key) self.app.show_request(lightning, key)
def get_card(self, req): def get_card(self, req):
is_lightning = req.get('type') == PR_TYPE_LN is_lightning = req.is_lightning()
if not is_lightning: if not is_lightning:
address = req['address'] address = req.get_address()
key = address key = address
else: else:
key = req['rhash'] key = req.rhash
address = req['invoice'] address = req.invoice
amount = req.get('amount') amount = req.amount
description = req.get('message') or req.get('memo', '') # TODO: a db upgrade would be needed to simplify that. description = req.message
status, status_str = get_request_status(req) status = self.app.wallet.get_request_status(key)
status_str = req.get_status_str(status)
ci = {} ci = {}
ci['screen'] = self ci['screen'] = self
ci['address'] = address ci['address'] = address

4
electrum/gui/kivy/uix/ui_screens/receive.kv

@ -1,6 +1,6 @@
#:import _ electrum.gui.kivy.i18n._ #:import _ electrum.gui.kivy.i18n._
#:import pr_color electrum.util.pr_color #:import pr_color electrum.invoices.pr_color
#:import PR_UNKNOWN electrum.util.PR_UNKNOWN #:import PR_UNKNOWN electrum.invoices.PR_UNKNOWN
#:import Factory kivy.factory.Factory #:import Factory kivy.factory.Factory
#:import Decimal decimal.Decimal #:import Decimal decimal.Decimal
#:set btc_symbol chr(171) #:set btc_symbol chr(171)

4
electrum/gui/kivy/uix/ui_screens/send.kv

@ -1,6 +1,6 @@
#:import _ electrum.gui.kivy.i18n._ #:import _ electrum.gui.kivy.i18n._
#:import pr_color electrum.util.pr_color #:import pr_color electrum.invoices.pr_color
#:import PR_UNKNOWN electrum.util.PR_UNKNOWN #:import PR_UNKNOWN electrum.invoices.PR_UNKNOWN
#:import Factory kivy.factory.Factory #:import Factory kivy.factory.Factory
#:import Decimal decimal.Decimal #:import Decimal decimal.Decimal
#:set btc_symbol chr(171) #: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 PyQt5.QtWidgets import QMenu, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QHeaderView
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED from electrum.util import format_time
from electrum.util import get_request_status from electrum.invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.lnutil import PaymentAttemptLog from electrum.lnutil import PaymentAttemptLog
from .util import (MyTreeView, read_QIcon, MySortModel, from .util import (MyTreeView, read_QIcon, MySortModel,
@ -77,7 +76,7 @@ class InvoiceList(MyTreeView):
self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.update() self.update()
def update_item(self, key, req): def update_item(self, key, invoice: Invoice):
model = self.std_model model = self.std_model
for row in range(0, model.rowCount()): for row in range(0, model.rowCount()):
item = model.item(row, 0) item = model.item(row, 0)
@ -86,7 +85,8 @@ class InvoiceList(MyTreeView):
else: else:
return return
status_item = model.item(row, self.Columns.STATUS) 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: if self.parent.wallet.lnworker:
log = self.parent.wallet.lnworker.logs.get(key) log = self.parent.wallet.lnworker.logs.get(key)
if log and status == PR_INFLIGHT: if log and status == PR_INFLIGHT:
@ -100,21 +100,21 @@ class InvoiceList(MyTreeView):
self.std_model.clear() self.std_model.clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for idx, item in enumerate(self.parent.wallet.get_invoices()): for idx, item in enumerate(self.parent.wallet.get_invoices()):
invoice_type = item['type'] if item.type == PR_TYPE_LN:
if invoice_type == PR_TYPE_LN: key = item.rhash
key = item['rhash']
icon_name = 'lightning.png' icon_name = 'lightning.png'
elif invoice_type == PR_TYPE_ONCHAIN: elif item.type == PR_TYPE_ONCHAIN:
key = item['id'] key = item.id
icon_name = 'bitcoin.png' icon_name = 'bitcoin.png'
if item.get('bip70'): if item.bip70:
icon_name = 'seal.png' icon_name = 'seal.png'
else: else:
raise Exception('Unsupported type') raise Exception('Unsupported type')
status, status_str = get_request_status(item) status = self.parent.wallet.get_invoice_status(item)
message = item['message'] status_str = item.get_status_str(status)
amount = item['amount'] message = item.message
timestamp = item.get('time', 0) amount = item.amount
timestamp = item.time or 0
date_str = format_time(timestamp) if timestamp else _('Unknown') date_str = format_time(timestamp) if timestamp else _('Unknown')
amount_str = self.parent.format_amount(amount, whitespaces=True) amount_str = self.parent.format_amount(amount, whitespaces=True)
labels = [date_str, message, amount_str, status_str] 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.DATE].setIcon(read_QIcon(icon_name))
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status))) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID) items[self.Columns.DATE].setData(key, role=ROLE_REQUEST_ID)
items[self.Columns.DATE].setData(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) items[self.Columns.DATE].setData(timestamp, role=ROLE_SORT_ORDER)
self.std_model.insertRow(idx, items) self.std_model.insertRow(idx, items)
self.filter() self.filter()
@ -143,11 +143,12 @@ class InvoiceList(MyTreeView):
export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file) export_meta_gui(self.parent, _('invoices'), self.parent.invoices.export_file)
def create_menu(self, position): def create_menu(self, position):
wallet = self.parent.wallet
items = self.selected_in_column(0) items = self.selected_in_column(0)
if len(items)>1: if len(items)>1:
keys = [ item.data(ROLE_REQUEST_ID) for item in items] keys = [ item.data(ROLE_REQUEST_ID) for item in items]
invoices = [ self.parent.wallet.get_invoice(key) for key in keys] invoices = [ wallet.invoices.get(key) for key in keys]
can_batch_pay = all([ invoice['status'] == PR_UNPAID and invoice['type'] == PR_TYPE_ONCHAIN for invoice in invoices]) can_batch_pay = all([i.type == PR_TYPE_ONCHAIN and wallet.get_invoice_status(i) == PR_UNPAID for i in invoices])
menu = QMenu(self) menu = QMenu(self)
if can_batch_pay: if can_batch_pay:
menu.addAction(_("Batch pay invoices"), lambda: self.parent.pay_multiple_invoices(invoices)) menu.addAction(_("Batch pay invoices"), lambda: self.parent.pay_multiple_invoices(invoices))
@ -164,9 +165,10 @@ class InvoiceList(MyTreeView):
self.add_copy_menu(menu, idx) self.add_copy_menu(menu, idx)
invoice = self.parent.wallet.get_invoice(key) invoice = self.parent.wallet.get_invoice(key)
menu.addAction(_("Details"), lambda: self.parent.show_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)) 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)) menu.addAction(_("Retry"), lambda: self.parent.do_pay_invoice(invoice))
if self.parent.wallet.lnworker: if self.parent.wallet.lnworker:
log = self.parent.wallet.lnworker.logs.get(key) 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, get_new_wallet_name, send_exception_to_crash_reporter,
InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds, InvalidBitcoinURI, maybe_extract_bolt11_invoice, NotEnoughFunds,
NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs) NoDynamicFeeEstimates, MultipleSpendMaxTxOutputs)
from electrum.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, from electrum.transaction import (Transaction, PartialTxInput,
PartialTransaction, PartialTxOutput) PartialTransaction, PartialTxOutput)
from electrum.address_synchronizer import AddTransactionException 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.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.logging import Logger 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.lnutil import ln_dummy_address
from electrum.lnaddr import parse_lightning_invoice
from .exception_window import Exception_Hook from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit
@ -1192,7 +1190,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.receive_message_e.setText('') self.receive_message_e.setText('')
# copy to clipboard # copy to clipboard
r = self.wallet.get_request(key) r = self.wallet.get_request(key)
content = r.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') title = _('Invoice') if is_lightning else _('Address')
self.do_copy(content, title=title) self.do_copy(content, title=title)
@ -1231,7 +1229,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def export_payment_request(self, addr): def export_payment_request(self, addr):
r = self.wallet.receive_requests.get(addr) r = self.wallet.receive_requests.get(addr)
pr = paymentrequest.serialize_request(r).SerializeToString() 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") fileName = self.getSaveFileName(_("Select where to save your payment request"), name, "*.bip70")
if fileName: if fileName:
with open(fileName, "wb+") as f: 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(): if self.check_send_tab_payto_line_and_show_errors():
return return
if not self._is_onchain: if not self._is_onchain:
invoice = self.payto_e.lightning_invoice invoice_str = self.payto_e.lightning_invoice
if not invoice: if not invoice_str:
return return
if not self.wallet.lnworker: if not self.wallet.lnworker:
self.show_error(_('Lightning is disabled')) self.show_error(_('Lightning is disabled'))
return return
invoice_dict = parse_lightning_invoice(invoice) invoice = LNInvoice.from_bech32(invoice_str)
if invoice_dict.get('amount') is None: if invoice.amount is None:
amount = self.amount_e.get_amount() amount = self.amount_e.get_amount()
if amount: if amount:
invoice_dict['amount'] = amount invoice.amount = amount
else: else:
self.show_error(_('No amount')) self.show_error(_('No amount'))
return return
return invoice_dict return invoice
else: else:
outputs = self.read_outputs() outputs = self.read_outputs()
if self.check_send_tab_onchain_outputs_and_show_errors(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): def pay_multiple_invoices(self, invoices):
outputs = [] outputs = []
for invoice in invoices: for invoice in invoices:
outputs += invoice['outputs'] outputs += invoice.outputs
self.pay_onchain_dialog(self.get_coins(), outputs) self.pay_onchain_dialog(self.get_coins(), outputs)
def do_pay_invoice(self, invoice): def do_pay_invoice(self, invoice):
if invoice['type'] == PR_TYPE_LN: if invoice.type == PR_TYPE_LN:
self.pay_lightning_invoice(invoice['invoice'], invoice['amount']) self.pay_lightning_invoice(invoice.invoice, invoice.amount)
elif invoice['type'] == PR_TYPE_ONCHAIN: elif invoice.type == PR_TYPE_ONCHAIN:
outputs = invoice['outputs'] self.pay_onchain_dialog(self.get_coins(), invoice.outputs)
self.pay_onchain_dialog(self.get_coins(), outputs)
else: else:
raise Exception('unknown invoice type') raise Exception('unknown invoice type')
@ -1775,7 +1772,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return return
key = pr.get_id() key = pr.get_id()
invoice = self.wallet.get_invoice(key) 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.show_message("invoice already paid")
self.do_clear() self.do_clear()
self.payment_request = None self.payment_request = None
@ -1970,7 +1967,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if invoice is None: if invoice is None:
self.show_error('Cannot find payment request in wallet.') self.show_error('Cannot find payment request in wallet.')
return return
bip70 = invoice.get('bip70') bip70 = invoice.bip70
if bip70: if bip70:
pr = paymentrequest.PaymentRequest(bytes.fromhex(bip70)) pr = paymentrequest.PaymentRequest(bytes.fromhex(bip70))
pr.verify(self.contacts) 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 PyQt5.QtCore import Qt, QItemSelectionModel, QModelIndex
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time, get_request_status from electrum.util import format_time
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.plugin import run_hook from electrum.plugin import run_hook
from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel from .util import MyTreeView, pr_icons, read_QIcon, webopen, MySortModel
@ -90,18 +90,17 @@ class RequestList(MyTreeView):
return return
# TODO use siblingAtColumn when min Qt version is >=5.11 # TODO use siblingAtColumn when min Qt version is >=5.11
item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE)) item = self.item_from_index(idx.sibling(idx.row(), self.Columns.DATE))
request_type = item.data(ROLE_REQUEST_TYPE)
key = item.data(ROLE_KEY) key = item.data(ROLE_KEY)
req = self.wallet.get_request(key) req = self.wallet.get_request(key)
if req is None: if req is None:
self.update() self.update()
return return
if request_type == PR_TYPE_LN: if req.is_lightning():
self.parent.receive_payreq_e.setText(req.get('invoice')) self.parent.receive_payreq_e.setText(req.invoice)
self.parent.receive_address_e.setText(req.get('invoice')) self.parent.receive_address_e.setText(req.invoice)
else: else:
self.parent.receive_payreq_e.setText(req.get('URI')) self.parent.receive_payreq_e.setText(self.parent.wallet.get_request_URI(req))
self.parent.receive_address_e.setText(req['address']) self.parent.receive_address_e.setText(req.get_address())
self.parent.receive_payreq_e.repaint() # macOS hack (similar to #4777) self.parent.receive_payreq_e.repaint() # macOS hack (similar to #4777)
self.parent.receive_address_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) key = date_item.data(ROLE_KEY)
req = self.wallet.get_request(key) req = self.wallet.get_request(key)
if req: 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.setText(status_str)
status_item.setIcon(read_QIcon(pr_icons.get(status))) status_item.setIcon(read_QIcon(pr_icons.get(status)))
@ -130,20 +130,22 @@ class RequestList(MyTreeView):
self.std_model.clear() self.std_model.clear()
self.update_headers(self.__class__.headers) self.update_headers(self.__class__.headers)
for req in self.wallet.get_sorted_requests(): for req in self.wallet.get_sorted_requests():
status, status_str = get_request_status(req) key = req.rhash if req.is_lightning() else req.id
request_type = req['type'] status = self.parent.wallet.get_request_status(key)
timestamp = req.get('time', 0) status_str = req.get_status_str(status)
amount = req.get('amount') request_type = req.type
message = req.get('message') or req.get('memo') timestamp = req.time
amount = req.amount
message = req.message
date = format_time(timestamp) date = format_time(timestamp)
amount_str = self.parent.format_amount(amount) if amount else "" amount_str = self.parent.format_amount(amount) if amount else ""
labels = [date, message, amount_str, status_str] labels = [date, message, amount_str, status_str]
if request_type == PR_TYPE_LN: if req.is_lightning():
key = req['rhash'] key = req.rhash
icon = read_QIcon("lightning.png") icon = read_QIcon("lightning.png")
tooltip = 'lightning request' tooltip = 'lightning request'
elif request_type == PR_TYPE_ONCHAIN: else:
key = req['address'] key = req.get_address()
icon = read_QIcon("bitcoin.png") icon = read_QIcon("bitcoin.png")
tooltip = 'onchain request' tooltip = 'onchain request'
items = [QStandardItem(e) for e in labels] items = [QStandardItem(e) for e in labels]
@ -182,20 +184,20 @@ class RequestList(MyTreeView):
if not item: if not item:
return return
key = item.data(ROLE_KEY) key = item.data(ROLE_KEY)
request_type = item.data(ROLE_REQUEST_TYPE)
req = self.wallet.get_request(key) req = self.wallet.get_request(key)
if req is None: if req is None:
self.update() self.update()
return return
menu = QMenu(self) menu = QMenu(self)
self.add_copy_menu(menu, idx) self.add_copy_menu(menu, idx)
if request_type == PR_TYPE_LN: if req.is_lightning():
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req['invoice'], title='Lightning Request')) menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req.invoice, title='Lightning Request'))
else: else:
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(req['URI'], title='Bitcoin URI')) URI = self.wallet.get_request_URI(req)
menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req['address'], title='Bitcoin Address')) menu.addAction(_("Copy Request"), lambda: self.parent.do_copy(URI, title='Bitcoin URI'))
if 'view_url' in req: menu.addAction(_("Copy Address"), lambda: self.parent.do_copy(req.get_address(), title='Bitcoin Address'))
menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) #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])) menu.addAction(_("Delete"), lambda: self.parent.delete_requests([key]))
run_hook('receive_list_menu', menu, key) run_hook('receive_list_menu', menu, key)
menu.exec_(self.viewport().mapToGlobal(position)) 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.i18n import _, languages
from electrum.util import FileImportFailed, FileExportFailed, make_aiohttp_session, resource_path 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: if TYPE_CHECKING:
from .main_window import ElectrumWindow 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 .segwit_addr import bech32_encode, bech32_decode, CHARSET
from . import constants from . import constants
from . import ecc from . import ecc
from .util import PR_TYPE_LN
from .bitcoin import COIN 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__': if __name__ == '__main__':
# run using # run using
# python3 -m electrum.lnaddr <invoice> <expected hrp> # python3 -m electrum.lnaddr <invoice> <expected hrp>

3
electrum/lnchannel.py

@ -35,7 +35,8 @@ import attr
from . import ecc from . import ecc
from . import constants, util 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 .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d from .crypto import sha256, sha256d
from .transaction import Transaction, PartialTransaction, TxInput 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 constants, util
from . import keystore from . import keystore
from .util import profiler from .util import profiler
from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING from .invoices import PR_TYPE_LN, PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED, PR_ROUTING, LNInvoice
from .util import PR_TYPE_LN, NetworkRetryManager from .util import NetworkRetryManager
from .lnutil import LN_MAX_FUNDING_SAT from .lnutil import LN_MAX_FUNDING_SAT
from .keystore import BIP32_KeyStore from .keystore import BIP32_KeyStore
from .bitcoin import COIN from .bitcoin import COIN
@ -1102,15 +1102,7 @@ class LNWallet(LNWorker):
payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage)) payment_secret=derive_payment_secret_from_payment_preimage(payment_preimage))
invoice = lnencode(lnaddr, self.node_keypair.privkey) invoice = lnencode(lnaddr, self.node_keypair.privkey)
key = bh2u(lnaddr.paymenthash) key = bh2u(lnaddr.paymenthash)
req = { req = LNInvoice.from_bech32(invoice)
'type': PR_TYPE_LN,
'amount': amount_sat,
'time': lnaddr.date,
'exp': expiry,
'message': message,
'rhash': key,
'invoice': invoice
}
self.save_preimage(payment_hash, payment_preimage) self.save_preimage(payment_hash, payment_preimage)
self.save_payment_info(info) self.save_payment_info(info)
self.wallet.add_payment_request(req) self.wallet.add_payment_request(req)
@ -1145,7 +1137,8 @@ class LNWallet(LNWorker):
info = self.get_payment_info(payment_hash) info = self.get_payment_info(payment_hash)
return info.status if info else PR_UNPAID return info.status if info else PR_UNPAID
def get_invoice_status(self, key): def get_invoice_status(self, invoice):
key = invoice.rhash
log = self.logs[key] log = self.logs[key]
if key in self.is_routing: if key in self.is_routing:
return PR_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 return Decimal(max(chan.available_to_spend(REMOTE) if chan.is_open() else 0
for chan in self.channels.values()))/1000 if self.channels else 0 for chan in self.channels.values()))/1000 if self.channels else 0
def can_pay_invoice(self, invoice):
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): async def close_channel(self, chan_id):
chan = self._channels[chan_id] chan = self._channels[chan_id]
peer = self._peers[chan.node_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 . import bitcoin, ecc, util, transaction, x509, rsakey
from .util import bh2u, bfh, export_meta, import_meta, make_aiohttp_session 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 .crypto import sha256
from .bitcoin import address_to_script from .bitcoin import address_to_script
from .transaction import PartialTxOutput from .transaction import PartialTxOutput

61
electrum/util.py

@ -43,6 +43,7 @@ from typing import NamedTuple, Optional
import ssl import ssl
import ipaddress import ipaddress
import random import random
import attr
import aiohttp import aiohttp
from aiohttp_socks import ProxyConnector, ProxyType 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 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 class UnknownBaseUnit(Exception): pass

282
electrum/wallet.py

@ -52,10 +52,10 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs, WalletFileException, BitcoinException, MultipleSpendMaxTxOutputs,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis, InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex) 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 .simple_config import SimpleConfig
from .bitcoin import (COIN, is_address, address_to_script, from .bitcoin import COIN, TYPE_ADDRESS
is_minikey, relayfee, dust_threshold) from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
from .crypto import sha256d from .crypto import sha256d
from . import keystore from . import keystore
from .keystore import load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, AddressIndexGeneric 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 .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL, from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE) TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
from .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 .contacts import Contacts
from .interface import NetworkException from .interface import NetworkException
from .mnemonic import Mnemonic from .mnemonic import Mnemonic
@ -660,39 +661,43 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
amount = '!' amount = '!'
else: else:
amount = sum(x.value for x in outputs) amount = sum(x.value for x in outputs)
invoice = { outputs = [x.to_legacy_tuple() for x in outputs]
'type': PR_TYPE_ONCHAIN,
'message': message,
'outputs': outputs,
'amount': amount,
}
if pr: if pr:
invoice['bip70'] = pr.raw.hex() invoice = OnchainInvoice(
invoice['time'] = pr.get_time() type = PR_TYPE_ONCHAIN,
invoice['exp'] = pr.get_expiration_date() - pr.get_time() amount = amount,
invoice['requestor'] = pr.get_requestor() outputs = outputs,
invoice['message'] = pr.get_memo() message = pr.get_memo(),
elif URI: id = pr.get_id(),
timestamp = URI.get('time') time = pr.get_time(),
if timestamp: invoice['time'] = timestamp exp = pr.get_expiration_date() - pr.get_time(),
exp = URI.get('exp') bip70 = pr.raw.hex() if pr else None,
if exp: invoice['exp'] = exp requestor = pr.get_requestor(),
if 'time' not in invoice: )
invoice['time'] = int(time.time()) 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 return invoice
def save_invoice(self, invoice): def save_invoice(self, invoice: Invoice):
invoice_type = invoice['type'] invoice_type = invoice.type
if invoice_type == PR_TYPE_LN: if invoice_type == PR_TYPE_LN:
key = invoice['rhash'] key = invoice.rhash
elif invoice_type == PR_TYPE_ONCHAIN: elif invoice_type == PR_TYPE_ONCHAIN:
key = invoice.id
if self.is_onchain_invoice_paid(invoice): if self.is_onchain_invoice_paid(invoice):
self.logger.info("saving invoice... but it is already paid!") 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: with self.transaction_lock:
for txout in outputs: for txout in invoice.outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key) self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(key)
else: else:
raise Exception('Unsupported invoice type') raise Exception('Unsupported invoice type')
@ -704,26 +709,13 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.save_db() self.save_db()
def get_invoices(self): def get_invoices(self):
out = [self.get_invoice(key) for key in self.invoices.keys()] out = list(self.invoices.values())
out = list(filter(None, out)) #out = list(filter(None, out)) filter out ln
out.sort(key=operator.itemgetter('time')) out.sort(key=lambda x:x.time)
return out return out
def get_invoice(self, key): def get_invoice(self, key):
if key not in self.invoices: return self.invoices.get(key)
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
def _get_relevant_invoice_keys_for_tx(self, tx: Transaction) -> Set[str]: def _get_relevant_invoice_keys_for_tx(self, tx: Transaction) -> Set[str]:
relevant_invoice_keys = set() relevant_invoice_keys = set()
@ -736,16 +728,15 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
# scriptpubkey -> list(invoice_keys) # scriptpubkey -> list(invoice_keys)
self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]] self._invoices_from_scriptpubkey_map = defaultdict(set) # type: Dict[bytes, Set[str]]
for invoice_key, invoice in self.invoices.items(): for invoice_key, invoice in self.invoices.items():
if invoice.get('type') == PR_TYPE_ONCHAIN: if invoice.type == PR_TYPE_ONCHAIN:
outputs = invoice['outputs'] # type: List[PartialTxOutput] for txout in invoice.outputs:
for txout in outputs:
self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key) self._invoices_from_scriptpubkey_map[txout.scriptpubkey].add(invoice_key)
def _is_onchain_invoice_paid(self, invoice: 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.""" """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 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 invoice_amounts[txo.scriptpubkey] += 1 if txo.value == '!' else txo.value
relevant_txs = [] relevant_txs = []
with self.transaction_lock: with self.transaction_lock:
@ -762,7 +753,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return False, [] return False, []
return True, relevant_txs 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] return self._is_onchain_invoice_paid(invoice)[0]
def _maybe_set_tx_label_based_on_invoices(self, tx: Transaction) -> bool: 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]: def get_unused_addresses(self) -> Sequence[str]:
domain = self.get_receiving_addresses() 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) return [addr for addr in domain if not self.is_used(addr)
and addr not in in_use_by_request] and addr not in in_use_by_request]
@ -1608,60 +1599,84 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
return True, conf return True, conf
return False, None return False, None
def get_request_URI(self, addr): def get_request_URI(self, req: Invoice):
req = self.receive_requests[addr] addr = req.get_address()
message = self.labels.get(addr, '') message = self.labels.get(addr, '')
amount = req['amount'] amount = req.amount
extra_query_params = {} extra_query_params = {}
if req.get('time'): if req.time:
extra_query_params['time'] = str(int(req.get('time'))) extra_query_params['time'] = str(int(req.time))
if req.get('exp'): if req.exp:
extra_query_params['exp'] = str(int(req.get('exp'))) extra_query_params['exp'] = str(int(req.exp))
if req.get('name') and req.get('sig'): #if req.get('name') and req.get('sig'):
sig = bfh(req.get('sig')) # sig = bfh(req.get('sig'))
sig = bitcoin.base_encode(sig, base=58) # sig = bitcoin.base_encode(sig, base=58)
extra_query_params['name'] = req['name'] # extra_query_params['name'] = req['name']
extra_query_params['sig'] = sig # extra_query_params['sig'] = sig
uri = create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params) uri = create_bip21_uri(addr, amount, message, extra_query_params=extra_query_params)
return str(uri) return str(uri)
def get_request_status(self, address): def check_expired_status(self, r, status):
r = self.receive_requests.get(address) 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: if r is None:
return PR_UNKNOWN return PR_UNKNOWN
amount = r.get('amount', 0) or 0 if r.is_lightning():
timestamp = r.get('time', 0) status = self.lnworker.get_payment_status(bfh(r.rhash))
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
else: else:
status = PR_PAID paid, conf = self.get_payment_status(r.get_address(), r.amount)
return status, conf status = PR_PAID if paid else PR_UNPAID
return self.check_expired_status(r, status)
def get_request(self, key): def get_request(self, key):
req = self.receive_requests.get(key) return self.receive_requests.get(key)
if not req:
return def get_formatted_request(self, key):
# convert StoredDict to dict x = self.receive_requests.get(key)
req = dict(req) if x:
_type = req.get('type') return self.export_request(x)
if _type == PR_TYPE_ONCHAIN:
addr = req['address'] def export_request(self, x):
req['URI'] = self.get_request_URI(addr) key = x.rhash if x.is_lightning() else x.get_address()
status, conf = self.get_request_status(addr) status = self.get_request_status(key)
req['status'] = status status_str = x.get_status_str(status)
if conf is not None: is_lightning = x.is_lightning()
req['confirmations'] = conf d = {
elif self.lnworker and _type == PR_TYPE_LN: 'is_lightning': is_lightning,
req['status'] = self.lnworker.get_payment_status(bfh(key)) '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: 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 # add URL if we are running a payserver
payserver = self.config.get_netaddress('payserver_address') payserver = self.config.get_netaddress('payserver_address')
if payserver: if payserver:
@ -1669,32 +1684,58 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
use_ssl = bool(self.config.get('ssl_keyfile')) use_ssl = bool(self.config.get('ssl_keyfile'))
protocol = 'https' if use_ssl else 'http' protocol = 'https' if use_ssl else 'http'
base = '%s://%s:%d'%(protocol, payserver.host, payserver.port) base = '%s://%s:%d'%(protocol, payserver.host, payserver.port)
req['view_url'] = base + root + '/pay?id=' + key d['view_url'] = base + root + '/pay?id=' + key
if use_ssl and 'URI' in req: if use_ssl and 'URI' in d:
request_url = base + '/bip70/' + key + '.bip70' request_url = base + '/bip70/' + key + '.bip70'
req['bip70_url'] = request_url d['bip70_url'] = request_url
return req 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): def receive_tx_callback(self, tx_hash, tx, tx_height):
super().receive_tx_callback(tx_hash, tx, tx_height) super().receive_tx_callback(tx_hash, tx, tx_height)
for txo in tx.outputs(): for txo in tx.outputs():
addr = self.get_txout_address(txo) addr = self.get_txout_address(txo)
if addr in self.receive_requests: 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) 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()) timestamp = int(time.time())
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10] _id = bh2u(sha256d(address + "%d"%timestamp))[0:10]
return { return OnchainInvoice(
'type': PR_TYPE_ONCHAIN, type = PR_TYPE_ONCHAIN,
'time':timestamp, outputs = [(TYPE_ADDRESS, address, amount)],
'amount':amount, message = message,
'exp':expiration, time = timestamp,
'address':addr, amount = amount,
'memo':message, exp = expiration,
'id':_id, id = _id,
} bip70 = None,
requestor = None)
def sign_payment_request(self, key, alias, alias_addr, password): def sign_payment_request(self, key, alias, alias_addr, password):
req = self.receive_requests.get(key) req = self.receive_requests.get(key)
@ -1706,20 +1747,17 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
self.receive_requests[key] = req self.receive_requests[key] = req
def add_payment_request(self, req): def add_payment_request(self, req):
if req['type'] == PR_TYPE_ONCHAIN: if not req.is_lightning():
addr = req['address'] addr = req.get_address()
if not bitcoin.is_address(addr): if not bitcoin.is_address(addr):
raise Exception(_('Invalid Bitcoin address.')) raise Exception(_('Invalid Bitcoin address.'))
if not self.is_mine(addr): if not self.is_mine(addr):
raise Exception(_('Address not in wallet.')) raise Exception(_('Address not in wallet.'))
key = addr key = addr
message = req['memo'] message = req.message
elif req['type'] == PR_TYPE_LN:
key = req['rhash']
message = req['message']
else: else:
raise Exception('Unknown request type') key = req.rhash
amount = req.get('amount') message = req.message
self.receive_requests[key] = req self.receive_requests[key] = req
self.set_label(key, message) # should be a default label self.set_label(key, message) # should be a default label
return req return req
@ -1748,7 +1786,7 @@ class Abstract_Wallet(AddressSynchronizer, ABC):
""" sorted by timestamp """ """ sorted by timestamp """
out = [self.get_request(x) for x in self.receive_requests.keys()] out = [self.get_request(x) for x in self.receive_requests.keys()]
out = [x for x in out if x is not None] out = [x for x in out if x is not None]
out.sort(key=operator.itemgetter('time')) out.sort(key=lambda x: x.time)
return out return out
@abstractmethod @abstractmethod

54
electrum/wallet_db.py

@ -32,7 +32,8 @@ from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Seque
import binascii import binascii
from . import util, bitcoin 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 .keystore import bip44_derivation
from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransaction, PartialTxOutput
from .logging import Logger from .logging import Logger
@ -50,7 +51,7 @@ if TYPE_CHECKING:
OLD_SEED_VERSION = 4 # electrum versions < 2.0 OLD_SEED_VERSION = 4 # electrum versions < 2.0
NEW_SEED_VERSION = 11 # electrum versions >= 2.0 NEW_SEED_VERSION = 11 # electrum versions >= 2.0
FINAL_SEED_VERSION = 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 # old versions from overwriting new format
@ -174,6 +175,7 @@ class WalletDB(JsonDB):
self._convert_version_26() self._convert_version_26()
self._convert_version_27() self._convert_version_27()
self._convert_version_28() self._convert_version_28()
self._convert_version_29()
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
self._after_upgrade_tasks() self._after_upgrade_tasks()
@ -605,6 +607,41 @@ class WalletDB(JsonDB):
c['local_config']['channel_seed'] = None c['local_config']['channel_seed'] = None
self.data['seed_version'] = 28 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): def _convert_imported(self):
if not self._is_upgrade_method_needed(0, 13): if not self._is_upgrade_method_needed(0, 13):
return return
@ -1072,15 +1109,6 @@ class WalletDB(JsonDB):
if spending_txid not in self.transactions: if spending_txid not in self.transactions:
self.logger.info("removing unreferenced spent outpoint") self.logger.info("removing unreferenced spent outpoint")
d.pop(prevout_n) 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 @modifier
def clear_history(self): def clear_history(self):
@ -1097,6 +1125,10 @@ class WalletDB(JsonDB):
if key == 'transactions': if key == 'transactions':
# note: for performance, "deserialize=False" so that we will deserialize these on-demand # note: for performance, "deserialize=False" so that we will deserialize these on-demand
v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items()) v = dict((k, tx_from_any(x, deserialize=False)) for k, x in v.items())
if key == 'invoices':
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': elif key == 'adds':
v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items()) v = dict((k, UpdateAddHtlc.from_tuple(*x)) for k, x in v.items())
elif key == 'fee_updates': elif key == 'fee_updates':

2
electrum/www

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