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.
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
self.send_button.setEnabled(not pi_unusable and bool(self.amount_e.get_amount()) and not pi.has_expired())
|
amount_valid = is_spk_script or bool(self.amount_e.get_amount())
|
||||||
self.save_button.setEnabled(not pi_unusable and pi.type not in [PaymentIdentifierType.LNURLP,
|
|
||||||
PaymentIdentifierType.LNADDR])
|
self.send_button.setEnabled(not pi_unusable and amount_valid and not pi.has_expired())
|
||||||
|
self.save_button.setEnabled(not pi_unusable and not is_spk_script and \
|
||||||
|
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:
|
||||||
|
|||||||
@@ -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 = ''
|
||||||
|
|||||||
Reference in New Issue
Block a user