Skip to content

Commit 307cf25

Browse files
committed
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.
1 parent 0d96bc1 commit 307cf25

File tree

2 files changed

+42
-25
lines changed

2 files changed

+42
-25
lines changed

electrum/gui/qt/send_tab.py

+31-18
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,14 @@ def showSpinner(self, b):
205205

206206
def on_amount_changed(self, text):
207207
# FIXME: implement full valid amount check to enable/disable Pay button
208-
pi_valid = self.payto_e.payment_identifier.is_valid() if self.payto_e.payment_identifier else False
209-
pi_error = self.payto_e.payment_identifier.is_error() if pi_valid else False
210-
self.send_button.setEnabled(bool(self.amount_e.get_amount()) and pi_valid and not pi_error)
208+
pi = self.payto_e.payment_identifier
209+
if not pi:
210+
self.send_button.setEnabled(False)
211+
return
212+
pi_error = pi.is_error() if pi.is_valid() else False
213+
is_spk_script = pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address
214+
valid_amount = is_spk_script or bool(self.amount_e.get_amount())
215+
self.send_button.setEnabled(pi.is_valid() and not pi_error and valid_amount)
211216

212217
def do_paste(self):
213218
self.logger.debug('do_paste')
@@ -224,16 +229,20 @@ def set_payment_identifier(self, text):
224229
self.show_error(_('Invalid payment identifier'))
225230

226231
def spend_max(self):
227-
if self.payto_e.payment_identifier is None:
232+
pi = self.payto_e.payment_identifier
233+
234+
if pi is None or pi.type == PaymentIdentifierType.UNKNOWN:
228235
return
229236

230-
assert self.payto_e.payment_identifier.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE,
231-
PaymentIdentifierType.BIP21, PaymentIdentifierType.OPENALIAS]
232-
assert not self.payto_e.payment_identifier.is_amount_locked()
237+
assert pi.type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE,
238+
PaymentIdentifierType.BIP21, PaymentIdentifierType.OPENALIAS]
239+
240+
if pi.type == PaymentIdentifierType.BIP21:
241+
assert 'amount' not in pi.bip21
233242

234243
if run_hook('abort_send', self):
235244
return
236-
outputs = self.payto_e.payment_identifier.get_onchain_outputs('!')
245+
outputs = pi.get_onchain_outputs('!')
237246
if not outputs:
238247
return
239248
make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction(
@@ -446,10 +455,13 @@ def update_fields(self):
446455
self.spend_max()
447456

448457
pi_unusable = pi.is_error() or (not self.wallet.has_lightning() and not pi.is_onchain())
458+
is_spk_script = pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address
459+
460+
amount_valid = is_spk_script or bool(self.amount_e.get_amount())
449461

450-
self.send_button.setEnabled(not pi_unusable and bool(self.amount_e.get_amount()) and not pi.has_expired())
451-
self.save_button.setEnabled(not pi_unusable and pi.type not in [PaymentIdentifierType.LNURLP,
452-
PaymentIdentifierType.LNADDR])
462+
self.send_button.setEnabled(not pi_unusable and amount_valid and not pi.has_expired())
463+
self.save_button.setEnabled(not pi_unusable and not is_spk_script and \
464+
pi.type not in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.LNADDR])
453465

454466
def _handle_payment_identifier(self):
455467
self.update_fields()
@@ -479,11 +491,8 @@ def get_message(self):
479491
def read_invoice(self) -> Optional[Invoice]:
480492
if self.check_payto_line_and_show_errors():
481493
return
482-
amount_sat = self.read_amount()
483-
if not amount_sat:
484-
self.show_error(_('No amount'))
485-
return
486494

495+
amount_sat = self.read_amount()
487496
invoice = invoice_from_payment_identifier(
488497
self.payto_e.payment_identifier, self.wallet, amount_sat, self.get_message())
489498
if not invoice:
@@ -551,15 +560,19 @@ def pay_multiple_invoices(self, invoices):
551560
def do_edit_invoice(self, invoice: 'Invoice'): # FIXME broken
552561
assert not bool(invoice.get_amount_sat())
553562
text = invoice.lightning_invoice if invoice.is_lightning() else invoice.get_address()
554-
self.payto_e._on_input_btn(text)
563+
self.set_payment_identifier(text)
555564
self.amount_e.setFocus()
556565
# disable save button, because it would create a new invoice
557566
self.save_button.setEnabled(False)
558567

559568
def do_pay_invoice(self, invoice: 'Invoice'):
560569
if not bool(invoice.get_amount_sat()):
561-
self.show_error(_('No amount'))
562-
return
570+
pi = self.payto_e.payment_identifier
571+
if pi.type == PaymentIdentifierType.SPK and not pi.spk_is_address:
572+
pass
573+
else:
574+
self.show_error(_('No amount'))
575+
return
563576
if invoice.is_lightning():
564577
self.pay_lightning_invoice(invoice)
565578
else:

electrum/payment_identifier.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def __init__(self, wallet: Optional['Abstract_Wallet'], text: str):
120120
self.bolt11 = None # type: Optional[Invoice]
121121
self.bip21 = None
122122
self.spk = None
123+
self.spk_is_address = False
123124
#
124125
self.emaillike = None
125126
self.domainlike = None
@@ -258,9 +259,11 @@ def parse(self, text: str):
258259
except InvoiceError as e:
259260
self.logger.debug(self._get_error_from_invoiceerror(e))
260261
self.set_state(PaymentIdentifierState.AVAILABLE)
261-
elif scriptpubkey := self.parse_output(text):
262+
elif self.parse_output(text)[0]:
263+
scriptpubkey, is_address = self.parse_output(text)
262264
self._type = PaymentIdentifierType.SPK
263265
self.spk = scriptpubkey
266+
self.spk_is_address = is_address
264267
self.set_state(PaymentIdentifierState.AVAILABLE)
265268
elif self.contacts and (contact := self.contacts.by_name(text)):
266269
if contact['type'] == 'address':
@@ -464,7 +467,8 @@ def get_onchain_outputs(self, amount):
464467
return [PartialTxOutput(scriptpubkey=self.spk, value=amount)]
465468
elif self.bip21:
466469
address = self.bip21.get('address')
467-
scriptpubkey = self.parse_output(address)
470+
scriptpubkey, is_address = self.parse_output(address)
471+
assert is_address # unlikely, but make sure it is an address, not a script
468472
return [PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)]
469473
else:
470474
raise Exception('not onchain')
@@ -499,25 +503,25 @@ def parse_address_and_amount(self, line: str) -> 'PartialTxOutput':
499503
x, y = line.split(',')
500504
except ValueError:
501505
raise Exception("expected two comma-separated values: (address, amount)") from None
502-
scriptpubkey = self.parse_output(x)
506+
scriptpubkey, is_address = self.parse_output(x)
503507
if not scriptpubkey:
504508
raise Exception('Invalid address')
505509
amount = self.parse_amount(y)
506510
return PartialTxOutput(scriptpubkey=scriptpubkey, value=amount)
507511

508-
def parse_output(self, x: str) -> bytes:
512+
def parse_output(self, x: str) -> Tuple[bytes, bool]:
509513
try:
510514
address = self.parse_address(x)
511-
return bytes.fromhex(bitcoin.address_to_script(address))
515+
return bytes.fromhex(bitcoin.address_to_script(address)), True
512516
except Exception as e:
513517
pass
514518
try:
515519
script = self.parse_script(x)
516-
return bytes.fromhex(script)
520+
return bytes.fromhex(script), False
517521
except Exception as e:
518522
pass
519523

520-
# raise Exception("Invalid address or script.")
524+
return None, False
521525

522526
def parse_script(self, x: str):
523527
script = ''

0 commit comments

Comments
 (0)