mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-31 09:21:39 +00:00
Simplify invoices and requests.
- We need only two types: PR_TYPE_ONCHAIN and PR_TYPE_LN - BIP70 is no longer a type, but an optional field in the dict - Invoices in the wallet are indexed by a hash of their serialized list of outputs. - Requests are still indexed by address, because we never generate Paytomany requests. - Add 'clear_invoices' command to CLI - Add 'save invoice' button to Qt
This commit is contained in:
parent
1b332748c3
commit
aaed594772
10 changed files with 221 additions and 177 deletions
|
@ -795,11 +795,17 @@ class Commands:
|
|||
return wallet.remove_payment_request(address)
|
||||
|
||||
@command('w')
|
||||
async def clearrequests(self, wallet=None):
|
||||
async def clear_requests(self, wallet=None):
|
||||
"""Remove all payment requests"""
|
||||
for k in list(wallet.receive_requests.keys()):
|
||||
wallet.remove_payment_request(k)
|
||||
|
||||
@command('w')
|
||||
async def clear_invoices(self, wallet=None):
|
||||
"""Remove all invoices"""
|
||||
wallet.clear_invoices()
|
||||
return True
|
||||
|
||||
@command('n')
|
||||
async def notify(self, address: str, URL: str):
|
||||
"""Watch an address. Every time the address changes, a http POST is sent to the URL."""
|
||||
|
|
|
@ -21,8 +21,9 @@ from kivy.lang import Builder
|
|||
from kivy.factory import Factory
|
||||
from kivy.utils import platform
|
||||
|
||||
from electrum.bitcoin import TYPE_ADDRESS
|
||||
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.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||
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
|
||||
|
@ -180,6 +181,7 @@ class SendScreen(CScreen):
|
|||
kvname = 'send'
|
||||
payment_request = None
|
||||
payment_request_queued = None
|
||||
parsed_URI = None
|
||||
|
||||
def set_URI(self, text):
|
||||
if not self.app.wallet:
|
||||
|
@ -190,12 +192,13 @@ class SendScreen(CScreen):
|
|||
except InvalidBitcoinURI as e:
|
||||
self.app.show_info(_("Error parsing URI") + f":\n{e}")
|
||||
return
|
||||
self.parsed_URI = uri
|
||||
amount = uri.get('amount')
|
||||
self.screen.address = uri.get('address', '')
|
||||
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 = PR_TYPE_ADDRESS
|
||||
self.screen.is_lightning = False
|
||||
|
||||
def set_ln_invoice(self, invoice):
|
||||
try:
|
||||
|
@ -207,7 +210,7 @@ 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 = PR_TYPE_LN
|
||||
self.screen.is_lightning = True
|
||||
|
||||
def update(self):
|
||||
if not self.loaded:
|
||||
|
@ -227,14 +230,14 @@ class SendScreen(CScreen):
|
|||
if invoice_type == PR_TYPE_LN:
|
||||
key = item['rhash']
|
||||
status = get_request_status(item) # convert to str
|
||||
elif invoice_type == PR_TYPE_BIP70:
|
||||
elif invoice_type == PR_TYPE_ONCHAIN:
|
||||
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
|
||||
else:
|
||||
raise Exception('unknown invoice type')
|
||||
return {
|
||||
'is_lightning': invoice_type == PR_TYPE_LN,
|
||||
'is_bip70': 'bip70' in item,
|
||||
'screen': self,
|
||||
'status': status,
|
||||
'key': key,
|
||||
|
@ -247,19 +250,16 @@ class SendScreen(CScreen):
|
|||
self.screen.message = ''
|
||||
self.screen.address = ''
|
||||
self.payment_request = None
|
||||
self.screen.destinationtype = PR_TYPE_ADDRESS
|
||||
self.screen.locked = False
|
||||
self.parsed_URI = None
|
||||
|
||||
def set_request(self, pr):
|
||||
self.screen.address = pr.get_requestor()
|
||||
amount = pr.get_amount()
|
||||
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 = PR_TYPE_BIP70
|
||||
self.payment_request = pr
|
||||
else:
|
||||
self.screen.destinationtype = PR_TYPE_ADDRESS
|
||||
self.payment_request = None
|
||||
self.screen.locked = True
|
||||
self.payment_request = pr
|
||||
|
||||
def do_paste(self):
|
||||
data = self.app._clipboard.paste().strip()
|
||||
|
@ -299,30 +299,19 @@ class SendScreen(CScreen):
|
|||
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
|
||||
return
|
||||
message = self.screen.message
|
||||
if self.screen.destinationtype == PR_TYPE_LN:
|
||||
if self.screen.is_lightning:
|
||||
return {
|
||||
'type': PR_TYPE_LN,
|
||||
'invoice': address,
|
||||
'amount': amount,
|
||||
'message': message,
|
||||
}
|
||||
elif self.screen.destinationtype == PR_TYPE_ADDRESS:
|
||||
else:
|
||||
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')
|
||||
outputs = [(TYPE_ADDRESS, address, amount)]
|
||||
return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
|
||||
|
||||
def do_save(self):
|
||||
invoice = self.read_invoice()
|
||||
|
@ -345,20 +334,18 @@ class SendScreen(CScreen):
|
|||
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']
|
||||
elif invoice['type'] == PR_TYPE_ONCHAIN:
|
||||
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_onchain(amount, message, outputs, b))
|
||||
d.open()
|
||||
do_pay = lambda rbf: self._do_send_onchain(amount, message, outputs, rbf)
|
||||
if self.app.electrum_config.get('use_rbf'):
|
||||
d = Question(_('Should this transaction be replaceable?'), do_pay)
|
||||
d.open()
|
||||
else:
|
||||
do_pay(False)
|
||||
else:
|
||||
self._do_send_onchain(amount, message, outputs, False)
|
||||
raise Exception('unknown invoice type')
|
||||
|
||||
def _do_send_lightning(self, invoice, amount):
|
||||
attempts = 10
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
#:import _ electrum.gui.kivy.i18n._
|
||||
#: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)
|
||||
|
@ -68,7 +65,9 @@ SendScreen:
|
|||
address: ''
|
||||
amount: ''
|
||||
message: ''
|
||||
destinationtype: PR_TYPE_ADDRESS
|
||||
is_bip70: False
|
||||
is_lightning: False
|
||||
is_locked: self.is_lightning or self.is_bip70
|
||||
BoxLayout
|
||||
padding: '12dp', '12dp', '12dp', '12dp'
|
||||
spacing: '12dp'
|
||||
|
@ -82,7 +81,7 @@ SendScreen:
|
|||
height: blue_bottom.item_height
|
||||
spacing: '5dp'
|
||||
Image:
|
||||
source: 'atlas://electrum/gui/kivy/theming/light/globe' if root.destinationtype != PR_TYPE_LN else 'atlas://electrum/gui/kivy/theming/light/lightning'
|
||||
source: 'atlas://electrum/gui/kivy/theming/light/lightning' if root.is_lightning else 'atlas://electrum/gui/kivy/theming/light/globe'
|
||||
size_hint: None, None
|
||||
size: '22dp', '22dp'
|
||||
pos_hint: {'center_y': .5}
|
||||
|
@ -93,7 +92,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 == PR_TYPE_ADDRESS)
|
||||
opacity: int(not root.is_locked)
|
||||
color: blue_bottom.foreground_color
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
|
@ -109,10 +108,10 @@ SendScreen:
|
|||
id: amount_e
|
||||
default_text: _('Amount')
|
||||
text: s.amount if s.amount else _('Amount')
|
||||
disabled: root.destinationtype == PR_TYPE_BIP70 or root.destinationtype == PR_TYPE_LN and not s.amount
|
||||
disabled: root.is_bip70 or (root.is_lightning and not s.amount)
|
||||
on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, True))
|
||||
CardSeparator:
|
||||
opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
|
||||
opacity: int(not root.is_locked)
|
||||
color: blue_bottom.foreground_color
|
||||
BoxLayout:
|
||||
id: message_selection
|
||||
|
@ -126,11 +125,11 @@ SendScreen:
|
|||
pos_hint: {'center_y': .5}
|
||||
BlueButton:
|
||||
id: description
|
||||
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
|
||||
text: s.message if s.message else (_('No Description') if root.is_locked else _('Description'))
|
||||
disabled: root.is_locked
|
||||
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
|
||||
CardSeparator:
|
||||
opacity: int(root.destinationtype == PR_TYPE_ADDRESS)
|
||||
opacity: int(not root.is_locked)
|
||||
color: blue_bottom.foreground_color
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
|
@ -144,8 +143,8 @@ SendScreen:
|
|||
BlueButton:
|
||||
id: fee_e
|
||||
default_text: _('Fee')
|
||||
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
|
||||
text: app.fee_status if not root.is_lightning else ''
|
||||
on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if not root.is_lightning else None
|
||||
BoxLayout:
|
||||
size_hint: 1, None
|
||||
height: '48dp'
|
||||
|
|
|
@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QHeaderView, QMenu
|
|||
|
||||
from electrum.i18n import _
|
||||
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.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||
from electrum.lnutil import lndecode, RECEIVED
|
||||
from electrum.bitcoin import COIN
|
||||
from electrum import constants
|
||||
|
@ -78,12 +78,11 @@ class InvoiceList(MyTreeView):
|
|||
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:
|
||||
elif invoice_type == PR_TYPE_ONCHAIN:
|
||||
key = item['id']
|
||||
icon_name = 'seal.png'
|
||||
icon_name = 'bitcoin.png'
|
||||
if item.get('bip70'):
|
||||
icon_name = 'seal.png'
|
||||
else:
|
||||
raise Exception('Unsupported type')
|
||||
status = item['status']
|
||||
|
@ -126,7 +125,6 @@ class InvoiceList(MyTreeView):
|
|||
return
|
||||
key = item_col0.data(ROLE_REQUEST_ID)
|
||||
request_type = item_col0.data(ROLE_REQUEST_TYPE)
|
||||
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()
|
||||
|
@ -135,20 +133,9 @@ 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 in [PR_TYPE_BIP70, PR_TYPE_ADDRESS]:
|
||||
self.create_menu_bitcoin_payreq(menu, key)
|
||||
elif request_type == PR_TYPE_LN:
|
||||
self.create_menu_ln_payreq(menu, key)
|
||||
invoice = self.parent.wallet.get_invoice(key)
|
||||
menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
|
||||
if invoice['status'] == PR_UNPAID:
|
||||
menu.addAction(_("Pay Now"), lambda: self.parent.do_pay_invoice(invoice))
|
||||
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(key))
|
||||
menu.exec_(self.viewport().mapToGlobal(position))
|
||||
|
||||
def create_menu_bitcoin_payreq(self, menu, 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))
|
||||
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(payreq_key))
|
||||
|
||||
def create_menu_ln_payreq(self, menu, payreq_key):
|
||||
req = self.parent.wallet.lnworker.invoices[payreq_key][0]
|
||||
menu.addAction(_("Copy Lightning invoice"), lambda: self.parent.do_copy('Lightning invoice', req))
|
||||
menu.addAction(_("Delete"), lambda: self.parent.delete_invoice(payreq_key))
|
||||
|
|
|
@ -62,6 +62,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
|
|||
UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException,
|
||||
get_new_wallet_name, send_exception_to_crash_reporter,
|
||||
InvalidBitcoinURI, InvoiceError)
|
||||
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||
from electrum.lnutil import PaymentFailure, SENT, RECEIVED
|
||||
from electrum.transaction import Transaction, TxOutput
|
||||
from electrum.address_synchronizer import AddTransactionException
|
||||
|
@ -142,7 +143,6 @@ 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.contacts = wallet.contacts
|
||||
self.tray = gui_object.tray
|
||||
self.app = gui_object.app
|
||||
|
@ -171,6 +171,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
|
||||
self.completions = QStringListModel()
|
||||
|
||||
self.send_tab_is_onchain = False
|
||||
|
||||
self.tabs = tabs = QTabWidget(self)
|
||||
self.send_tab = self.create_send_tab()
|
||||
self.receive_tab = self.create_receive_tab()
|
||||
|
@ -1001,7 +1003,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
|
||||
self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
|
||||
|
||||
self.receive_requests_label = QLabel(_('Incoming invoices'))
|
||||
self.receive_requests_label = QLabel(_('Incoming payments'))
|
||||
|
||||
from .request_list import RequestList
|
||||
self.request_list = RequestList(self)
|
||||
|
@ -1076,6 +1078,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.address_list.update()
|
||||
self.request_list.update()
|
||||
self.request_list.select_key(key)
|
||||
# clear request fields
|
||||
self.receive_amount_e.setText('')
|
||||
self.receive_message_e.setText('')
|
||||
|
||||
def create_bitcoin_request(self, amount, message, expiration):
|
||||
addr = self.wallet.get_unused_address()
|
||||
|
@ -1206,34 +1211,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.message_e = MyLineEdit()
|
||||
grid.addWidget(self.message_e, 2, 1, 1, -1)
|
||||
|
||||
self.from_label = QLabel(_('From'))
|
||||
grid.addWidget(self.from_label, 3, 0)
|
||||
self.from_list = FromList(self, self.from_list_menu)
|
||||
grid.addWidget(self.from_list, 3, 1, 1, -1)
|
||||
self.set_pay_from([])
|
||||
|
||||
msg = _('Amount to be sent.') + '\n\n' \
|
||||
+ _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' ' \
|
||||
+ _('Note that if you have frozen some of your addresses, the available funds will be lower than your total balance.') + '\n\n' \
|
||||
+ _('Keyboard shortcut: type "!" to send all your coins.')
|
||||
amount_label = HelpLabel(_('Amount'), msg)
|
||||
grid.addWidget(amount_label, 4, 0)
|
||||
grid.addWidget(self.amount_e, 4, 1)
|
||||
grid.addWidget(amount_label, 3, 0)
|
||||
grid.addWidget(self.amount_e, 3, 1)
|
||||
|
||||
self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')
|
||||
if not self.fx or not self.fx.is_enabled():
|
||||
self.fiat_send_e.setVisible(False)
|
||||
grid.addWidget(self.fiat_send_e, 4, 2)
|
||||
grid.addWidget(self.fiat_send_e, 3, 2)
|
||||
self.amount_e.frozen.connect(
|
||||
lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
|
||||
|
||||
self.max_button = EnterButton(_("Max"), self.spend_max)
|
||||
self.max_button.setFixedWidth(self.amount_e.width())
|
||||
self.max_button.setCheckable(True)
|
||||
grid.addWidget(self.max_button, 4, 3)
|
||||
grid.addWidget(self.max_button, 3, 3)
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addStretch(1)
|
||||
grid.addLayout(hbox, 4, 4)
|
||||
grid.addLayout(hbox, 3, 4)
|
||||
|
||||
self.from_label = QLabel(_('From'))
|
||||
grid.addWidget(self.from_label, 4, 0)
|
||||
self.from_list = FromList(self, self.from_list_menu)
|
||||
grid.addWidget(self.from_list, 4, 1, 1, -1)
|
||||
self.set_pay_from([])
|
||||
|
||||
msg = _('Bitcoin transactions are in general not free. A transaction fee is paid by the sender of the funds.') + '\n\n'\
|
||||
+ _('The amount of fee can be decided freely by the sender. However, transactions with low fees take more time to be processed.') + '\n\n'\
|
||||
|
@ -1337,12 +1342,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
if not self.config.get('show_fee', False):
|
||||
self.fee_adv_controls.setVisible(False)
|
||||
|
||||
self.save_button = EnterButton(_("Save"), self.do_save_invoice)
|
||||
self.preview_button = EnterButton(_("Preview"), self.do_preview)
|
||||
self.preview_button.setToolTip(_('Display the details of your transaction before signing it.'))
|
||||
self.send_button = EnterButton(_("Send"), self.do_send)
|
||||
self.send_button = EnterButton(_("Send"), self.do_pay)
|
||||
self.clear_button = EnterButton(_("Clear"), self.do_clear)
|
||||
buttons = QHBoxLayout()
|
||||
buttons.addStretch(1)
|
||||
buttons.addWidget(self.save_button)
|
||||
buttons.addWidget(self.clear_button)
|
||||
buttons.addWidget(self.preview_button)
|
||||
buttons.addWidget(self.send_button)
|
||||
|
@ -1355,7 +1362,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
def reset_max(text):
|
||||
self.max_button.setChecked(False)
|
||||
enable = not bool(text) and not self.amount_e.isReadOnly()
|
||||
self.max_button.setEnabled(enable)
|
||||
#self.max_button.setEnabled(enable)
|
||||
self.amount_e.textEdited.connect(reset_max)
|
||||
self.fiat_send_e.textEdited.connect(reset_max)
|
||||
|
||||
|
@ -1398,7 +1405,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.fee_e.textChanged.connect(entry_changed)
|
||||
self.feerate_e.textChanged.connect(entry_changed)
|
||||
|
||||
self.invoices_label = QLabel(_('Outgoing invoices'))
|
||||
self.set_onchain(False)
|
||||
|
||||
self.invoices_label = QLabel(_('Outgoing payments'))
|
||||
from .invoice_list import InvoiceList
|
||||
self.invoice_list = InvoiceList(self)
|
||||
|
||||
|
@ -1436,7 +1445,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
'''Recalculate the fee. If the fee was manually input, retain it, but
|
||||
still build the TX to see if there are enough funds.
|
||||
'''
|
||||
if self.payto_e.is_lightning:
|
||||
if not self.is_onchain:
|
||||
return
|
||||
freeze_fee = self.is_send_fee_frozen()
|
||||
freeze_feerate = self.is_send_feerate_frozen()
|
||||
|
@ -1448,7 +1457,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.statusBar().showMessage('')
|
||||
return
|
||||
|
||||
outputs, fee_estimator, tx_desc, coins = self.read_send_tab()
|
||||
outputs = self.read_outputs()
|
||||
fee_estimator = self.get_send_fee_estimator()
|
||||
coins = self.get_coins()
|
||||
|
||||
if not outputs:
|
||||
_type, addr = self.get_payto_or_dummy()
|
||||
outputs = [TxOutput(_type, addr, amount)]
|
||||
|
@ -1607,15 +1619,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
fee_estimator = None
|
||||
return fee_estimator
|
||||
|
||||
def read_send_tab(self):
|
||||
label = self.message_e.text()
|
||||
def read_outputs(self):
|
||||
if self.payment_request:
|
||||
outputs = self.payment_request.get_outputs()
|
||||
else:
|
||||
outputs = self.payto_e.get_outputs(self.max_button.isChecked())
|
||||
fee_estimator = self.get_send_fee_estimator()
|
||||
coins = self.get_coins()
|
||||
return outputs, fee_estimator, label, coins
|
||||
return outputs
|
||||
|
||||
def check_send_tab_outputs_and_show_errors(self, outputs) -> bool:
|
||||
"""Returns whether there are errors with outputs.
|
||||
|
@ -1658,9 +1667,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
|
||||
return False # no errors
|
||||
|
||||
def do_preview(self):
|
||||
self.do_send(preview = True)
|
||||
|
||||
def pay_lightning_invoice(self, invoice):
|
||||
amount_sat = self.amount_e.get_amount()
|
||||
attempts = LN_NUM_PAYMENT_ATTEMPTS
|
||||
|
@ -1684,15 +1690,60 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
e = args[0]
|
||||
self.show_error(_('Error') + '\n' + str(e))
|
||||
|
||||
def do_send(self, preview = False):
|
||||
if self.payto_e.is_lightning:
|
||||
def read_invoice(self):
|
||||
message = self.message_e.text()
|
||||
amount = self.amount_e.get_amount()
|
||||
if not self.is_onchain:
|
||||
return {
|
||||
'type': PR_TYPE_LN,
|
||||
'invoice': self.payto_e.lightning_invoice,
|
||||
'amount': amount,
|
||||
'message': message,
|
||||
}
|
||||
else:
|
||||
outputs = self.read_outputs()
|
||||
if self.check_send_tab_outputs_and_show_errors(outputs):
|
||||
return
|
||||
return self.wallet.create_invoice(outputs, message, self.payment_request, self.payto_URI)
|
||||
|
||||
def do_save_invoice(self):
|
||||
invoice = self.read_invoice()
|
||||
if not invoice:
|
||||
return
|
||||
self.wallet.save_invoice(invoice)
|
||||
self.do_clear()
|
||||
self.invoice_list.update()
|
||||
|
||||
def do_preview(self):
|
||||
self.do_pay(preview=True)
|
||||
|
||||
def do_pay(self, preview=False):
|
||||
invoice = self.read_invoice()
|
||||
if not invoice:
|
||||
return
|
||||
if not preview:
|
||||
self.wallet.save_invoice(invoice)
|
||||
self.do_clear()
|
||||
self.invoice_list.update()
|
||||
self.do_pay_invoice(invoice, preview)
|
||||
|
||||
def do_pay_invoice(self, invoice, preview=False):
|
||||
if invoice['type'] == PR_TYPE_LN:
|
||||
self.pay_lightning_invoice(self.payto_e.lightning_invoice)
|
||||
return
|
||||
elif invoice['type'] == PR_TYPE_ONCHAIN:
|
||||
message = invoice['message']
|
||||
outputs = invoice['outputs']
|
||||
amount = sum(map(lambda x:x[2], outputs))
|
||||
else:
|
||||
raise Exception('unknowwn invoicce type')
|
||||
|
||||
if run_hook('abort_send', self):
|
||||
return
|
||||
outputs, fee_estimator, tx_desc, coins = self.read_send_tab()
|
||||
if self.check_send_tab_outputs_and_show_errors(outputs):
|
||||
return
|
||||
|
||||
outputs = [TxOutput(*x) for x in outputs]
|
||||
fee_estimator = self.get_send_fee_estimator()
|
||||
coins = self.get_coins()
|
||||
try:
|
||||
is_sweep = bool(self.tx_external_keypairs)
|
||||
tx = self.wallet.make_unsigned_transaction(
|
||||
|
@ -1724,7 +1775,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
return
|
||||
|
||||
if preview:
|
||||
self.show_transaction(tx, tx_desc)
|
||||
self.show_transaction(tx, message)
|
||||
return
|
||||
|
||||
if not self.network:
|
||||
|
@ -1764,7 +1815,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.show_transaction(tx)
|
||||
self.do_clear()
|
||||
else:
|
||||
self.broadcast_transaction(tx, tx_desc)
|
||||
self.broadcast_transaction(tx, message)
|
||||
self.sign_tx_with_password(tx, sign_done, password)
|
||||
|
||||
@protected
|
||||
|
@ -1935,8 +1986,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
if lnaddr.amount is not None:
|
||||
self.amount_e.setAmount(lnaddr.amount * COIN)
|
||||
#self.amount_e.textEdited.emit("")
|
||||
self.payto_e.is_lightning = True
|
||||
self.show_send_tab_onchain_fees(False)
|
||||
self.set_onchain(False)
|
||||
|
||||
def set_onchain(self, b):
|
||||
self.is_onchain = b
|
||||
self.preview_button.setEnabled(b)
|
||||
self.max_button.setEnabled(b)
|
||||
self.show_send_tab_onchain_fees(b)
|
||||
|
||||
def show_send_tab_onchain_fees(self, b: bool):
|
||||
self.feecontrol_fields.setVisible(b)
|
||||
|
@ -1951,6 +2007,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.show_error(_("Error parsing URI") + f":\n{e}")
|
||||
return
|
||||
self.show_send_tab()
|
||||
self.payto_URI = out
|
||||
r = out.get('r')
|
||||
sig = out.get('sig')
|
||||
name = out.get('name')
|
||||
|
@ -1977,9 +2034,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.max_button.setChecked(False)
|
||||
self.not_enough_funds = False
|
||||
self.payment_request = None
|
||||
self.payto_URI = None
|
||||
self.payto_e.is_pr = False
|
||||
self.payto_e.is_lightning = False
|
||||
self.show_send_tab_onchain_fees(True)
|
||||
self.is_onchain = False
|
||||
self.set_onchain(False)
|
||||
for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e,
|
||||
self.fee_e, self.feerate_e]:
|
||||
e.setText('')
|
||||
|
@ -1993,6 +2051,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.update_status()
|
||||
run_hook('do_clear', self)
|
||||
|
||||
|
||||
def set_frozen_state_of_addresses(self, addrs, freeze: bool):
|
||||
self.wallet.set_frozen_state_of_addresses(addrs, freeze)
|
||||
self.address_list.update()
|
||||
|
@ -2048,6 +2107,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
|
||||
def spend_coins(self, coins):
|
||||
self.set_pay_from(coins)
|
||||
self.set_onchain(len(coins) > 0)
|
||||
self.show_send_tab()
|
||||
self.update_fee()
|
||||
|
||||
|
@ -2095,16 +2155,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
self.update_completions()
|
||||
|
||||
def show_invoice(self, key):
|
||||
pr = self.wallet.get_invoice(key)
|
||||
if pr is None:
|
||||
invoice = self.wallet.get_invoice(key)
|
||||
if invoice is None:
|
||||
self.show_error('Cannot find payment request in wallet.')
|
||||
return
|
||||
pr.verify(self.contacts)
|
||||
self.show_pr_details(pr)
|
||||
bip70 = invoice.get('bip70')
|
||||
if bip70:
|
||||
pr = paymentrequest.PaymentRequest(bytes.fromhex(bip70))
|
||||
pr.verify(self.contacts)
|
||||
self.show_bip70_details(pr)
|
||||
|
||||
def show_pr_details(self, pr):
|
||||
def show_bip70_details(self, pr):
|
||||
key = pr.get_id()
|
||||
d = WindowModalDialog(self, _("Invoice"))
|
||||
d = WindowModalDialog(self, _("BIP70 Invoice"))
|
||||
vbox = QVBoxLayout(d)
|
||||
grid = QGridLayout()
|
||||
grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
|
||||
|
@ -2140,7 +2203,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
|
|||
vbox.addLayout(Buttons(exportButton, deleteButton, CloseButton(d)))
|
||||
d.exec_()
|
||||
|
||||
def do_pay_invoice(self, key):
|
||||
def pay_bip70_invoice(self, key):
|
||||
pr = self.wallet.get_invoice(key)
|
||||
self.payment_request = pr
|
||||
self.prepare_for_payment_request()
|
||||
|
|
|
@ -61,7 +61,6 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
|
|||
self.errors = []
|
||||
self.is_pr = False
|
||||
self.is_alias = False
|
||||
self.is_lightning = False
|
||||
self.update_size()
|
||||
self.payto_address = None
|
||||
self.previous_payto = ''
|
||||
|
@ -143,6 +142,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
|
|||
except:
|
||||
pass
|
||||
if self.payto_address:
|
||||
self.win.set_onchain(True)
|
||||
self.win.lock_amount(False)
|
||||
return
|
||||
|
||||
|
@ -153,12 +153,13 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
|
|||
except:
|
||||
self.errors.append((i, line.strip()))
|
||||
continue
|
||||
|
||||
outputs.append(output)
|
||||
if output.value == '!':
|
||||
is_max = True
|
||||
else:
|
||||
total += output.value
|
||||
if outputs:
|
||||
self.win.set_onchain(True)
|
||||
|
||||
self.win.max_button.setChecked(is_max)
|
||||
self.outputs = outputs
|
||||
|
|
|
@ -31,7 +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_TYPE_ONCHAIN, PR_TYPE_LN
|
||||
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
|
||||
|
@ -118,35 +118,30 @@ class RequestList(MyTreeView):
|
|||
status = req.get('status')
|
||||
if status == PR_PAID:
|
||||
continue
|
||||
is_lightning = req['type'] == PR_TYPE_LN
|
||||
request_type = req['type']
|
||||
timestamp = req.get('time', 0)
|
||||
expiration = req.get('exp', None)
|
||||
amount = req.get('amount')
|
||||
message = req['message'] if is_lightning else req['memo']
|
||||
message = req.get('message') or req.get('memo')
|
||||
date = format_time(timestamp)
|
||||
amount_str = self.parent.format_amount(amount) if amount else ""
|
||||
status_str = get_request_status(req)
|
||||
labels = [date, message, amount_str, status_str]
|
||||
if request_type == PR_TYPE_LN:
|
||||
key = req['rhash']
|
||||
icon = read_QIcon("lightning.png")
|
||||
tooltip = 'lightning request'
|
||||
elif request_type == PR_TYPE_ONCHAIN:
|
||||
key = req['address']
|
||||
icon = read_QIcon("bitcoin.png")
|
||||
tooltip = 'onchain request'
|
||||
items = [QStandardItem(e) for e in labels]
|
||||
self.set_editability(items)
|
||||
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE)
|
||||
items[self.Columns.DATE].setData(key, ROLE_KEY)
|
||||
items[self.Columns.DATE].setIcon(icon)
|
||||
items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
|
||||
if request_type == PR_TYPE_LN:
|
||||
items[self.Columns.DATE].setData(req['rhash'], ROLE_KEY)
|
||||
items[self.Columns.DATE].setIcon(read_QIcon("lightning.png"))
|
||||
elif request_type == PR_TYPE_ADDRESS:
|
||||
address = req['address']
|
||||
if address not in domain:
|
||||
continue
|
||||
expiration = req.get('exp', None)
|
||||
signature = req.get('sig')
|
||||
requestor = req.get('name', '')
|
||||
items[self.Columns.DATE].setData(address, ROLE_KEY)
|
||||
if signature is not None:
|
||||
items[self.Columns.DATE].setIcon(read_QIcon("seal.png"))
|
||||
items[self.Columns.DATE].setToolTip(f'signed by {requestor}')
|
||||
else:
|
||||
items[self.Columns.DATE].setIcon(read_QIcon("bitcoin.png"))
|
||||
items[self.Columns.DATE].setToolTip(tooltip)
|
||||
self.model().insertRow(self.model().rowCount(), items)
|
||||
self.filter()
|
||||
# sort requests by date
|
||||
|
@ -177,12 +172,10 @@ class RequestList(MyTreeView):
|
|||
if column == self.Columns.AMOUNT:
|
||||
column_data = column_data.strip()
|
||||
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.do_copy(column_title, column_data))
|
||||
if request_type == PR_TYPE_ADDRESS:
|
||||
menu.addAction(_("Copy Address"), lambda: self.parent.do_copy('Address', key))
|
||||
if request_type == PR_TYPE_LN:
|
||||
menu.addAction(_("Copy lightning payment request"), lambda: self.parent.do_copy('Request', req['invoice']))
|
||||
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy('Lightning Request', req['invoice']))
|
||||
else:
|
||||
menu.addAction(_("Copy URI"), lambda: self.parent.do_copy('URI', req['URI']))
|
||||
menu.addAction(_("Copy Request"), lambda: self.parent.do_copy('Bitcoin URI', req['URI']))
|
||||
if 'view_url' in req:
|
||||
menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
|
||||
menu.addAction(_("Delete"), lambda: self.parent.delete_request(key))
|
||||
|
|
|
@ -41,7 +41,6 @@ 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
|
||||
|
@ -151,10 +150,6 @@ class PaymentRequest:
|
|||
self.memo = self.details.memo
|
||||
self.payment_url = self.details.payment_url
|
||||
|
||||
def is_pr(self):
|
||||
return self.get_amount() != 0
|
||||
#return self.get_outputs() != [(TYPE_ADDRESS, self.get_requestor(), self.get_amount())]
|
||||
|
||||
def verify(self, contacts):
|
||||
if self.error:
|
||||
return False
|
||||
|
@ -269,19 +264,6 @@ class PaymentRequest:
|
|||
def get_memo(self):
|
||||
return self.memo
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
'type': PR_TYPE_BIP70,
|
||||
'id': self.get_id(),
|
||||
'requestor': self.get_requestor(),
|
||||
'message': self.get_memo(),
|
||||
'time': self.get_time(),
|
||||
'exp': self.get_expiration_date() - self.get_time(),
|
||||
'amount': self.get_amount(),
|
||||
'outputs': self.get_outputs(),
|
||||
'hex': self.raw.hex(),
|
||||
}
|
||||
|
||||
def get_id(self):
|
||||
return self.id if self.requestor else self.get_address()
|
||||
|
||||
|
|
|
@ -74,8 +74,7 @@ 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_ONCHAIN = 0
|
||||
PR_TYPE_LN = 2
|
||||
|
||||
# status of payment requests
|
||||
|
|
|
@ -41,12 +41,13 @@ from decimal import Decimal
|
|||
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence
|
||||
|
||||
from .i18n import _
|
||||
from .crypto import sha256
|
||||
from .util import (NotEnoughFunds, UserCancelled, profiler,
|
||||
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
|
||||
WalletFileException, BitcoinException,
|
||||
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
|
||||
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex)
|
||||
from .util import PR_TYPE_ADDRESS, PR_TYPE_BIP70, PR_TYPE_LN
|
||||
from .util import PR_TYPE_ONCHAIN, PR_TYPE_LN
|
||||
from .simple_config import SimpleConfig
|
||||
from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
|
||||
is_minikey, relayfee, dust_threshold)
|
||||
|
@ -505,22 +506,47 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||
'txpos_in_block': hist_item.tx_mined_status.txpos,
|
||||
}
|
||||
|
||||
def create_invoice(self, outputs, message, pr, URI):
|
||||
amount = sum(x[2] for x in outputs)
|
||||
invoice = {
|
||||
'type': PR_TYPE_ONCHAIN,
|
||||
'message': message,
|
||||
'outputs': outputs,
|
||||
'amount': amount,
|
||||
}
|
||||
if pr:
|
||||
invoice['bip70'] = pr.raw.hex()
|
||||
invoice['time'] = pr.get_time()
|
||||
invoice['exp'] = pr.get_expiration_date() - pr.get_time()
|
||||
invoice['requestor'] = pr.get_requestor()
|
||||
invoice['message'] = pr.get_memo()
|
||||
elif URI:
|
||||
timestamp = URI.get('time')
|
||||
if timestamp: invoice['time'] = timestamp
|
||||
exp = URI.get('exp')
|
||||
if exp: invoice['exp'] = exp
|
||||
if 'time' not in invoice:
|
||||
invoice['time'] = int(time.time())
|
||||
return invoice
|
||||
|
||||
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')
|
||||
elif invoice_type == PR_TYPE_ONCHAIN:
|
||||
key = bh2u(sha256(repr(invoice))[0:16])
|
||||
invoice['id'] = key
|
||||
invoice['txid'] = None
|
||||
self.invoices[key] = invoice
|
||||
self.storage.put('invoices', self.invoices)
|
||||
self.storage.write()
|
||||
else:
|
||||
raise Exception('Unsupported invoice type')
|
||||
|
||||
def clear_invoices(self):
|
||||
self.invoices = {}
|
||||
self.storage.put('invoices', self.invoices)
|
||||
self.storage.write()
|
||||
|
||||
def get_invoices(self):
|
||||
out = [self.get_invoice(key) for key in self.invoices.keys()]
|
||||
|
@ -1284,7 +1310,7 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||
if not r:
|
||||
return
|
||||
out = copy.copy(r)
|
||||
out['type'] = PR_TYPE_ADDRESS
|
||||
out['type'] = PR_TYPE_ONCHAIN
|
||||
out['URI'] = self.get_request_URI(addr)
|
||||
status, conf = self.get_request_status(addr)
|
||||
out['status'] = status
|
||||
|
@ -1362,9 +1388,10 @@ class Abstract_Wallet(AddressSynchronizer):
|
|||
self.network.trigger_callback('payment_received', self, addr, status)
|
||||
|
||||
def make_payment_request(self, addr, amount, message, expiration):
|
||||
from .bitcoin import TYPE_ADDRESS
|
||||
timestamp = int(time.time())
|
||||
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10]
|
||||
r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id}
|
||||
r = {'time':timestamp, 'amount':amount, 'exp':expiration, 'address':addr, 'memo':message, 'id':_id, 'outputs': [(TYPE_ADDRESS, addr, amount)]}
|
||||
return r
|
||||
|
||||
def sign_payment_request(self, key, alias, alias_addr, password):
|
||||
|
|
Loading…
Add table
Reference in a new issue