electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

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:
Melectrum/commands.py | 8+++++++-
Melectrum/gui/kivy/uix/screens.py | 65++++++++++++++++++++++++++---------------------------------------
Melectrum/gui/kivy/uix/ui_screens/send.kv | 25++++++++++++-------------
Melectrum/gui/qt/invoice_list.py | 33++++++++++-----------------------
Melectrum/gui/qt/main_window.py | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Melectrum/gui/qt/paytoedit.py | 5+++--
Melectrum/gui/qt/request_list.py | 39++++++++++++++++-----------------------
Melectrum/paymentrequest.py | 18------------------
Melectrum/util.py | 3+--
Melectrum/wallet.py | 51+++++++++++++++++++++++++++++++++++++++------------
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):