commit a50f935aecc1a47dffce195808ed3b1839c8e3a0
parent 3902d774f7ced2311ab07e5309dac2eeff2af0c7
Author: ThomasV <thomasv@electrum.org>
Date: Sun, 11 Aug 2019 14:47:06 +0200
Restructure invoices and requests (WIP)
- Terminology: use 'invoices' for outgoing payments, 'requests' for incoming payments
- At the GUI level, try to handle invoices in a generic way.
- Display ongoing payments in send tab.
Diffstat:
13 files changed, 563 insertions(+), 359 deletions(-)
diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
@@ -11,11 +11,10 @@ import asyncio
from electrum.bitcoin import TYPE_ADDRESS
from electrum.storage import WalletStorage
from electrum.wallet import Wallet, InternalAddressCorruption
-from electrum.paymentrequest import InvoiceStore
from electrum.util import profiler, InvalidPassword, send_exception_to_crash_reporter
from electrum.plugin import run_hook
from electrum.util import format_satoshis, format_satoshis_plain, format_fee_satoshis
-from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
+from electrum.util import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
from electrum import blockchain
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
from .i18n import _
@@ -201,6 +200,19 @@ class ElectrumWindow(App):
if status == PR_PAID:
self.show_info(_('Payment Received') + '\n' + key)
+ def on_payment_status(self, event, key, status, *args):
+ self.update_tab('send')
+ if status == 'success':
+ self.show_info(_('Payment was sent'))
+ self._trigger_update_history()
+ elif status == 'progress':
+ pass
+ elif status == 'failure':
+ self.show_info(_('Payment failed'))
+ elif status == 'error':
+ e = args[0]
+ self.show_error(_('Error') + '\n' + str(e))
+
def _get_bu(self):
decimal_point = self.electrum_config.get('decimal_point', DECIMAL_POINT_DEFAULT)
try:
@@ -343,19 +355,16 @@ class ElectrumWindow(App):
self.show_error(_('No wallet loaded.'))
return
if pr.verify(self.wallet.contacts):
- key = self.wallet.invoices.add(pr)
- if self.invoices_screen:
- self.invoices_screen.update()
- status = self.wallet.invoices.get_status(key)
- if status == PR_PAID:
+ key = pr.get_id()
+ invoice = self.wallet.get_invoice(key)
+ if invoice and invoice['status'] == PR_PAID:
self.show_error("invoice already paid")
self.send_screen.do_clear()
+ elif pr.has_expired():
+ self.show_error(_('Payment request has expired'))
else:
- if pr.has_expired():
- self.show_error(_('Payment request has expired'))
- else:
- self.switch_to('send')
- self.send_screen.set_request(pr)
+ self.switch_to('send')
+ self.send_screen.set_request(pr)
else:
self.show_error("invoice error:" + pr.error)
self.send_screen.do_clear()
@@ -418,6 +427,19 @@ class ElectrumWindow(App):
self.request_popup.set_status(status)
self.request_popup.open()
+ def show_invoice(self, is_lightning, key):
+ from .uix.dialogs.invoice_dialog import InvoiceDialog
+ invoice = self.wallet.get_invoice(key)
+ if not invoice:
+ return
+ status = invoice['status']
+ if is_lightning:
+ data = invoice['invoice']
+ else:
+ data = key
+ self.invoice_popup = InvoiceDialog('Invoice', data, key)
+ self.invoice_popup.open()
+
def qr_dialog(self, title, data, show_text=False, text_for_clipboard=None):
from .uix.dialogs.qr_dialog import QRDialog
def on_qr_failure():
@@ -519,6 +541,7 @@ class ElectrumWindow(App):
self.network.register_callback(self.on_payment_received, ['payment_received'])
self.network.register_callback(self.on_channels, ['channels'])
self.network.register_callback(self.on_channel, ['channel'])
+ self.network.register_callback(self.on_payment_status, ['payment_status'])
# load wallet
self.load_wallet_by_name(self.electrum_config.get_wallet_path())
# URI passed in config
diff --git a/electrum/gui/kivy/uix/dialogs/invoice_dialog.py b/electrum/gui/kivy/uix/dialogs/invoice_dialog.py
@@ -0,0 +1,97 @@
+from kivy.factory import Factory
+from kivy.lang import Builder
+from kivy.core.clipboard import Clipboard
+from kivy.app import App
+from kivy.clock import Clock
+
+from electrum.gui.kivy.i18n import _
+from electrum.util import pr_tooltips
+
+
+Builder.load_string('''
+<InvoiceDialog@Popup>
+ id: popup
+ title: ''
+ data: ''
+ status: 'unknown'
+ shaded: False
+ show_text: False
+ AnchorLayout:
+ anchor_x: 'center'
+ BoxLayout:
+ orientation: 'vertical'
+ size_hint: 1, 1
+ padding: '10dp'
+ spacing: '10dp'
+ TopLabel:
+ text: root.data
+ TopLabel:
+ text: _('Status') + ': ' + root.status
+ Widget:
+ size_hint: 1, 0.2
+ BoxLayout:
+ size_hint: 1, None
+ height: '48dp'
+ Button:
+ size_hint: 1, None
+ height: '48dp'
+ text: _('Delete')
+ on_release: root.delete_dialog()
+ IconButton:
+ icon: 'atlas://electrum/gui/kivy/theming/light/copy'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release: root.copy_to_clipboard()
+ IconButton:
+ icon: 'atlas://electrum/gui/kivy/theming/light/share'
+ size_hint: 0.5, None
+ height: '48dp'
+ on_release: root.do_share()
+ Button:
+ size_hint: 1, None
+ height: '48dp'
+ text: _('Pay')
+ on_release: root.do_pay()
+''')
+
+class InvoiceDialog(Factory.Popup):
+
+ def __init__(self, title, data, key):
+ Factory.Popup.__init__(self)
+ self.app = App.get_running_app()
+ self.title = title
+ self.data = data
+ self.key = key
+
+ #def on_open(self):
+ # self.ids.qr.set_data(self.data)
+
+ def set_status(self, status):
+ self.status = pr_tooltips[status]
+
+ def on_dismiss(self):
+ self.app.request_popup = None
+
+ def copy_to_clipboard(self):
+ Clipboard.copy(self.data)
+ msg = _('Text copied to clipboard.')
+ Clock.schedule_once(lambda dt: self.app.show_info(msg))
+
+ def do_share(self):
+ self.app.do_share(self.data, _("Share Invoice"))
+ self.dismiss()
+
+ def do_pay(self):
+ invoice = self.app.wallet.get_invoice(self.key)
+ self.app.send_screen.do_pay_invoice(invoice)
+ self.dismiss()
+
+ def delete_dialog(self):
+ from .question import Question
+ def cb(result):
+ if result:
+ self.app.wallet.delete_invoice(self.key)
+ self.dismiss()
+ self.app.send_screen.update()
+ d = Question(_('Delete invoice?'), cb)
+ d.open()
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
@@ -4,7 +4,6 @@ from decimal import Decimal
import re
import threading
import traceback, sys
-from enum import Enum, auto
from kivy.app import App
from kivy.cache import Cache
@@ -23,6 +22,7 @@ from kivy.factory import Factory
from kivy.utils import platform
from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds, Fiat
+from electrum.util import PR_TYPE_ADDRESS, PR_TYPE_LN, PR_TYPE_BIP70
from electrum import bitcoin, constants
from electrum.transaction import TxOutput, Transaction, tx_from_str
from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI
@@ -38,10 +38,6 @@ from .dialogs.lightning_open_channel import LightningOpenChannelDialog
from electrum.gui.kivy.i18n import _
-class Destination(Enum):
- Address = auto()
- PR = auto()
- LN = auto()
class HistoryRecycleView(RecycleView):
pass
@@ -49,6 +45,9 @@ class HistoryRecycleView(RecycleView):
class RequestRecycleView(RecycleView):
pass
+class PaymentRecycleView(RecycleView):
+ pass
+
class CScreen(Factory.Screen):
__events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave')
action_view = ObjectProperty(None)
@@ -119,14 +118,12 @@ class HistoryScreen(CScreen):
super(HistoryScreen, self).__init__(**kwargs)
def show_item(self, obj):
- print(obj)
key = obj.key
tx = self.app.wallet.db.get_transaction(key)
if not tx:
return
self.app.tx_dialog(tx)
-
def get_card(self, tx_item): #tx_hash, tx_mined_status, value, balance):
is_lightning = tx_item.get('lightning', False)
timestamp = tx_item['timestamp']
@@ -192,7 +189,7 @@ class SendScreen(CScreen):
self.screen.message = uri.get('message', '')
self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
self.payment_request = None
- self.screen.destinationtype = Destination.Address
+ self.screen.destinationtype = PR_TYPE_ADDRESS
def set_ln_invoice(self, invoice):
try:
@@ -204,19 +201,47 @@ class SendScreen(CScreen):
self.screen.message = dict(lnaddr.tags).get('d', None)
self.screen.amount = self.app.format_amount_and_units(lnaddr.amount * bitcoin.COIN) if lnaddr.amount else ''
self.payment_request = None
- self.screen.destinationtype = Destination.LN
+ self.screen.destinationtype = PR_TYPE_LN
def update(self):
+ if not self.loaded:
+ return
if self.app.wallet and self.payment_request_queued:
self.set_URI(self.payment_request_queued)
self.payment_request_queued = None
+ _list = self.app.wallet.get_invoices()
+ payments_container = self.screen.ids.payments_container
+ payments_container.data = [self.get_card(item) for item in _list if item['status'] != PR_PAID]
+
+ def show_item(self, obj):
+ self.app.show_invoice(obj.is_lightning, obj.key)
+
+ def get_card(self, item):
+ invoice_type = item['type']
+ if invoice_type == PR_TYPE_LN:
+ key = item['rhash']
+ status = get_request_status(item) # convert to str
+ elif invoice_type == PR_TYPE_BIP70:
+ key = item['id']
+ status = get_request_status(item) # convert to str
+ elif invoice_type == PR_TYPE_ADDRESS:
+ key = item['address']
+ status = get_request_status(item) # convert to str
+ return {
+ 'is_lightning': invoice_type == PR_TYPE_LN,
+ 'screen': self,
+ 'status': status,
+ 'key': key,
+ 'memo': item['message'],
+ 'amount': self.app.format_amount_and_units(item['amount'] or 0),
+ }
def do_clear(self):
self.screen.amount = ''
self.screen.message = ''
self.screen.address = ''
self.payment_request = None
- self.screen.destinationtype = Destination.Address
+ self.screen.destinationtype = PR_TYPE_ADDRESS
def set_request(self, pr):
self.screen.address = pr.get_requestor()
@@ -224,32 +249,10 @@ class SendScreen(CScreen):
self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
self.screen.message = pr.get_memo()
if pr.is_pr():
- self.screen.destinationtype = Destination.PR
- self.payment_request = pr
- else:
- self.screen.destinationtype = Destination.Address
- self.payment_request = None
-
- def save_invoice(self):
- if not self.screen.address:
- return
- if self.screen.destinationtype == Destination.PR:
- # it should be already saved
- return
- # save address as invoice
- from electrum.paymentrequest import make_unsigned_request, PaymentRequest
- req = {'address':self.screen.address, 'memo':self.screen.message}
- amount = self.app.get_amount(self.screen.amount) if self.screen.amount else 0
- req['amount'] = amount
- pr = make_unsigned_request(req).SerializeToString()
- pr = PaymentRequest(pr)
- self.app.wallet.invoices.add(pr)
- #self.app.show_info(_("Invoice saved"))
- if pr.is_pr():
- self.screen.destinationtype = Destination.PR
+ self.screen.destinationtype = PR_TYPE_BIP70
self.payment_request = pr
else:
- self.screen.destinationtype = Destination.Address
+ self.screen.destinationtype = PR_TYPE_ADDRESS
self.payment_request = None
def do_paste(self):
@@ -275,63 +278,87 @@ class SendScreen(CScreen):
self.set_ln_invoice(lower)
else:
self.set_URI(data)
- # save automatically
- self.save_invoice()
- def _do_send_lightning(self):
+ def read_invoice(self):
+ address = str(self.screen.address)
+ if not address:
+ self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request'))
+ return
if not self.screen.amount:
- self.app.show_error(_('Since the invoice contained no amount, you must enter one'))
+ self.app.show_error(_('Please enter an amount'))
return
- invoice = self.screen.address
- amount_sat = self.app.get_amount(self.screen.amount)
- threading.Thread(target=self._lnpay_thread, args=(invoice, amount_sat)).start()
-
- def _lnpay_thread(self, invoice, amount_sat):
- self.do_clear()
- self.app.show_info(_('Payment in progress..'))
try:
- success = self.app.wallet.lnworker.pay(invoice, attempts=10, amount_sat=amount_sat, timeout=60)
- except PaymentFailure as e:
- self.app.show_error(_('Payment failure') + '\n' + str(e))
- return
- if success:
- self.app.show_info(_('Payment was sent'))
- self.app._trigger_update_history()
- else:
- self.app.show_error(_('Payment failed'))
-
- def do_send(self):
- if self.screen.destinationtype == Destination.LN:
- self._do_send_lightning()
+ amount = self.app.get_amount(self.screen.amount)
+ except:
+ self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
return
- elif self.screen.destinationtype == Destination.PR:
- if self.payment_request.has_expired():
- self.app.show_error(_('Payment request has expired'))
- return
- outputs = self.payment_request.get_outputs()
- else:
- address = str(self.screen.address)
- if not address:
- self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request'))
- return
+ message = self.screen.message
+ if self.screen.destinationtype == PR_TYPE_LN:
+ return {
+ 'type': PR_TYPE_LN,
+ 'invoice': address,
+ 'amount': amount,
+ 'message': message,
+ }
+ elif self.screen.destinationtype == PR_TYPE_ADDRESS:
if not bitcoin.is_address(address):
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
return
- try:
- amount = self.app.get_amount(self.screen.amount)
- except:
- self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
+ return {
+ 'type': PR_TYPE_ADDRESS,
+ 'address': address,
+ 'amount': amount,
+ 'message': message,
+ }
+ elif self.screen.destinationtype == PR_TYPE_BIP70:
+ if self.payment_request.has_expired():
+ self.app.show_error(_('Payment request has expired'))
return
+ return self.payment_request.get_dict()
+ else:
+ raise Exception('Unknown invoice type')
+
+ def do_save(self):
+ invoice = self.read_invoice()
+ if not invoice:
+ return
+ self.app.wallet.save_invoice(invoice)
+ self.do_clear()
+ self.update()
+
+ def do_pay(self):
+ invoice = self.read_invoice()
+ if not invoice:
+ return
+ self.app.wallet.save_invoice(invoice)
+ self.do_clear()
+ self.update()
+ self.do_pay_invoice(invoice)
+
+ def do_pay_invoice(self, invoice):
+ if invoice['type'] == PR_TYPE_LN:
+ self._do_send_lightning(invoice['invoice'], invoice['amount'])
+ return
+ elif invoice['type'] == PR_TYPE_ADDRESS:
+ address = invoice['address']
+ amount = invoice['amount']
+ message = invoice['message']
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, address, amount)]
- message = self.screen.message
- amount = sum(map(lambda x:x[2], outputs))
+ elif invoice['type'] == PR_TYPE_BIP70:
+ outputs = invoice['outputs']
+ amount = sum(map(lambda x:x[2], outputs))
+ # onchain payment
if self.app.electrum_config.get('use_rbf'):
- d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send(amount, message, outputs, b))
+ d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send_onchain(amount, message, outputs, b))
d.open()
else:
- self._do_send(amount, message, outputs, False)
+ self._do_send_onchain(amount, message, outputs, False)
+
+ def _do_send_lightning(self, invoice, amount):
+ attempts = 10
+ threading.Thread(target=self.app.wallet.lnworker.pay, args=(invoice, amount, attempts)).start()
- def _do_send(self, amount, message, outputs, rbf):
+ def _do_send_onchain(self, amount, message, outputs, rbf):
# make unsigned transaction
config = self.app.electrum_config
coins = self.app.wallet.get_spendable_coins(None, config)
@@ -447,7 +474,7 @@ class ReceiveScreen(CScreen):
self.app.show_request(lightning, key)
def get_card(self, req):
- is_lightning = req.get('lightning', False)
+ is_lightning = req.get('type') == PR_TYPE_LN
if not is_lightning:
address = req['address']
key = address
diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
@@ -1,10 +1,66 @@
#:import _ electrum.gui.kivy.i18n._
-#:import Destination electrum.gui.kivy.uix.screens.Destination
+#:import Factory kivy.factory.Factory
+#:import PR_TYPE_ADDRESS electrum.util.PR_TYPE_ADDRESS
+#:import PR_TYPE_LN electrum.util.PR_TYPE_LN
+#:import PR_TYPE_BIP70 electrum.util.PR_TYPE_BIP70
#:import Decimal decimal.Decimal
#:set btc_symbol chr(171)
#:set mbtc_symbol chr(187)
#:set font_light 'electrum/gui/kivy/data/fonts/Roboto-Condensed.ttf'
+<PaymentLabel@Label>
+ #color: .305, .309, .309, 1
+ text_size: self.width, None
+ halign: 'left'
+ valign: 'top'
+
+<PaymentItem@CardItem>
+ key: ''
+ memo: ''
+ amount: ''
+ status: ''
+ date: ''
+ BoxLayout:
+ spacing: '8dp'
+ height: '32dp'
+ orientation: 'vertical'
+ Widget
+ PaymentLabel:
+ text: root.memo
+ shorten: True
+ shorten_from: 'right'
+ Widget
+ PaymentLabel:
+ text: root.key
+ color: .699, .699, .699, 1
+ font_size: '13sp'
+ shorten: True
+ Widget
+ BoxLayout:
+ spacing: '8dp'
+ height: '32dp'
+ orientation: 'vertical'
+ Widget
+ PaymentLabel:
+ text: root.amount
+ halign: 'right'
+ font_size: '15sp'
+ Widget
+ PaymentLabel:
+ text: root.status
+ halign: 'right'
+ font_size: '13sp'
+ color: .699, .699, .699, 1
+ Widget
+
+<PaymentRecycleView>:
+ viewclass: 'PaymentItem'
+ RecycleBoxLayout:
+ default_size: None, dp(56)
+ default_size_hint: 1, None
+ size_hint: 1, None
+ height: self.minimum_height
+ orientation: 'vertical'
SendScreen:
id: s
@@ -12,7 +68,7 @@ SendScreen:
address: ''
amount: ''
message: ''
- destinationtype: Destination.Address
+ destinationtype: PR_TYPE_ADDRESS
BoxLayout
padding: '12dp', '12dp', '12dp', '12dp'
spacing: '12dp'
@@ -26,7 +82,7 @@ SendScreen:
height: blue_bottom.item_height
spacing: '5dp'
Image:
- source: 'atlas://electrum/gui/kivy/theming/light/globe'
+ source: 'atlas://electrum/gui/kivy/theming/light/globe' if root.destinationtype != PR_TYPE_LN else 'atlas://electrum/gui/kivy/theming/light/lightning'
size_hint: None, None
size: '22dp', '22dp'
pos_hint: {'center_y': .5}
@@ -37,7 +93,7 @@ SendScreen:
on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the recipient address using the Paste button, or use the camera to scan a QR code.')))
#on_release: Clock.schedule_once(lambda dt: app.popup_dialog('contacts'))
CardSeparator:
- opacity: int(root.destinationtype == Destination.Address)
+ opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
color: blue_bottom.foreground_color
BoxLayout:
size_hint: 1, None
@@ -53,10 +109,10 @@ SendScreen:
id: amount_e
default_text: _('Amount')
text: s.amount if s.amount else _('Amount')
- disabled: root.destinationtype == Destination.PR or root.destinationtype == Destination.LN and not s.amount
+ disabled: root.destinationtype == PR_TYPE_BIP70 or root.destinationtype == PR_TYPE_LN and not s.amount
on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, True))
CardSeparator:
- opacity: int(root.destinationtype == Destination.Address)
+ opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
color: blue_bottom.foreground_color
BoxLayout:
id: message_selection
@@ -70,37 +126,40 @@ SendScreen:
pos_hint: {'center_y': .5}
BlueButton:
id: description
- text: s.message if s.message else ({Destination.LN: _('No description'), Destination.Address: _('Description'), Destination.PR: _('No Description')}[root.destinationtype])
- disabled: root.destinationtype != Destination.Address
+ text: s.message if s.message else ({PR_TYPE_LN: _('No description'), PR_TYPE_ADDRESS: _('Description'), PR_TYPE_BIP70: _('No Description')}[root.destinationtype])
+ disabled: root.destinationtype != PR_TYPE_ADDRESS
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
CardSeparator:
- opacity: int(root.destinationtype == Destination.Address)
+ opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
color: blue_bottom.foreground_color
BoxLayout:
size_hint: 1, None
- height: blue_bottom.item_height if root.destinationtype != Destination.LN else 0
+ height: blue_bottom.item_height
spacing: '5dp'
Image:
source: 'atlas://electrum/gui/kivy/theming/light/star_big_inactive'
- opacity: 0.7 if root.destinationtype != Destination.LN else 0
size_hint: None, None
size: '22dp', '22dp'
pos_hint: {'center_y': .5}
BlueButton:
id: fee_e
default_text: _('Fee')
- text: app.fee_status if root.destinationtype != Destination.LN else ''
- on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if root.destinationtype != Destination.LN else None
+ text: app.fee_status if root.destinationtype != PR_TYPE_LN else ''
+ on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if root.destinationtype != PR_TYPE_LN else None
BoxLayout:
size_hint: 1, None
height: '48dp'
IconButton:
size_hint: 0.5, 1
+ on_release: s.parent.do_save()
+ icon: 'atlas://electrum/gui/kivy/theming/light/save'
+ IconButton:
+ size_hint: 0.5, 1
icon: 'atlas://electrum/gui/kivy/theming/light/copy'
on_release: s.parent.do_paste()
IconButton:
id: qr
- size_hint: 0.5, 1
+ size_hint: 1, 1
on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=app.on_qr))
icon: 'atlas://electrum/gui/kivy/theming/light/camera'
Button:
@@ -110,19 +169,10 @@ SendScreen:
Button:
text: _('Pay')
size_hint: 1, 1
- on_release: s.parent.do_send()
+ on_release: s.parent.do_pay()
Widget:
- size_hint: 1, 1
- #BoxLayout:
- # size_hint: 1, None
- # height: '48dp'
- #IconButton:
- # size_hint: 0.5, 1
- # on_release: s.parent.do_save()
- # icon: 'atlas://electrum/gui/kivy/theming/light/save'
- #IconButton:
- # size_hint: 0.5, 1
- # icon: 'atlas://electrum/gui/kivy/theming/light/list'
- # on_release: Clock.schedule_once(lambda dt: app.invoices_dialog(s))
- #Widget:
- # size_hint: 2.5, 1
+ size_hint: 1, 0.1
+ PaymentRecycleView:
+ id: payments_container
+ scroll_type: ['bars', 'content']
+ bar_width: '25dp'
diff --git a/electrum/gui/qt/history_list.py b/electrum/gui/qt/history_list.py
@@ -196,9 +196,9 @@ class HistoryModel(QAbstractItemModel, Logger):
elif col != HistoryColumns.STATUS and role == Qt.FontRole:
monospace_font = QFont(MONOSPACE_FONT)
return QVariant(monospace_font)
- elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole and not is_lightning\
- and self.parent.wallet.invoices.paid.get(tx_hash):
- return QVariant(read_QIcon("seal"))
+ #elif col == HistoryColumns.DESCRIPTION and role == Qt.DecorationRole and not is_lightning\
+ # and self.parent.wallet.invoices.paid.get(tx_hash):
+ # return QVariant(read_QIcon("seal"))
elif col in (HistoryColumns.DESCRIPTION, HistoryColumns.AMOUNT) \
and role == Qt.ForegroundRole and not is_lightning and tx_item['value'].value < 0:
red_brush = QBrush(QColor("#BC1E1E"))
diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
@@ -30,22 +30,20 @@ from PyQt5.QtGui import QStandardItemModel, QStandardItem, QFont
from PyQt5.QtWidgets import QHeaderView, QMenu
from electrum.i18n import _
-from electrum.util import format_time, pr_tooltips, PR_UNPAID
+from electrum.util import format_time, PR_UNPAID, PR_PAID, get_request_status
+from electrum.util import PR_TYPE_ADDRESS, PR_TYPE_LN, PR_TYPE_BIP70
from electrum.lnutil import lndecode, RECEIVED
from electrum.bitcoin import COIN
from electrum import constants
-from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT, PR_UNPAID,
+from .util import (MyTreeView, read_QIcon, MONOSPACE_FONT,
import_meta_gui, export_meta_gui, pr_icons)
-REQUEST_TYPE_BITCOIN = 0
-REQUEST_TYPE_LN = 1
ROLE_REQUEST_TYPE = Qt.UserRole
ROLE_REQUEST_ID = Qt.UserRole + 1
-from electrum.paymentrequest import PR_PAID
class InvoiceList(MyTreeView):
@@ -56,7 +54,7 @@ class InvoiceList(MyTreeView):
STATUS = 3
headers = {
- Columns.DATE: _('Expires'),
+ Columns.DATE: _('Date'),
Columns.DESCRIPTION: _('Description'),
Columns.AMOUNT: _('Amount'),
Columns.STATUS: _('Status'),
@@ -72,48 +70,38 @@ class InvoiceList(MyTreeView):
self.update()
def update(self):
- inv_list = self.parent.invoices.unpaid_invoices()
+ _list = self.parent.wallet.get_invoices()
self.model().clear()
self.update_headers(self.__class__.headers)
- for idx, pr in enumerate(inv_list):
- key = pr.get_id()
- status = self.parent.invoices.get_status(key)
- if status is None:
- continue
- requestor = pr.get_requestor()
- exp = pr.get_time()
- date_str = format_time(exp) if exp else _('Never')
- labels = [date_str, '[%s] '%requestor + pr.memo, self.parent.format_amount(pr.get_amount(), whitespaces=True), pr_tooltips.get(status,'')]
+ for idx, item in enumerate(_list):
+ invoice_type = item['type']
+ if invoice_type == PR_TYPE_LN:
+ key = item['rhash']
+ icon_name = 'lightning.png'
+ elif invoice_type == PR_TYPE_ADDRESS:
+ key = item['address']
+ icon_name = 'bitcoin.png'
+ elif invoice_type == PR_TYPE_BIP70:
+ key = item['id']
+ icon_name = 'seal.png'
+ else:
+ raise Exception('Unsupported type')
+ status = item['status']
+ status_str = get_request_status(item) # convert to str
+ message = item['message']
+ amount = item['amount']
+ timestamp = item.get('time', 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]
items = [QStandardItem(e) for e in labels]
self.set_editability(items)
- items[self.Columns.DATE].setIcon(read_QIcon('bitcoin.png'))
+ 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(REQUEST_TYPE_BITCOIN, role=ROLE_REQUEST_TYPE)
+ items[self.Columns.DATE].setData(invoice_type, role=ROLE_REQUEST_TYPE)
self.model().insertRow(idx, items)
- lnworker = self.parent.wallet.lnworker
- items = list(lnworker.invoices.items()) if lnworker else []
- for key, (invoice, direction, is_paid) in items:
- if direction == RECEIVED:
- continue
- status = lnworker.get_invoice_status(key)
- if status == PR_PAID:
- continue
- lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
- amount_sat = lnaddr.amount*COIN if lnaddr.amount else None
- amount_str = self.parent.format_amount(amount_sat) if amount_sat else ''
- description = lnaddr.get_description()
- date_str = format_time(lnaddr.date)
- labels = [date_str, description, amount_str, pr_tooltips.get(status,'')]
- items = [QStandardItem(e) for e in labels]
- self.set_editability(items)
- items[self.Columns.DATE].setIcon(read_QIcon('lightning.png'))
- 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(REQUEST_TYPE_LN, role=ROLE_REQUEST_TYPE)
- self.model().insertRow(self.model().rowCount(), items)
-
self.selectionModel().select(self.model().index(0,0), QItemSelectionModel.SelectCurrent)
# sort requests by date
self.model().sort(self.Columns.DATE)
@@ -138,7 +126,7 @@ class InvoiceList(MyTreeView):
return
key = item_col0.data(ROLE_REQUEST_ID)
request_type = item_col0.data(ROLE_REQUEST_TYPE)
- assert request_type in [REQUEST_TYPE_BITCOIN, REQUEST_TYPE_LN]
+ assert request_type in [PR_TYPE_ADDRESS, PR_TYPE_BIP70, PR_TYPE_LN]
column = idx.column()
column_title = self.model().horizontalHeaderItem(column).text()
column_data = item.text()
@@ -147,17 +135,17 @@ class InvoiceList(MyTreeView):
if column == self.Columns.AMOUNT:
column_data = column_data.strip()
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
- if request_type == REQUEST_TYPE_BITCOIN:
+ if request_type in [PR_TYPE_BIP70, PR_TYPE_ADDRESS]:
self.create_menu_bitcoin_payreq(menu, key)
- elif request_type == REQUEST_TYPE_LN:
+ elif request_type == PR_TYPE_LN:
self.create_menu_ln_payreq(menu, key)
menu.exec_(self.viewport().mapToGlobal(position))
def create_menu_bitcoin_payreq(self, menu, payreq_key):
- status = self.parent.invoices.get_status(payreq_key)
+ #status = self.parent.wallet.get_invoice_status(payreq_key)
menu.addAction(_("Details"), lambda: self.parent.show_invoice(payreq_key))
- if status == PR_UNPAID:
- menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(payreq_key))
+ #if status == PR_UNPAID:
+ menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(payreq_key))
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(payreq_key))
def create_menu_ln_payreq(self, menu, payreq_key):
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -120,7 +120,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
payment_request_ok_signal = pyqtSignal()
payment_request_error_signal = pyqtSignal()
network_signal = pyqtSignal(str, object)
- ln_payment_attempt_signal = pyqtSignal(str)
+ #ln_payment_attempt_signal = pyqtSignal(str)
alias_received_signal = pyqtSignal()
computing_privkeys_signal = pyqtSignal()
show_privkeys_signal = pyqtSignal()
@@ -138,7 +138,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
assert wallet, "no wallet"
self.wallet = wallet
self.fx = gui_object.daemon.fx # type: FxThread
- self.invoices = wallet.invoices
+ #self.invoices = wallet.invoices
self.contacts = wallet.contacts
self.tray = gui_object.tray
self.app = gui_object.app
@@ -225,7 +225,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
'new_transaction', 'status',
'banner', 'verified', 'fee', 'fee_histogram', 'on_quotes',
'on_history', 'channel', 'channels', 'payment_received',
- 'ln_payment_completed', 'ln_payment_attempt']
+ 'payment_status']
# To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be
# methods of this class only, and specifically not be
@@ -374,14 +374,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
elif event == 'channel':
self.channels_list.update_single_row.emit(*args)
self.update_status()
- elif event == 'ln_payment_attempt':
- msg = _('Sending lightning payment') + '... (%d/%d)'%(args[0]+1, LN_NUM_PAYMENT_ATTEMPTS)
- self.ln_payment_attempt_signal.emit(msg)
- elif event == 'ln_payment_completed':
- # FIXME it is really inefficient to force update the whole GUI
- # just for a single LN payment. individual rows in lists should be updated instead.
- # consider: history tab, invoice list, request list
- self.need_update.set()
+ elif event == 'payment_status':
+ self.on_payment_status(*args)
elif event == 'status':
self.update_status()
elif event == 'banner':
@@ -1671,33 +1665,32 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.do_send(preview = True)
def pay_lightning_invoice(self, invoice):
- amount = self.amount_e.get_amount()
- def on_success(result):
- self.logger.info(f'ln payment success. {result}')
- self.show_error(_('Payment succeeded'))
- self.do_clear()
- def on_failure(exc_info):
- type_, e, traceback = exc_info
- if isinstance(e, PaymentFailure):
- self.show_error(_('Payment failed. {}').format(e))
- elif isinstance(e, InvoiceError):
- self.show_error(_('InvoiceError: {}').format(e))
- else:
- raise e
+ amount_sat = self.amount_e.get_amount()
+ attempts = LN_NUM_PAYMENT_ATTEMPTS
def task():
- success = self.wallet.lnworker.pay(invoice, attempts=LN_NUM_PAYMENT_ATTEMPTS, amount_sat=amount, timeout=60)
- if not success:
- raise PaymentFailure(f'Failed after {LN_NUM_PAYMENT_ATTEMPTS} attempts')
+ self.wallet.lnworker.pay(invoice, amount_sat, attempts)
+ self.do_clear()
+ self.wallet.thread.add(task)
+ self.invoice_list.update()
- msg = _('Sending lightning payment...')
- d = WaitingDialog(self, msg, task, on_success, on_failure)
- self.ln_payment_attempt_signal.connect(d.update)
+ def on_payment_status(self, key, status, *args):
+ # todo: check that key is in this wallet's invoice list
+ self.invoice_list.update()
+ if status == 'success':
+ self.show_message(_('Payment succeeded'))
+ self.need_update.set()
+ elif status == 'progress':
+ print('on_payment_status', key, status, args)
+ elif status == 'failure':
+ self.show_info(_('Payment failed'))
+ elif status == 'error':
+ e = args[0]
+ self.show_error(_('Error') + '\n' + str(e))
def do_send(self, preview = False):
if self.payto_e.is_lightning:
self.pay_lightning_invoice(self.payto_e.lightning_invoice)
return
- #
if run_hook('abort_send', self):
return
outputs, fee_estimator, tx_desc, coins = self.read_send_tab()
@@ -1817,8 +1810,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
else:
status, msg = True, tx.txid()
if pr and status is True:
- self.invoices.set_paid(pr, tx.txid())
- self.invoices.save()
+ key = pr.get_id()
+ self.wallet.set_invoice_paid(key, tx.txid())
self.payment_request = None
refund_address = self.wallet.get_receiving_address()
coro = pr.send_payment_and_receive_paymentack(str(tx), refund_address)
@@ -1889,17 +1882,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return True
def delete_invoice(self, key):
- self.invoices.remove(key)
+ self.wallet.delete_invoice(key)
self.invoice_list.update()
def payment_request_ok(self):
pr = self.payment_request
if not pr:
return
- key = self.invoices.add(pr)
- status = self.invoices.get_status(key)
- self.invoice_list.update()
- if status == PR_PAID:
+ key = pr.get_id()
+ invoice = self.wallet.get_invoice(key)
+ if invoice and invoice['status'] == PR_PAID:
self.show_message("invoice already paid")
self.do_clear()
self.payment_request = None
@@ -2106,7 +2098,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.update_completions()
def show_invoice(self, key):
- pr = self.invoices.get(key)
+ pr = self.wallet.get_invoice(key)
if pr is None:
self.show_error('Cannot find payment request in wallet.')
return
@@ -2143,7 +2135,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
exportButton = EnterButton(_('Save'), do_export)
def do_delete():
if self.question(_('Delete invoice?')):
- self.invoices.remove(key)
+ self.wallet.delete_invoices(key)
self.history_list.update()
self.invoice_list.update()
d.close()
@@ -2152,7 +2144,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
d.exec_()
def do_pay_invoice(self, key):
- pr = self.invoices.get(key)
+ pr = self.wallet.get_invoice(key)
self.payment_request = pr
self.prepare_for_payment_request()
pr.error = None # this forces verify() to re-run
diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
@@ -31,6 +31,7 @@ from PyQt5.QtCore import Qt, QItemSelectionModel
from electrum.i18n import _
from electrum.util import format_time, age, get_request_status
+from electrum.util import PR_TYPE_ADDRESS, PR_TYPE_LN, PR_TYPE_BIP70
from electrum.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, pr_tooltips
from electrum.lnutil import SENT, RECEIVED
from electrum.plugin import run_hook
@@ -104,9 +105,10 @@ class RequestList(MyTreeView):
is_lightning = date_item.data(ROLE_REQUEST_TYPE) == REQUEST_TYPE_LN
req = self.wallet.get_request(key, is_lightning)
if req:
+ status = req['status']
status_str = get_request_status(req)
status_item.setText(status_str)
- status_item.setIcon(read_QIcon(pr_icons.get(req['status'])))
+ status_item.setIcon(read_QIcon(pr_icons.get(status)))
def update(self):
self.wallet = self.parent.wallet
@@ -118,10 +120,11 @@ class RequestList(MyTreeView):
status = req.get('status')
if status == PR_PAID:
continue
- request_type = REQUEST_TYPE_LN if req.get('lightning', False) else REQUEST_TYPE_BITCOIN
+ is_lightning = req['type'] == PR_TYPE_LN
+ request_type = REQUEST_TYPE_LN if is_lightning else REQUEST_TYPE_BITCOIN
timestamp = req.get('time', 0)
amount = req.get('amount')
- message = req['memo']
+ message = req['message'] if is_lightning else req['memo']
date = format_time(timestamp)
amount_str = self.parent.format_amount(amount) if amount else ""
status_str = get_request_status(req)
diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
@@ -31,7 +31,7 @@ from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Itera
import time
from . import ecc
-from .util import bfh, bh2u
+from .util import bfh, bh2u, PR_PAID, PR_FAILED
from .bitcoin import TYPE_SCRIPT, TYPE_ADDRESS
from .bitcoin import redeem_script_to_address
from .crypto import sha256, sha256d
@@ -165,8 +165,11 @@ class Channel(Logger):
log = self.hm.log[subject]
for htlc_id, htlc in log.get('adds', {}).items():
if htlc_id in log.get('fails',{}):
- continue
- status = 'settled' if htlc_id in log.get('settles',{}) else 'inflight'
+ status = 'failed'
+ elif htlc_id in log.get('settles',{}):
+ status = 'settled'
+ else:
+ status = 'inflight'
direction = SENT if subject is LOCAL else RECEIVED
rhash = bh2u(htlc.payment_hash)
out[rhash] = (self.channel_id, htlc, direction, status)
@@ -563,7 +566,7 @@ class Channel(Logger):
assert htlc_id not in log['settles']
self.hm.send_settle(htlc_id)
if self.lnworker:
- self.lnworker.set_paid(htlc.payment_hash)
+ self.lnworker.set_invoice_status(htlc.payment_hash, PR_PAID)
def receive_htlc_settle(self, preimage, htlc_id):
self.logger.info("receive_htlc_settle")
@@ -574,7 +577,7 @@ class Channel(Logger):
self.hm.recv_settle(htlc_id)
if self.lnworker:
self.lnworker.save_preimage(htlc.payment_hash, preimage)
- self.lnworker.set_paid(htlc.payment_hash)
+ self.lnworker.set_invoice_status(htlc.payment_hash, PR_PAID)
def fail_htlc(self, htlc_id):
self.logger.info("fail_htlc")
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -21,7 +21,8 @@ import dns.exception
from . import constants
from . import keystore
-from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
+from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, profiler
+from .util import PR_TYPE_LN
from .keystore import BIP32_KeyStore
from .bitcoin import COIN
from .transaction import Transaction
@@ -396,14 +397,12 @@ class LNWallet(LNWorker):
def get_invoice_status(self, key):
if key not in self.invoices:
return PR_UNKNOWN
- invoice, direction, is_paid = self.invoices[key]
+ invoice, direction, status = self.invoices[key]
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
- if is_paid:
- return PR_PAID
- elif lnaddr.is_expired():
+ if status == PR_UNPAID and lnaddr.is_expired():
return PR_EXPIRED
else:
- return PR_UNPAID
+ return status
def get_payments(self):
# return one item per payment_hash
@@ -415,11 +414,35 @@ class LNWallet(LNWorker):
out[k].append(v)
return out
+ def get_unsettled_payments(self):
+ out = []
+ for payment_hash, plist in self.get_payments().items():
+ if len(plist) != 1:
+ continue
+ chan_id, htlc, _direction, status = plist[0]
+ if _direction != SENT:
+ continue
+ if status == 'settled':
+ continue
+ amount = htlc.amount_msat//1000
+ item = {
+ 'is_lightning': True,
+ 'status': status,
+ 'key': payment_hash,
+ 'amount': amount,
+ 'timestamp': htlc.timestamp,
+ 'label': self.wallet.get_label(payment_hash)
+ }
+ out.append(item)
+ return out
+
def get_history(self):
out = []
for payment_hash, plist in self.get_payments().items():
if len(plist) == 1:
chan_id, htlc, _direction, status = plist[0]
+ if status != 'settled':
+ continue
direction = 'sent' if _direction == SENT else 'received'
amount_msat= int(_direction) * htlc.amount_msat
timestamp = htlc.timestamp
@@ -751,17 +774,23 @@ class LNWallet(LNWorker):
raise Exception(_("open_channel timed out"))
return chan
- def pay(self, invoice, attempts=1, amount_sat=None, timeout=10):
+ def pay(self, invoice, amount_sat=None, attempts=1):
"""
Can be called from other threads
- Raises exception after timeout
"""
- coro = self._pay(invoice, attempts, amount_sat)
+ addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
+ key = bh2u(addr.paymenthash)
+ coro = self._pay(invoice, amount_sat, attempts)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
try:
- return fut.result(timeout=timeout)
- except concurrent.futures.TimeoutError:
- raise PaymentFailure(_("Payment timed out"))
+ success = fut.result()
+ except Exception as e:
+ self.network.trigger_callback('payment_status', key, 'error', e)
+ return
+ if success:
+ self.network.trigger_callback('payment_status', key, 'success')
+ else:
+ self.network.trigger_callback('payment_status', key, 'failure')
def get_channel_by_short_id(self, short_channel_id):
with self.lock:
@@ -770,20 +799,22 @@ class LNWallet(LNWorker):
return chan
@log_exceptions
- async def _pay(self, invoice, attempts=1, amount_sat=None):
+ async def _pay(self, invoice, amount_sat=None, attempts=1):
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
- status = self.get_invoice_status(bh2u(addr.paymenthash))
+ key = bh2u(addr.paymenthash)
+ status = self.get_invoice_status(key)
if status == PR_PAID:
+ # fixme: use lightning_preimaages, because invoices are not permanently stored
raise PaymentFailure(_("This invoice has been paid already"))
self._check_invoice(invoice, amount_sat)
- self.save_invoice(addr.paymenthash, invoice, SENT, is_paid=False)
- self.wallet.set_label(bh2u(addr.paymenthash), addr.get_description())
+ self.save_invoice(addr.paymenthash, invoice, SENT, PR_INFLIGHT)
+ self.wallet.set_label(key, addr.get_description())
for i in range(attempts):
route = await self._create_route_from_invoice(decoded_invoice=addr)
if not self.get_channel_by_short_id(route[0].short_channel_id):
scid = format_short_channel_id(route[0].short_channel_id)
raise Exception(f"Got route with unknown first channel: {scid}")
- self.network.trigger_callback('ln_payment_attempt', i)
+ self.network.trigger_callback('payment_status', key, 'progress', i)
if await self._pay_to_route(route, addr, invoice):
return True
return False
@@ -895,7 +926,7 @@ class LNWallet(LNWorker):
('x', expiry)]
+ routing_hints),
self.node_keypair.privkey)
- self.save_invoice(payment_hash, invoice, RECEIVED, is_paid=False)
+ self.save_invoice(payment_hash, invoice, RECEIVED, PR_UNPAID)
self.save_preimage(payment_hash, payment_preimage)
self.wallet.set_label(bh2u(payment_hash), message)
return payment_hash
@@ -915,20 +946,24 @@ class LNWallet(LNWorker):
except KeyError as e:
raise UnknownPaymentHash(payment_hash) from e
- def save_invoice(self, payment_hash:bytes, invoice, direction, *, is_paid=False):
+ def save_new_invoice(self, invoice):
+ addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
+ self.save_invoice(addr.paymenthash, invoice, SENT, PR_UNPAID)
+
+ def save_invoice(self, payment_hash:bytes, invoice, direction, status):
key = bh2u(payment_hash)
- self.invoices[key] = invoice, direction, is_paid
+ self.invoices[key] = invoice, direction, status
self.storage.put('lightning_invoices', self.invoices)
self.storage.write()
- def set_paid(self, payment_hash):
+ def set_invoice_status(self, payment_hash, status):
key = bh2u(payment_hash)
if key not in self.invoices:
# if we are forwarding
return
invoice, direction, _ = self.invoices[key]
- self.save_invoice(payment_hash, invoice, direction, is_paid=True)
- if direction == RECEIVED:
+ self.save_invoice(payment_hash, invoice, direction, status)
+ if direction == RECEIVED and status == PR_PAID:
self.network.trigger_callback('payment_received', self.wallet, key, PR_PAID)
def get_invoice(self, payment_hash: bytes) -> LnAddr:
@@ -939,6 +974,9 @@ class LNWallet(LNWorker):
raise UnknownPaymentHash(payment_hash) from e
def get_request(self, key):
+ if key not in self.invoices:
+ return
+ # todo: parse invoices when saving
invoice, direction, is_paid = self.invoices[key]
status = self.get_invoice_status(key)
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
@@ -946,23 +984,32 @@ class LNWallet(LNWorker):
description = lnaddr.get_description()
timestamp = lnaddr.date
return {
- 'lightning':True,
- 'status':status,
- 'amount':amount_sat,
- 'time':timestamp,
- 'exp':lnaddr.get_expiry(),
- 'memo':description,
- 'rhash':key,
+ 'type': PR_TYPE_LN,
+ 'status': status,
+ 'amount': amount_sat,
+ 'time': timestamp,
+ 'exp': lnaddr.get_expiry(),
+ 'message': description,
+ 'rhash': key,
'invoice': invoice
}
+ @profiler
def get_invoices(self):
- items = self.invoices.items()
+ # invoices = outgoing
out = []
- for key, (invoice, direction, is_paid) in items:
- if direction == SENT:
- continue
- out.append(self.get_request(key))
+ for key, (invoice, direction, status) in self.invoices.items():
+ if direction == SENT and status != PR_PAID:
+ out.append(self.get_request(key))
+ return out
+
+ @profiler
+ def get_requests(self):
+ # requests = incoming
+ out = []
+ for key, (invoice, direction, status) in self.invoices.items():
+ if direction == RECEIVED and status != PR_PAID:
+ out.append(self.get_request(key))
return out
async def _calc_routing_hints_for_invoice(self, amount_sat):
diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
@@ -41,6 +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 .util import PR_TYPE_BIP70
from .crypto import sha256
from .bitcoin import TYPE_ADDRESS
from .transaction import TxOutput
@@ -270,13 +271,15 @@ class PaymentRequest:
def get_dict(self):
return {
+ 'type': PR_TYPE_BIP70,
+ 'id': self.get_id(),
'requestor': self.get_requestor(),
- 'memo':self.get_memo(),
- 'exp': self.get_expiration_date(),
+ 'message': self.get_memo(),
+ 'time': self.get_time(),
+ 'exp': self.get_expiration_date() - self.get_time(),
'amount': self.get_amount(),
- 'signature': self.get_verify_status(),
- 'txid': self.tx,
- 'outputs': self.get_outputs()
+ 'outputs': self.get_outputs(),
+ 'hex': self.raw.hex(),
}
def get_id(self):
@@ -475,94 +478,3 @@ def make_request(config, req):
if key_path and cert_path:
sign_request_with_x509(pr, key_path, cert_path)
return pr
-
-
-
-class InvoiceStore(Logger):
-
- def __init__(self, storage):
- Logger.__init__(self)
- self.storage = storage
- self.invoices = {}
- self.paid = {}
- d = self.storage.get('invoices', {})
- self.load(d)
-
- def set_paid(self, pr, txid):
- pr.tx = txid
- pr_id = pr.get_id()
- self.paid[txid] = pr_id
- if pr_id not in self.invoices:
- # in case the user had deleted it previously
- self.add(pr)
-
- def load(self, d):
- for k, v in d.items():
- try:
- pr = PaymentRequest(bfh(v.get('hex')))
- pr.tx = v.get('txid')
- pr.requestor = v.get('requestor')
- self.invoices[k] = pr
- if pr.tx:
- self.paid[pr.tx] = k
- except:
- continue
-
- def import_file(self, path):
- def validate(data):
- return data # TODO
- import_meta(path, validate, self.on_import)
-
- def on_import(self, data):
- self.load(data)
- self.save()
-
- def export_file(self, filename):
- export_meta(self.dump(), filename)
-
- def dump(self):
- d = {}
- for k, pr in self.invoices.items():
- d[k] = {
- 'hex': bh2u(pr.raw),
- 'requestor': pr.requestor,
- 'txid': pr.tx
- }
- return d
-
- def save(self):
- self.storage.put('invoices', self.dump())
-
- def get_status(self, key):
- pr = self.get(key)
- if pr is None:
- self.logger.info(f"get_status() can't find pr for {key}")
- return
- if pr.tx is not None:
- return PR_PAID
- if pr.has_expired():
- return PR_EXPIRED
- return PR_UNPAID
-
- def add(self, pr):
- key = pr.get_id()
- self.invoices[key] = pr
- self.save()
- return key
-
- def remove(self, key):
- self.invoices.pop(key)
- self.save()
-
- def get(self, k):
- return self.invoices.get(k)
-
- def sorted_list(self):
- # sort
- return self.invoices.values()
-
- def unpaid_invoices(self):
- return [self.invoices[k] for k in
- filter(lambda x: self.get_status(x) not in (PR_PAID, None),
- self.invoices.keys())
- ]
diff --git a/electrum/util.py b/electrum/util.py
@@ -73,19 +73,26 @@ base_units_list = ['BTC', 'mBTC', 'bits', 'sat'] # list(dict) does not guarante
DECIMAL_POINT_DEFAULT = 5 # mBTC
+# types of payment requests
+PR_TYPE_ADDRESS = 0
+PR_TYPE_BIP70= 1
+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_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_tooltips = {
PR_UNPAID:_('Pending'),
PR_PAID:_('Paid'),
PR_UNKNOWN:_('Unknown'),
PR_EXPIRED:_('Expired'),
- PR_INFLIGHT:_('Paid (unconfirmed)')
+ PR_INFLIGHT:_('In progress'),
+ PR_FAILED:_('Failed'),
}
pr_expiration_values = {
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -47,6 +47,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
from .util import age
+from .util import PR_TYPE_ADDRESS, PR_TYPE_BIP70, PR_TYPE_LN
from .simple_config import get_config
from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
is_minikey, relayfee, dust_threshold)
@@ -60,14 +61,14 @@ from .transaction import Transaction, TxOutput, TxOutputHwInfo
from .plugin import run_hook
from .address_synchronizer import (AddressSynchronizer, TX_HEIGHT_LOCAL,
TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE)
-from .paymentrequest import (PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT,
- InvoiceStore)
+from .util import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED, PR_INFLIGHT
from .contacts import Contacts
from .interface import NetworkException
from .ecc_fast import is_using_fast_ecc
from .mnemonic import Mnemonic
from .logging import get_logger
from .lnworker import LNWallet
+from .paymentrequest import PaymentRequest
if TYPE_CHECKING:
from .network import Network
@@ -225,6 +226,7 @@ class Abstract_Wallet(AddressSynchronizer):
self.frozen_coins = set(storage.get('frozen_coins', [])) # set of txid:vout strings
self.fiat_value = storage.get('fiat_value', {})
self.receive_requests = storage.get('payment_requests', {})
+ self.invoices = storage.get('invoices', {})
self.calc_unused_change_addresses()
@@ -232,8 +234,7 @@ class Abstract_Wallet(AddressSynchronizer):
if self.storage.get('wallet_type') is None:
self.storage.put('wallet_type', self.wallet_type)
- # invoices and contacts
- self.invoices = InvoiceStore(self.storage)
+ # contacts
self.contacts = Contacts(self.storage)
self._coin_price_cache = {}
self.lnworker = LNWallet(self) if get_config().get('lightning') else None
@@ -498,6 +499,51 @@ class Abstract_Wallet(AddressSynchronizer):
'txpos_in_block': tx_mined_status.txpos,
}
+ def save_invoice(self, invoice):
+ invoice_type = invoice['type']
+ if invoice_type == PR_TYPE_LN:
+ self.lnworker.save_new_invoice(invoice['invoice'])
+ else:
+ if invoice_type == PR_TYPE_ADDRESS:
+ key = invoice['address']
+ invoice['time'] = int(time.time())
+ elif invoice_type == PR_TYPE_BIP70:
+ key = invoice['id']
+ invoice['txid'] = None
+ else:
+ raise Exception('Unsupported invoice type')
+ self.invoices[key] = invoice
+ self.storage.put('invoices', self.invoices)
+ self.storage.write()
+
+ def get_invoices(self):
+ out = [self.get_invoice(key) for key in self.invoices.keys()]
+ out = [x for x in out if x and x.get('status') != PR_PAID]
+ if self.lnworker:
+ out += self.lnworker.get_invoices()
+ out.sort(key=operator.itemgetter('time'))
+ return out
+
+ def get_invoice(self, key):
+ if key in self.invoices:
+ item = copy.copy(self.invoices[key])
+ request_type = item.get('type')
+ if request_type is None:
+ # todo: convert old bip70 invoices
+ return
+ # add status
+ if item.get('txid'):
+ status = PR_PAID
+ elif 'exp' in item and item['time'] + item['exp'] < time.time():
+ status = PR_EXPIRED
+ else:
+ status = PR_UNPAID
+ item['status'] = status
+ return item
+ if self.lnworker:
+ return self.lnworker.get_request(key)
+
+
@profiler
def get_full_history(self, fx=None):
transactions = OrderedDictWithIndex()
@@ -1221,6 +1267,7 @@ class Abstract_Wallet(AddressSynchronizer):
if not r:
return
out = copy.copy(r)
+ out['type'] = PR_TYPE_ADDRESS
out['URI'] = 'bitcoin:' + addr + '?amount=' + format_satoshis(out.get('amount'))
status, conf = self.get_request_status(addr)
out['status'] = status
@@ -1363,6 +1410,14 @@ class Abstract_Wallet(AddressSynchronizer):
elif self.lnworker:
self.lnworker.delete_invoice(key)
+ def delete_invoice(self, key):
+ """ lightning or on-chain """
+ if key in self.invoices:
+ self.invoices.pop(key)
+ self.storage.put('invoices', self.invoices)
+ elif self.lnworker:
+ self.lnworker.delete_invoice(key)
+
def remove_payment_request(self, addr, config):
if addr not in self.receive_requests:
return False
@@ -1381,7 +1436,7 @@ class Abstract_Wallet(AddressSynchronizer):
""" sorted by timestamp """
out = [self.get_payment_request(x, config) for x in self.receive_requests.keys()]
if self.lnworker:
- out += self.lnworker.get_invoices()
+ out += self.lnworker.get_requests()
out.sort(key=operator.itemgetter('time'))
return out