Browse Source

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.
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
ThomasV 6 years ago
parent
commit
a50f935aec
  1. 47
      electrum/gui/kivy/main_window.py
  2. 97
      electrum/gui/kivy/uix/dialogs/invoice_dialog.py
  3. 183
      electrum/gui/kivy/uix/screens.py
  4. 108
      electrum/gui/kivy/uix/ui_screens/send.kv
  5. 6
      electrum/gui/qt/history_list.py
  6. 80
      electrum/gui/qt/invoice_list.py
  7. 74
      electrum/gui/qt/main_window.py
  8. 9
      electrum/gui/qt/request_list.py
  9. 13
      electrum/lnchannel.py
  10. 117
      electrum/lnworker.py
  11. 104
      electrum/paymentrequest.py
  12. 19
      electrum/util.py
  13. 65
      electrum/wallet.py

47
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

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

183
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

108
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'

6
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"))

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

74
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

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

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

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

104
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())
]

19
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 = {

65
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

Loading…
Cancel
Save