Browse Source

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
dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
ThomasV 5 years ago
parent
commit
aaed594772
  1. 8
      electrum/commands.py
  2. 65
      electrum/gui/kivy/uix/screens.py
  3. 25
      electrum/gui/kivy/uix/ui_screens/send.kv
  4. 33
      electrum/gui/qt/invoice_list.py
  5. 151
      electrum/gui/qt/main_window.py
  6. 5
      electrum/gui/qt/paytoedit.py
  7. 39
      electrum/gui/qt/request_list.py
  8. 18
      electrum/paymentrequest.py
  9. 3
      electrum/util.py
  10. 51
      electrum/wallet.py

8
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."""

65
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

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

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

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

5
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

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

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

3
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

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

Loading…
Cancel
Save