mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
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.
This commit is contained in:
parent
3902d774f7
commit
a50f935aec
13 changed files with 563 additions and 359 deletions
|
@ -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,15 +355,12 @@ 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()
|
||||
else:
|
||||
if pr.has_expired():
|
||||
elif pr.has_expired():
|
||||
self.show_error(_('Payment request has expired'))
|
||||
else:
|
||||
self.switch_to('send')
|
||||
|
@ -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
Normal file
97
electrum/gui/kivy/uix/dialogs/invoice_dialog.py
Normal file
|
@ -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()
|
|
@ -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.screen.destinationtype = PR_TYPE_BIP70
|
||||
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.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):
|
||||
if not self.screen.amount:
|
||||
self.app.show_error(_('Since the invoice contained no amount, you must enter one'))
|
||||
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()
|
||||
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:
|
||||
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 bitcoin.is_address(address):
|
||||
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
|
||||
if not self.screen.amount:
|
||||
self.app.show_error(_('Please enter an amount'))
|
||||
return
|
||||
try:
|
||||
amount = self.app.get_amount(self.screen.amount)
|
||||
except:
|
||||
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
|
||||
return
|
||||
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, address, amount)]
|
||||
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
|
||||
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)]
|
||||
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(self, amount, message, outputs, rbf):
|
||||
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_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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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,16 +135,16 @@ 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:
|
||||
#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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,22 +984,31 @@ 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
|
||||
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
|
||||
|
||||
|
|
|
@ -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())
|
||||
]
|
||||
|
|
|
@ -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_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 = {
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue