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 6 years ago
parent
commit
aaed594772
  1. 8
      electrum/commands.py
  2. 59
      electrum/gui/kivy/uix/screens.py
  3. 25
      electrum/gui/kivy/uix/ui_screens/send.kv
  4. 31
      electrum/gui/qt/invoice_list.py
  5. 149
      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. 47
      electrum/wallet.py

8
electrum/commands.py

@ -795,11 +795,17 @@ class Commands:
return wallet.remove_payment_request(address) return wallet.remove_payment_request(address)
@command('w') @command('w')
async def clearrequests(self, wallet=None): async def clear_requests(self, wallet=None):
"""Remove all payment requests""" """Remove all payment requests"""
for k in list(wallet.receive_requests.keys()): for k in list(wallet.receive_requests.keys()):
wallet.remove_payment_request(k) wallet.remove_payment_request(k)
@command('w')
async def clear_invoices(self, wallet=None):
"""Remove all invoices"""
wallet.clear_invoices()
return True
@command('n') @command('n')
async def notify(self, address: str, URL: str): async def notify(self, address: str, URL: str):
"""Watch an address. Every time the address changes, a http POST is sent to the URL.""" """Watch an address. Every time the address changes, a http POST is sent to the URL."""

59
electrum/gui/kivy/uix/screens.py

@ -21,8 +21,9 @@ from kivy.lang import Builder
from kivy.factory import Factory from kivy.factory import Factory
from kivy.utils import platform 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 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 import bitcoin, constants
from electrum.transaction import TxOutput, Transaction, tx_from_str from electrum.transaction import TxOutput, Transaction, tx_from_str
from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI from electrum.util import send_exception_to_crash_reporter, parse_URI, InvalidBitcoinURI
@ -180,6 +181,7 @@ class SendScreen(CScreen):
kvname = 'send' kvname = 'send'
payment_request = None payment_request = None
payment_request_queued = None payment_request_queued = None
parsed_URI = None
def set_URI(self, text): def set_URI(self, text):
if not self.app.wallet: if not self.app.wallet:
@ -190,12 +192,13 @@ class SendScreen(CScreen):
except InvalidBitcoinURI as e: except InvalidBitcoinURI as e:
self.app.show_info(_("Error parsing URI") + f":\n{e}") self.app.show_info(_("Error parsing URI") + f":\n{e}")
return return
self.parsed_URI = uri
amount = uri.get('amount') amount = uri.get('amount')
self.screen.address = uri.get('address', '') self.screen.address = uri.get('address', '')
self.screen.message = uri.get('message', '') self.screen.message = uri.get('message', '')
self.screen.amount = self.app.format_amount_and_units(amount) if amount else '' self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
self.payment_request = None self.payment_request = None
self.screen.destinationtype = PR_TYPE_ADDRESS self.screen.is_lightning = False
def set_ln_invoice(self, invoice): def set_ln_invoice(self, invoice):
try: try:
@ -207,7 +210,7 @@ class SendScreen(CScreen):
self.screen.message = dict(lnaddr.tags).get('d', None) 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.screen.amount = self.app.format_amount_and_units(lnaddr.amount * bitcoin.COIN) if lnaddr.amount else ''
self.payment_request = None self.payment_request = None
self.screen.destinationtype = PR_TYPE_LN self.screen.is_lightning = True
def update(self): def update(self):
if not self.loaded: if not self.loaded:
@ -227,14 +230,14 @@ class SendScreen(CScreen):
if invoice_type == PR_TYPE_LN: if invoice_type == PR_TYPE_LN:
key = item['rhash'] key = item['rhash']
status = get_request_status(item) # convert to str status = get_request_status(item) # convert to str
elif invoice_type == PR_TYPE_BIP70: elif invoice_type == PR_TYPE_ONCHAIN:
key = item['id'] key = item['id']
status = get_request_status(item) # convert to str status = get_request_status(item) # convert to str
elif invoice_type == PR_TYPE_ADDRESS: else:
key = item['address'] raise Exception('unknown invoice type')
status = get_request_status(item) # convert to str
return { return {
'is_lightning': invoice_type == PR_TYPE_LN, 'is_lightning': invoice_type == PR_TYPE_LN,
'is_bip70': 'bip70' in item,
'screen': self, 'screen': self,
'status': status, 'status': status,
'key': key, 'key': key,
@ -247,19 +250,16 @@ class SendScreen(CScreen):
self.screen.message = '' self.screen.message = ''
self.screen.address = '' self.screen.address = ''
self.payment_request = None self.payment_request = None
self.screen.destinationtype = PR_TYPE_ADDRESS self.screen.locked = False
self.parsed_URI = None
def set_request(self, pr): def set_request(self, pr):
self.screen.address = pr.get_requestor() self.screen.address = pr.get_requestor()
amount = pr.get_amount() amount = pr.get_amount()
self.screen.amount = self.app.format_amount_and_units(amount) if amount else '' self.screen.amount = self.app.format_amount_and_units(amount) if amount else ''
self.screen.message = pr.get_memo() self.screen.message = pr.get_memo()
if pr.is_pr(): self.screen.locked = True
self.screen.destinationtype = PR_TYPE_BIP70
self.payment_request = pr self.payment_request = pr
else:
self.screen.destinationtype = PR_TYPE_ADDRESS
self.payment_request = None
def do_paste(self): def do_paste(self):
data = self.app._clipboard.paste().strip() data = self.app._clipboard.paste().strip()
@ -299,30 +299,19 @@ class SendScreen(CScreen):
self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount) self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount)
return return
message = self.screen.message message = self.screen.message
if self.screen.destinationtype == PR_TYPE_LN: if self.screen.is_lightning:
return { return {
'type': PR_TYPE_LN, 'type': PR_TYPE_LN,
'invoice': address, 'invoice': address,
'amount': amount, 'amount': amount,
'message': message, 'message': message,
} }
elif self.screen.destinationtype == PR_TYPE_ADDRESS: else:
if not bitcoin.is_address(address): if not bitcoin.is_address(address):
self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address) self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address)
return return
return { outputs = [(TYPE_ADDRESS, address, amount)]
'type': PR_TYPE_ADDRESS, return self.app.wallet.create_invoice(outputs, message, self.payment_request, self.parsed_URI)
'address': address,
'amount': amount,
'message': message,
}
elif self.screen.destinationtype == PR_TYPE_BIP70:
if self.payment_request.has_expired():
self.app.show_error(_('Payment request has expired'))
return
return self.payment_request.get_dict()
else:
raise Exception('Unknown invoice type')
def do_save(self): def do_save(self):
invoice = self.read_invoice() invoice = self.read_invoice()
@ -345,20 +334,18 @@ class SendScreen(CScreen):
if invoice['type'] == PR_TYPE_LN: if invoice['type'] == PR_TYPE_LN:
self._do_send_lightning(invoice['invoice'], invoice['amount']) self._do_send_lightning(invoice['invoice'], invoice['amount'])
return return
elif invoice['type'] == PR_TYPE_ADDRESS: elif invoice['type'] == PR_TYPE_ONCHAIN:
address = invoice['address']
amount = invoice['amount']
message = invoice['message'] message = invoice['message']
outputs = [TxOutput(bitcoin.TYPE_ADDRESS, address, amount)]
elif invoice['type'] == PR_TYPE_BIP70:
outputs = invoice['outputs'] outputs = invoice['outputs']
amount = sum(map(lambda x:x[2], outputs)) amount = sum(map(lambda x:x[2], outputs))
# onchain payment do_pay = lambda rbf: self._do_send_onchain(amount, message, outputs, rbf)
if self.app.electrum_config.get('use_rbf'): 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 = Question(_('Should this transaction be replaceable?'), do_pay)
d.open() d.open()
else: else:
self._do_send_onchain(amount, message, outputs, False) do_pay(False)
else:
raise Exception('unknown invoice type')
def _do_send_lightning(self, invoice, amount): def _do_send_lightning(self, invoice, amount):
attempts = 10 attempts = 10

25
electrum/gui/kivy/uix/ui_screens/send.kv

@ -1,8 +1,5 @@
#:import _ electrum.gui.kivy.i18n._ #:import _ electrum.gui.kivy.i18n._
#:import Factory kivy.factory.Factory #: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 #:import Decimal decimal.Decimal
#:set btc_symbol chr(171) #:set btc_symbol chr(171)
#:set mbtc_symbol chr(187) #:set mbtc_symbol chr(187)
@ -68,7 +65,9 @@ SendScreen:
address: '' address: ''
amount: '' amount: ''
message: '' message: ''
destinationtype: PR_TYPE_ADDRESS is_bip70: False
is_lightning: False
is_locked: self.is_lightning or self.is_bip70
BoxLayout BoxLayout
padding: '12dp', '12dp', '12dp', '12dp' padding: '12dp', '12dp', '12dp', '12dp'
spacing: '12dp' spacing: '12dp'
@ -82,7 +81,7 @@ SendScreen:
height: blue_bottom.item_height height: blue_bottom.item_height
spacing: '5dp' spacing: '5dp'
Image: 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_hint: None, None
size: '22dp', '22dp' size: '22dp', '22dp'
pos_hint: {'center_y': .5} 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.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')) #on_release: Clock.schedule_once(lambda dt: app.popup_dialog('contacts'))
CardSeparator: CardSeparator:
opacity: int(root.destinationtype == PR_TYPE_ADDRESS) opacity: int(not root.is_locked)
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
@ -109,10 +108,10 @@ SendScreen:
id: amount_e id: amount_e
default_text: _('Amount') default_text: _('Amount')
text: s.amount if s.amount else _('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)) on_release: Clock.schedule_once(lambda dt: app.amount_dialog(s, True))
CardSeparator: CardSeparator:
opacity: int(root.destinationtype == PR_TYPE_ADDRESS) opacity: int(not root.is_locked)
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
BoxLayout: BoxLayout:
id: message_selection id: message_selection
@ -126,11 +125,11 @@ SendScreen:
pos_hint: {'center_y': .5} pos_hint: {'center_y': .5}
BlueButton: BlueButton:
id: description 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]) text: s.message if s.message else (_('No Description') if root.is_locked else _('Description'))
disabled: root.destinationtype != PR_TYPE_ADDRESS disabled: root.is_locked
on_release: Clock.schedule_once(lambda dt: app.description_dialog(s)) on_release: Clock.schedule_once(lambda dt: app.description_dialog(s))
CardSeparator: CardSeparator:
opacity: int(root.destinationtype == PR_TYPE_ADDRESS) opacity: int(not root.is_locked)
color: blue_bottom.foreground_color color: blue_bottom.foreground_color
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
@ -144,8 +143,8 @@ SendScreen:
BlueButton: BlueButton:
id: fee_e id: fee_e
default_text: _('Fee') default_text: _('Fee')
text: app.fee_status if root.destinationtype != PR_TYPE_LN else '' text: app.fee_status if not root.is_lightning else ''
on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if root.destinationtype != PR_TYPE_LN else None on_release: Clock.schedule_once(lambda dt: app.fee_dialog(s, True)) if not root.is_lightning else None
BoxLayout: BoxLayout:
size_hint: 1, None size_hint: 1, None
height: '48dp' height: '48dp'

31
electrum/gui/qt/invoice_list.py

@ -31,7 +31,7 @@ from PyQt5.QtWidgets import QHeaderView, QMenu
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import format_time, PR_UNPAID, PR_PAID, get_request_status 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.lnutil import lndecode, RECEIVED
from electrum.bitcoin import COIN from electrum.bitcoin import COIN
from electrum import constants from electrum import constants
@ -78,11 +78,10 @@ class InvoiceList(MyTreeView):
if invoice_type == PR_TYPE_LN: if invoice_type == PR_TYPE_LN:
key = item['rhash'] key = item['rhash']
icon_name = 'lightning.png' icon_name = 'lightning.png'
elif invoice_type == PR_TYPE_ADDRESS: elif invoice_type == PR_TYPE_ONCHAIN:
key = item['address']
icon_name = 'bitcoin.png'
elif invoice_type == PR_TYPE_BIP70:
key = item['id'] key = item['id']
icon_name = 'bitcoin.png'
if item.get('bip70'):
icon_name = 'seal.png' icon_name = 'seal.png'
else: else:
raise Exception('Unsupported type') raise Exception('Unsupported type')
@ -126,7 +125,6 @@ class InvoiceList(MyTreeView):
return return
key = item_col0.data(ROLE_REQUEST_ID) key = item_col0.data(ROLE_REQUEST_ID)
request_type = item_col0.data(ROLE_REQUEST_TYPE) 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 = idx.column()
column_title = self.model().horizontalHeaderItem(column).text() column_title = self.model().horizontalHeaderItem(column).text()
column_data = item.text() column_data = item.text()
@ -135,20 +133,9 @@ class InvoiceList(MyTreeView):
if column == self.Columns.AMOUNT: if column == self.Columns.AMOUNT:
column_data = column_data.strip() column_data = column_data.strip()
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data)) menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
if request_type in [PR_TYPE_BIP70, PR_TYPE_ADDRESS]: invoice = self.parent.wallet.get_invoice(key)
self.create_menu_bitcoin_payreq(menu, key) menu.addAction(_("Details"), lambda: self.parent.show_invoice(key))
elif request_type == PR_TYPE_LN: if invoice['status'] == PR_UNPAID:
self.create_menu_ln_payreq(menu, key) 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)) 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))

149
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, UnknownBaseUnit, DECIMAL_POINT_DEFAULT, UserFacingException,
get_new_wallet_name, send_exception_to_crash_reporter, get_new_wallet_name, send_exception_to_crash_reporter,
InvalidBitcoinURI, InvoiceError) InvalidBitcoinURI, InvoiceError)
from electrum.util import PR_TYPE_ONCHAIN, PR_TYPE_LN
from electrum.lnutil import PaymentFailure, SENT, RECEIVED from electrum.lnutil import PaymentFailure, SENT, RECEIVED
from electrum.transaction import Transaction, TxOutput from electrum.transaction import Transaction, TxOutput
from electrum.address_synchronizer import AddTransactionException from electrum.address_synchronizer import AddTransactionException
@ -142,7 +143,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
assert wallet, "no wallet" assert wallet, "no wallet"
self.wallet = wallet self.wallet = wallet
self.fx = gui_object.daemon.fx # type: FxThread self.fx = gui_object.daemon.fx # type: FxThread
#self.invoices = wallet.invoices
self.contacts = wallet.contacts self.contacts = wallet.contacts
self.tray = gui_object.tray self.tray = gui_object.tray
self.app = gui_object.app self.app = gui_object.app
@ -171,6 +171,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.completions = QStringListModel() self.completions = QStringListModel()
self.send_tab_is_onchain = False
self.tabs = tabs = QTabWidget(self) self.tabs = tabs = QTabWidget(self)
self.send_tab = self.create_send_tab() self.send_tab = self.create_send_tab()
self.receive_tab = self.create_receive_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.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor)) 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 from .request_list import RequestList
self.request_list = RequestList(self) self.request_list = RequestList(self)
@ -1076,6 +1078,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.address_list.update() self.address_list.update()
self.request_list.update() self.request_list.update()
self.request_list.select_key(key) 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): def create_bitcoin_request(self, amount, message, expiration):
addr = self.wallet.get_unused_address() addr = self.wallet.get_unused_address()
@ -1206,34 +1211,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.message_e = MyLineEdit() self.message_e = MyLineEdit()
grid.addWidget(self.message_e, 2, 1, 1, -1) 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' \ msg = _('Amount to be sent.') + '\n\n' \
+ _('The amount will be displayed in red if you do not have enough funds in your wallet.') + ' ' \ + _('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' \ + _('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.') + _('Keyboard shortcut: type "!" to send all your coins.')
amount_label = HelpLabel(_('Amount'), msg) amount_label = HelpLabel(_('Amount'), msg)
grid.addWidget(amount_label, 4, 0) grid.addWidget(amount_label, 3, 0)
grid.addWidget(self.amount_e, 4, 1) grid.addWidget(self.amount_e, 3, 1)
self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '') self.fiat_send_e = AmountEdit(self.fx.get_currency if self.fx else '')
if not self.fx or not self.fx.is_enabled(): if not self.fx or not self.fx.is_enabled():
self.fiat_send_e.setVisible(False) 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( self.amount_e.frozen.connect(
lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly())) lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
self.max_button = EnterButton(_("Max"), self.spend_max) self.max_button = EnterButton(_("Max"), self.spend_max)
self.max_button.setFixedWidth(self.amount_e.width()) self.max_button.setFixedWidth(self.amount_e.width())
self.max_button.setCheckable(True) self.max_button.setCheckable(True)
grid.addWidget(self.max_button, 4, 3) grid.addWidget(self.max_button, 3, 3)
hbox = QHBoxLayout() hbox = QHBoxLayout()
hbox.addStretch(1) 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'\ 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'\ + _('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): if not self.config.get('show_fee', False):
self.fee_adv_controls.setVisible(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 = EnterButton(_("Preview"), self.do_preview)
self.preview_button.setToolTip(_('Display the details of your transaction before signing it.')) 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) self.clear_button = EnterButton(_("Clear"), self.do_clear)
buttons = QHBoxLayout() buttons = QHBoxLayout()
buttons.addStretch(1) buttons.addStretch(1)
buttons.addWidget(self.save_button)
buttons.addWidget(self.clear_button) buttons.addWidget(self.clear_button)
buttons.addWidget(self.preview_button) buttons.addWidget(self.preview_button)
buttons.addWidget(self.send_button) buttons.addWidget(self.send_button)
@ -1355,7 +1362,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def reset_max(text): def reset_max(text):
self.max_button.setChecked(False) self.max_button.setChecked(False)
enable = not bool(text) and not self.amount_e.isReadOnly() 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.amount_e.textEdited.connect(reset_max)
self.fiat_send_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.fee_e.textChanged.connect(entry_changed)
self.feerate_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 from .invoice_list import InvoiceList
self.invoice_list = InvoiceList(self) 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 '''Recalculate the fee. If the fee was manually input, retain it, but
still build the TX to see if there are enough funds. still build the TX to see if there are enough funds.
''' '''
if self.payto_e.is_lightning: if not self.is_onchain:
return return
freeze_fee = self.is_send_fee_frozen() freeze_fee = self.is_send_fee_frozen()
freeze_feerate = self.is_send_feerate_frozen() freeze_feerate = self.is_send_feerate_frozen()
@ -1448,7 +1457,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.statusBar().showMessage('') self.statusBar().showMessage('')
return 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: if not outputs:
_type, addr = self.get_payto_or_dummy() _type, addr = self.get_payto_or_dummy()
outputs = [TxOutput(_type, addr, amount)] outputs = [TxOutput(_type, addr, amount)]
@ -1607,15 +1619,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
fee_estimator = None fee_estimator = None
return fee_estimator return fee_estimator
def read_send_tab(self): def read_outputs(self):
label = self.message_e.text()
if self.payment_request: if self.payment_request:
outputs = self.payment_request.get_outputs() outputs = self.payment_request.get_outputs()
else: else:
outputs = self.payto_e.get_outputs(self.max_button.isChecked()) outputs = self.payto_e.get_outputs(self.max_button.isChecked())
fee_estimator = self.get_send_fee_estimator() return outputs
coins = self.get_coins()
return outputs, fee_estimator, label, coins
def check_send_tab_outputs_and_show_errors(self, outputs) -> bool: def check_send_tab_outputs_and_show_errors(self, outputs) -> bool:
"""Returns whether there are errors with outputs. """Returns whether there are errors with outputs.
@ -1658,9 +1667,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return False # no errors return False # no errors
def do_preview(self):
self.do_send(preview = True)
def pay_lightning_invoice(self, invoice): def pay_lightning_invoice(self, invoice):
amount_sat = self.amount_e.get_amount() amount_sat = self.amount_e.get_amount()
attempts = LN_NUM_PAYMENT_ATTEMPTS attempts = LN_NUM_PAYMENT_ATTEMPTS
@ -1684,15 +1690,60 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
e = args[0] e = args[0]
self.show_error(_('Error') + '\n' + str(e)) self.show_error(_('Error') + '\n' + str(e))
def do_send(self, preview = False): def read_invoice(self):
if self.payto_e.is_lightning: 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) self.pay_lightning_invoice(self.payto_e.lightning_invoice)
return 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): if run_hook('abort_send', self):
return return
outputs, fee_estimator, tx_desc, coins = self.read_send_tab()
if self.check_send_tab_outputs_and_show_errors(outputs): outputs = [TxOutput(*x) for x in outputs]
return fee_estimator = self.get_send_fee_estimator()
coins = self.get_coins()
try: try:
is_sweep = bool(self.tx_external_keypairs) is_sweep = bool(self.tx_external_keypairs)
tx = self.wallet.make_unsigned_transaction( tx = self.wallet.make_unsigned_transaction(
@ -1724,7 +1775,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
return return
if preview: if preview:
self.show_transaction(tx, tx_desc) self.show_transaction(tx, message)
return return
if not self.network: if not self.network:
@ -1764,7 +1815,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.show_transaction(tx) self.show_transaction(tx)
self.do_clear() self.do_clear()
else: else:
self.broadcast_transaction(tx, tx_desc) self.broadcast_transaction(tx, message)
self.sign_tx_with_password(tx, sign_done, password) self.sign_tx_with_password(tx, sign_done, password)
@protected @protected
@ -1935,8 +1986,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
if lnaddr.amount is not None: if lnaddr.amount is not None:
self.amount_e.setAmount(lnaddr.amount * COIN) self.amount_e.setAmount(lnaddr.amount * COIN)
#self.amount_e.textEdited.emit("") #self.amount_e.textEdited.emit("")
self.payto_e.is_lightning = True self.set_onchain(False)
self.show_send_tab_onchain_fees(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): def show_send_tab_onchain_fees(self, b: bool):
self.feecontrol_fields.setVisible(b) self.feecontrol_fields.setVisible(b)
@ -1951,6 +2007,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.show_error(_("Error parsing URI") + f":\n{e}") self.show_error(_("Error parsing URI") + f":\n{e}")
return return
self.show_send_tab() self.show_send_tab()
self.payto_URI = out
r = out.get('r') r = out.get('r')
sig = out.get('sig') sig = out.get('sig')
name = out.get('name') name = out.get('name')
@ -1977,9 +2034,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.max_button.setChecked(False) self.max_button.setChecked(False)
self.not_enough_funds = False self.not_enough_funds = False
self.payment_request = None self.payment_request = None
self.payto_URI = None
self.payto_e.is_pr = False self.payto_e.is_pr = False
self.payto_e.is_lightning = False self.is_onchain = False
self.show_send_tab_onchain_fees(True) self.set_onchain(False)
for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e, for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e,
self.fee_e, self.feerate_e]: self.fee_e, self.feerate_e]:
e.setText('') e.setText('')
@ -1993,6 +2051,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.update_status() self.update_status()
run_hook('do_clear', self) run_hook('do_clear', self)
def set_frozen_state_of_addresses(self, addrs, freeze: bool): def set_frozen_state_of_addresses(self, addrs, freeze: bool):
self.wallet.set_frozen_state_of_addresses(addrs, freeze) self.wallet.set_frozen_state_of_addresses(addrs, freeze)
self.address_list.update() self.address_list.update()
@ -2048,6 +2107,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def spend_coins(self, coins): def spend_coins(self, coins):
self.set_pay_from(coins) self.set_pay_from(coins)
self.set_onchain(len(coins) > 0)
self.show_send_tab() self.show_send_tab()
self.update_fee() self.update_fee()
@ -2095,16 +2155,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.update_completions() self.update_completions()
def show_invoice(self, key): def show_invoice(self, key):
pr = self.wallet.get_invoice(key) invoice = self.wallet.get_invoice(key)
if pr is None: if invoice is None:
self.show_error('Cannot find payment request in wallet.') self.show_error('Cannot find payment request in wallet.')
return return
bip70 = invoice.get('bip70')
if bip70:
pr = paymentrequest.PaymentRequest(bytes.fromhex(bip70))
pr.verify(self.contacts) pr.verify(self.contacts)
self.show_pr_details(pr) self.show_bip70_details(pr)
def show_pr_details(self, pr): def show_bip70_details(self, pr):
key = pr.get_id() key = pr.get_id()
d = WindowModalDialog(self, _("Invoice")) d = WindowModalDialog(self, _("BIP70 Invoice"))
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
grid = QGridLayout() grid = QGridLayout()
grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0) grid.addWidget(QLabel(_("Requestor") + ':'), 0, 0)
@ -2140,7 +2203,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
vbox.addLayout(Buttons(exportButton, deleteButton, CloseButton(d))) vbox.addLayout(Buttons(exportButton, deleteButton, CloseButton(d)))
d.exec_() d.exec_()
def do_pay_invoice(self, key): def pay_bip70_invoice(self, key):
pr = self.wallet.get_invoice(key) pr = self.wallet.get_invoice(key)
self.payment_request = pr self.payment_request = pr
self.prepare_for_payment_request() self.prepare_for_payment_request()

5
electrum/gui/qt/paytoedit.py

@ -61,7 +61,6 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
self.errors = [] self.errors = []
self.is_pr = False self.is_pr = False
self.is_alias = False self.is_alias = False
self.is_lightning = False
self.update_size() self.update_size()
self.payto_address = None self.payto_address = None
self.previous_payto = '' self.previous_payto = ''
@ -143,6 +142,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
except: except:
pass pass
if self.payto_address: if self.payto_address:
self.win.set_onchain(True)
self.win.lock_amount(False) self.win.lock_amount(False)
return return
@ -153,12 +153,13 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit, Logger):
except: except:
self.errors.append((i, line.strip())) self.errors.append((i, line.strip()))
continue continue
outputs.append(output) outputs.append(output)
if output.value == '!': if output.value == '!':
is_max = True is_max = True
else: else:
total += output.value total += output.value
if outputs:
self.win.set_onchain(True)
self.win.max_button.setChecked(is_max) self.win.max_button.setChecked(is_max)
self.outputs = outputs 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.i18n import _
from electrum.util import format_time, age, get_request_status 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.util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT, pr_tooltips
from electrum.lnutil import SENT, RECEIVED from electrum.lnutil import SENT, RECEIVED
from electrum.plugin import run_hook from electrum.plugin import run_hook
@ -118,35 +118,30 @@ class RequestList(MyTreeView):
status = req.get('status') status = req.get('status')
if status == PR_PAID: if status == PR_PAID:
continue continue
is_lightning = req['type'] == PR_TYPE_LN
request_type = req['type'] request_type = req['type']
timestamp = req.get('time', 0) timestamp = req.get('time', 0)
expiration = req.get('exp', None)
amount = req.get('amount') 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) date = format_time(timestamp)
amount_str = self.parent.format_amount(amount) if amount else "" amount_str = self.parent.format_amount(amount) if amount else ""
status_str = get_request_status(req) status_str = get_request_status(req)
labels = [date, message, amount_str, status_str] 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] items = [QStandardItem(e) for e in labels]
self.set_editability(items) self.set_editability(items)
items[self.Columns.DATE].setData(request_type, ROLE_REQUEST_TYPE) 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))) items[self.Columns.STATUS].setIcon(read_QIcon(pr_icons.get(status)))
if request_type == PR_TYPE_LN: items[self.Columns.DATE].setToolTip(tooltip)
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"))
self.model().insertRow(self.model().rowCount(), items) self.model().insertRow(self.model().rowCount(), items)
self.filter() self.filter()
# sort requests by date # sort requests by date
@ -177,12 +172,10 @@ class RequestList(MyTreeView):
if column == self.Columns.AMOUNT: if column == self.Columns.AMOUNT:
column_data = column_data.strip() column_data = column_data.strip()
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.do_copy(column_title, column_data)) 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: 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: 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: if 'view_url' in req:
menu.addAction(_("View in web browser"), lambda: webopen(req['view_url'])) menu.addAction(_("View in web browser"), lambda: webopen(req['view_url']))
menu.addAction(_("Delete"), lambda: self.parent.delete_request(key)) 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 . import bitcoin, ecc, util, transaction, x509, rsakey
from .util import bh2u, bfh, export_meta, import_meta, make_aiohttp_session 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_UNPAID, PR_EXPIRED, PR_PAID, PR_UNKNOWN, PR_INFLIGHT
from .util import PR_TYPE_BIP70
from .crypto import sha256 from .crypto import sha256
from .bitcoin import TYPE_ADDRESS from .bitcoin import TYPE_ADDRESS
from .transaction import TxOutput from .transaction import TxOutput
@ -151,10 +150,6 @@ class PaymentRequest:
self.memo = self.details.memo self.memo = self.details.memo
self.payment_url = self.details.payment_url 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): def verify(self, contacts):
if self.error: if self.error:
return False return False
@ -269,19 +264,6 @@ class PaymentRequest:
def get_memo(self): def get_memo(self):
return self.memo 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): def get_id(self):
return self.id if self.requestor else self.get_address() 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 DECIMAL_POINT_DEFAULT = 5 # mBTC
# types of payment requests # types of payment requests
PR_TYPE_ADDRESS = 0 PR_TYPE_ONCHAIN = 0
PR_TYPE_BIP70= 1
PR_TYPE_LN = 2 PR_TYPE_LN = 2
# status of payment requests # status of payment requests

47
electrum/wallet.py

@ -41,12 +41,13 @@ from decimal import Decimal
from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence from typing import TYPE_CHECKING, List, Optional, Tuple, Union, NamedTuple, Sequence
from .i18n import _ from .i18n import _
from .crypto import sha256
from .util import (NotEnoughFunds, UserCancelled, profiler, from .util import (NotEnoughFunds, UserCancelled, profiler,
format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates, format_satoshis, format_fee_satoshis, NoDynamicFeeEstimates,
WalletFileException, BitcoinException, WalletFileException, BitcoinException,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis, InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, bh2u, TxMinedInfo, quantize_feerate, create_bip21_uri, OrderedDictWithIndex) 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 .simple_config import SimpleConfig
from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script, from .bitcoin import (COIN, TYPE_ADDRESS, is_address, address_to_script,
is_minikey, relayfee, dust_threshold) is_minikey, relayfee, dust_threshold)
@ -505,20 +506,45 @@ class Abstract_Wallet(AddressSynchronizer):
'txpos_in_block': hist_item.tx_mined_status.txpos, '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): def save_invoice(self, invoice):
invoice_type = invoice['type'] invoice_type = invoice['type']
if invoice_type == PR_TYPE_LN: if invoice_type == PR_TYPE_LN:
self.lnworker.save_new_invoice(invoice['invoice']) self.lnworker.save_new_invoice(invoice['invoice'])
else: elif invoice_type == PR_TYPE_ONCHAIN:
if invoice_type == PR_TYPE_ADDRESS: key = bh2u(sha256(repr(invoice))[0:16])
key = invoice['address'] invoice['id'] = key
invoice['time'] = int(time.time())
elif invoice_type == PR_TYPE_BIP70:
key = invoice['id']
invoice['txid'] = None invoice['txid'] = None
self.invoices[key] = invoice
self.storage.put('invoices', self.invoices)
self.storage.write()
else: else:
raise Exception('Unsupported invoice type') raise Exception('Unsupported invoice type')
self.invoices[key] = invoice
def clear_invoices(self):
self.invoices = {}
self.storage.put('invoices', self.invoices) self.storage.put('invoices', self.invoices)
self.storage.write() self.storage.write()
@ -1284,7 +1310,7 @@ class Abstract_Wallet(AddressSynchronizer):
if not r: if not r:
return return
out = copy.copy(r) out = copy.copy(r)
out['type'] = PR_TYPE_ADDRESS out['type'] = PR_TYPE_ONCHAIN
out['URI'] = self.get_request_URI(addr) out['URI'] = self.get_request_URI(addr)
status, conf = self.get_request_status(addr) status, conf = self.get_request_status(addr)
out['status'] = status out['status'] = status
@ -1362,9 +1388,10 @@ class Abstract_Wallet(AddressSynchronizer):
self.network.trigger_callback('payment_received', self, addr, status) self.network.trigger_callback('payment_received', self, addr, status)
def make_payment_request(self, addr, amount, message, expiration): def make_payment_request(self, addr, amount, message, expiration):
from .bitcoin import TYPE_ADDRESS
timestamp = int(time.time()) timestamp = int(time.time())
_id = bh2u(sha256d(addr + "%d"%timestamp))[0:10] _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 return r
def sign_payment_request(self, key, alias, alias_addr, password): def sign_payment_request(self, key, alias, alias_addr, password):

Loading…
Cancel
Save