commit aaed594772dde4b6240267e21f067e89bed5fa15
parent 1b332748c3ef641ecb94e8cbaefd4c2483b6993f
Author: ThomasV <thomasv@electrum.org>
Date: Sun, 8 Sep 2019 11:59:03 +0200
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
Diffstat:
10 files changed, 221 insertions(+), 177 deletions(-)
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -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."""
diff --git a/electrum/gui/kivy/uix/screens.py b/electrum/gui/kivy/uix/screens.py
@@ -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
diff --git a/electrum/gui/kivy/uix/ui_screens/send.kv b/electrum/gui/kivy/uix/ui_screens/send.kv
@@ -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'
diff --git a/electrum/gui/qt/invoice_list.py b/electrum/gui/qt/invoice_list.py
@@ -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))
diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
@@ -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()
diff --git a/electrum/gui/qt/paytoedit.py b/electrum/gui/qt/paytoedit.py
@@ -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
diff --git a/electrum/gui/qt/request_list.py b/electrum/gui/qt/request_list.py
@@ -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))
diff --git a/electrum/paymentrequest.py b/electrum/paymentrequest.py
@@ -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()
diff --git a/electrum/util.py b/electrum/util.py
@@ -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
diff --git a/electrum/wallet.py b/electrum/wallet.py
@@ -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):