Browse Source

register distinction between address and script for SPK type payment identifiers and allow zero amount for

script destinations.

This is mainly to support OP_RETURN outputs, which typically have a zero amount output value,
but as we don't special case OP_RETURN, this is currently done for all non-address scripts

Also, it's probably good to add a warning popup for OP_RETURN outputs with a non-zero output value, but this
would also need special casing for OP_RETURN.

Saving of script output payment identifiers is disabled for now, as reading the script from the stored invoice
back into human-readable form is currently not implemented, and currently only lightning invoices or address output
is supported.
master
Sander van Grieken 2 years ago
parent
commit
307cf25fd4
  1. 49
      electrum/gui/qt/send_tab.py
  2. 18
      electrum/payment_identifier.py

49
electrum/gui/qt/send_tab.py

@ -205,9 +205,14 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def on_amount_changed(self, text): def on_amount_changed(self, text):
# FIXME: implement full valid amount check to enable/disable Pay button # FIXME: implement full valid amount check to enable/disable Pay button
pi_valid = self.payto_e.payment_identifier.is_valid() if self.payto_e.payment_identifier else False pi = self.payto_e.payment_identifier
pi_error = self.payto_e.payment_identifier.is_error() if pi_valid else False if not pi:
self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid and not pi_error) self.send_button.setEnabled(False)
return
pi_error = pi.is_error() if pi.is_valid() else False
is_spk_script = pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address
valid_amount = is_spk_script or bool(self.amount_e.get_amount())
self.send_button.setEnabled(pi.is_valid() and not pi_error and valid_amount)
def do_paste(self): def do_paste(self):
self.logger.debug('do_paste') self.logger.debug('do_paste')
@ -224,16 +229,20 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.show_error(_('Invalid payment identifier')) self.show_error(_('Invalid payment identifier'))
def spend_max(self): def spend_max(self):
if self.payto_e.payment_identifier is None: pi = self.payto_e.payment_identifier
if pi is None or pi.type == PaymentIdentifierType.UNKNOWN:
return return
assert self.payto_e.payment_identifier.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE, assert pi.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE,
PaymentIdentifierType.BIP21, PaymentIdentifierType.OPENALIAS] PaymentIdentifierType.BIP21, PaymentIdentifierType.OPENALIAS]
assert not self.payto_e.payment_identifier.is_amount_locked()
if pi.type == PaymentIdentifierType.BIP21:
assert 'amount' not in pi.bip21
if run_hook('abort_send', self): if run_hook('abort_send', self):
return return
outputs = self.payto_e.payment_identifier.get_onchain_outputs('!') outputs = pi.get_onchain_outputs('!')
if not outputs: if not outputs:
return return
make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction( make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction(
@ -446,10 +455,13 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
self.spend_max() self.spend_max()
pi_unusable = pi.is_error() or (not self.wallet.has_lightning() and not pi.is_onchain()) pi_unusable = pi.is_error() or (not self.wallet.has_lightning() and not pi.is_onchain())
is_spk_script = pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address
amount_valid = is_spk_script or bool(self.amount_e.get_amount())
self.send_button.setEnabled(not pi_unusable and bool(self.amount_e.get_amount()) and not pi.has_expired()) self.send_button.setEnabled(not pi_unusable and amount_valid and not pi.has_expired())
self.save_button.setEnabled(not pi_unusable and pi.type not in [PaymentIdentifierType.LNURLP, self.save_button.setEnabled(not pi_unusable and not is_spk_script and \
PaymentIdentifierType.LNADDR]) pi.type not in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR])
def _handle_payment_identifier(self): def _handle_payment_identifier(self):
self.update_fields() self.update_fields()
@ -479,11 +491,8 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def read_invoice(self) -> Optional[Invoice]: def read_invoice(self) -> Optional[Invoice]:
if self.check_payto_line_and_show_errors(): if self.check_payto_line_and_show_errors():
return return
amount_sat = self.read_amount()
if not amount_sat:
self.show_error(_('No amount'))
return
amount_sat = self.read_amount()
invoice = invoice_from_payment_identifier( invoice = invoice_from_payment_identifier(
self.payto_e.payment_identifier, self.wallet, amount_sat, self.get_message()) self.payto_e.payment_identifier, self.wallet, amount_sat, self.get_message())
if not invoice: if not invoice:
@ -551,15 +560,19 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def do_edit_invoice(self, invoice: 'Invoice'): # FIXME broken def do_edit_invoice(self, invoice: 'Invoice'): # FIXME broken
assert not bool(invoice.get_amount_sat()) assert not bool(invoice.get_amount_sat())
text = invoice.lightning_invoice if invoice.is_lightning() else invoice.get_address() text = invoice.lightning_invoice if invoice.is_lightning() else invoice.get_address()
self.payto_e._on_input_btn(text) self.set_payment_identifier(text)
self.amount_e.setFocus() self.amount_e.setFocus()
# disable save button, because it would create a new invoice # disable save button, because it would create a new invoice
self.save_button.setEnabled(False) self.save_button.setEnabled(False)
def do_pay_invoice(self, invoice: 'Invoice'): def do_pay_invoice(self, invoice: 'Invoice'):
if not bool(invoice.get_amount_sat()): if not bool(invoice.get_amount_sat()):
self.show_error(_('No amount')) pi = self.payto_e.payment_identifier
return if pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address:
pass
else:
self.show_error(_('No amount'))
return
if invoice.is_lightning(): if invoice.is_lightning():
self.pay_lightning_invoice(invoice) self.pay_lightning_invoice(invoice)
else: else:

18
electrum/payment_identifier.py

@ -120,6 +120,7 @@ class PaymentIdentifier(Logger):
self.bolt11 = None # type: Optional[Invoice] self.bolt11 = None # type: Optional[Invoice]
self.bip21 = None self.bip21 = None
self.spk = None self.spk = None
self.spk_is_address = False
# #
self.emaillike = None self.emaillike = None
self.domainlike = None self.domainlike = None
@ -258,9 +259,11 @@ class PaymentIdentifier(Logger):
except InvoiceError as e: except InvoiceError as e:
self.logger.debug(self._get_error_from_invoiceerror(e)) self.logger.debug(self._get_error_from_invoiceerror(e))
self.set_state(PaymentIdentifierState.AVAILABLE) self.set_state(PaymentIdentifierState.AVAILABLE)
elif scriptpubkey := self.parse_output(text): elif self.parse_output(text)[0]:
scriptpubkey, is_address = self.parse_output(text)
self._type = PaymentIdentifierType.SPK self._type = PaymentIdentifierType.SPK
self.spk = scriptpubkey self.spk = scriptpubkey
self.spk_is_address = is_address
self.set_state(PaymentIdentifierState.AVAILABLE) self.set_state(PaymentIdentifierState.AVAILABLE)
elif self.contacts and (contact := self.contacts.by_name(text)): elif self.contacts and (contact := self.contacts.by_name(text)):
if contact['type'] == 'address': if contact['type'] == 'address':
@ -464,7 +467,8 @@ class PaymentIdentifier(Logger):
return [PartialTxOutput(scriptpubkey=self.spk, value=amount)] return [PartialTxOutput(scriptpubkey=self.spk, value=amount)]
elif self.bip21: elif self.bip21:
address = self.bip21.get('address') address = self.bip21.get('address')
scriptpubkey = self.parse_output(address) scriptpubkey, is_address = self.parse_output(address)
assert is_address # unlikely, but make sure it is an address, not a script
return [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)] return [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)]
else: else:
raise Exception('not onchain') raise Exception('not onchain')
@ -499,25 +503,25 @@ class PaymentIdentifier(Logger):
x, y = line.split(',') x, y = line.split(',')
except ValueError: except ValueError:
raise Exception("expected two comma-separated values: (address, amount)") from None raise Exception("expected two comma-separated values: (address, amount)") from None
scriptpubkey = self.parse_output(x) scriptpubkey, is_address = self.parse_output(x)
if not scriptpubkey: if not scriptpubkey:
raise Exception('Invalid address') raise Exception('Invalid address')
amount = self.parse_amount(y) amount = self.parse_amount(y)
return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount) return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)
def parse_output(self, x: str) -> bytes: def parse_output(self, x: str) -> Tuple[bytes, bool]:
try: try:
address = self.parse_address(x) address = self.parse_address(x)
return bytes.fromhex(bitcoin.address_to_script(address)) return bytes.fromhex(bitcoin.address_to_script(address)), True
except Exception as e: except Exception as e:
pass pass
try: try:
script = self.parse_script(x) script = self.parse_script(x)
return bytes.fromhex(script) return bytes.fromhex(script), False
except Exception as e: except Exception as e:
pass pass
# raise Exception("Invalid address or script.") return None, False
def parse_script(self, x: str): def parse_script(self, x: str):
script = '' script = ''

Loading…
Cancel
Save