Skip to content

Commit 3076b38

Browse files
author
kratm2
committed
refactor payment_identifier
# Conflicts: # electrum/gui/qt/send_tab.py
1 parent 7a53a8b commit 3076b38

File tree

3 files changed

+41
-25
lines changed

3 files changed

+41
-25
lines changed

electrum/gui/qt/send_tab.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ def spend_max(self):
246246
if pi is None or pi.type == PaymentIdentifierType.UNKNOWN:
247247
return
248248
elif pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE,
249-
PaymentIdentifierType.BIP21, PaymentIdentifierType.OPENALIAS]:
249+
PaymentIdentifierType.SILENT_PAYMENT,
250+
PaymentIdentifierType.BIP21, PaymentIdentifierType.OPENALIAS]:
250251
# clear the amount field once it is clear this PI is not eligible for '!'
251252
self.amount_e.clear()
252253
return
@@ -454,14 +455,15 @@ def update_fields(self):
454455
PaymentIdentifierType.OPENALIAS, PaymentIdentifierType.BIP70,
455456
PaymentIdentifierType.BIP21, PaymentIdentifierType.BOLT11] and not pi.need_resolve()
456457
lock_amount = pi.is_amount_locked()
457-
lock_max = lock_amount or pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21]
458+
lock_max = lock_amount or pi.type not in [PaymentIdentifierType.SPK, PaymentIdentifierType.BIP21,
459+
PaymentIdentifierType.SILENT_PAYMENT]
458460

459461
self.lock_fields(lock_recipient=lock_recipient,
460462
lock_amount=lock_amount,
461463
lock_max=lock_max,
462464
lock_description=False)
463465
if lock_recipient:
464-
fields = pi.get_fields_for_GUI(bip21_prefer_fallback=not self.wallet_can_send_sp)
466+
fields = pi.get_fields_for_GUI()
465467
if fields.recipient:
466468
self.payto_e.setText(fields.recipient)
467469
if fields.description:

electrum/payment_identifier.py

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class PaymentIdentifierType(IntEnum):
8585
OPENALIAS = 8
8686
LNADDR = 9
8787
DOMAINLIKE = 10
88+
SILENT_PAYMENT = 11
8889

8990

9091
class FieldsForGUI(NamedTuple):
@@ -138,6 +139,8 @@ def __init__(self, wallet: Optional['Abstract_Wallet'], text: str):
138139
#
139140
self.lnurl = None
140141
self.lnurl_data = None
142+
#
143+
self.sp_address = None # used when a single silent payment is identified
141144

142145
self.parse(text)
143146

@@ -173,7 +176,7 @@ def is_lightning(self):
173176

174177
def is_onchain(self):
175178
if self._type in [PaymentIdentifierType.SPK, PaymentIdentifierType.MULTILINE, PaymentIdentifierType.BIP70,
176-
PaymentIdentifierType.OPENALIAS]:
179+
PaymentIdentifierType.OPENALIAS, PaymentIdentifierType.SILENT_PAYMENT]:
177180
return True
178181
if self._type in [PaymentIdentifierType.LNURLP, PaymentIdentifierType.BOLT11, PaymentIdentifierType.LNADDR]:
179182
return bool(self.bolt11) and bool(self.bolt11.get_address())
@@ -275,12 +278,23 @@ def parse(self, text: str):
275278
# no address, no bolt11 and no silent payment address, invalid
276279
self.set_state(PaymentIdentifierState.INVALID)
277280
return
281+
elif self.bip21.get('address') == DummyAddress.SILENT_PAYMENT:
282+
self.set_state(PaymentIdentifierState.INVALID)
283+
return # user manually entered DummyAddress.SILENT_PAYMENT
278284
self.set_state(PaymentIdentifierState.AVAILABLE)
279285
elif self.parse_output(text)[0]:
280286
scriptpubkey, is_address = self.parse_output(text)
281-
self._type = PaymentIdentifierType.SPK
282-
self.spk = scriptpubkey
283-
self.spk_is_address = is_address
287+
if bitcoin.script_to_address(scriptpubkey) == DummyAddress.SILENT_PAYMENT:
288+
sp_address = self.parse_address(text)
289+
if sp_address == DummyAddress.SILENT_PAYMENT:
290+
self.set_state(PaymentIdentifierState.INVALID) # user manually entered DummyAddress.SILENT_PAYMENT
291+
return
292+
self.sp_address = sp_address
293+
self._type = PaymentIdentifierType.SILENT_PAYMENT
294+
else:
295+
self._type = PaymentIdentifierType.SPK
296+
self.spk = scriptpubkey
297+
self.spk_is_address = is_address
284298
self.set_state(PaymentIdentifierState.AVAILABLE)
285299
elif self.contacts and (contact := self.contacts.by_name(text)):
286300
if contact['type'] in ('address', 'sp_address'):
@@ -468,23 +482,18 @@ async def _do_notify_merchant(
468482
if on_finished:
469483
on_finished(self)
470484

471-
def get_onchain_outputs(self, amount, bip21_use_fallback=False):
485+
def get_onchain_outputs(self, amount, allow_silent_payment=True):
472486
if self.bip70:
473487
return self.bip70_data.get_outputs()
474488
elif self.multiline_outputs:
475489
return self.multiline_outputs
476490
elif self.spk:
477-
output = PartialTxOutput(scriptpubkey=self.spk, value=amount)
478-
if output.address == DummyAddress.SILENT_PAYMENT:
479-
sp_addr = self.parse_address(self.text) # parse again if e.g. a contact was entered
480-
output.sp_addr = SilentPaymentAddress(sp_addr)
481-
return [output]
491+
return [PartialTxOutput(scriptpubkey=self.spk, value=amount)]
482492
elif self.bip21:
483493
address = self.bip21.get('address') # fallback address if sp_address is present
484-
sp_address = self.bip21.get(constants.net.BIP352_HRP)
485494

486-
if sp_address:
487-
if bip21_use_fallback:
495+
if sp_address := self.bip21.get(constants.net.BIP352_HRP):
496+
if not allow_silent_payment:
488497
if not address:
489498
raise MissingFallbackAddress('requested BIP21 fallback address but none was provided.')
490499
# fallback is requested and present, keep using `address`
@@ -497,6 +506,10 @@ def get_onchain_outputs(self, amount, bip21_use_fallback=False):
497506
if output.address == DummyAddress.SILENT_PAYMENT:
498507
output.sp_addr = SilentPaymentAddress(address)
499508
return [output]
509+
elif self.sp_address:
510+
output = PartialTxOutput.from_address_and_value(address=DummyAddress.SILENT_PAYMENT, value=amount)
511+
output.sp_addr = SilentPaymentAddress(self.sp_address)
512+
return [output]
500513
else:
501514
raise Exception('not onchain')
502515

@@ -595,7 +608,7 @@ def _get_error_from_invoiceerror(self, e: 'InvoiceError') -> str:
595608
error = _("Invoice requires unknown or incompatible Lightning feature") + f":\n{e!r}"
596609
return error
597610

598-
def get_fields_for_GUI(self, *, bip21_prefer_fallback=False) -> FieldsForGUI:
611+
def get_fields_for_GUI(self) -> FieldsForGUI:
599612
recipient = None
600613
amount = None
601614
description = None
@@ -647,8 +660,7 @@ def get_fields_for_GUI(self, *, bip21_prefer_fallback=False) -> FieldsForGUI:
647660
elif self.bip21:
648661
label = self.bip21.get('label')
649662
address = self.bip21.get('address')
650-
sp_address = self.bip21.get(constants.net.BIP352_HRP)
651-
if sp_address and not (address and bip21_prefer_fallback): # return fallback address if provided and needed
663+
if sp_address := self.bip21.get(constants.net.BIP352_HRP):
652664
address = sp_address
653665
recipient = f'{label} <{address}>' if label else address
654666
amount = self.bip21.get('amount')
@@ -700,7 +712,7 @@ def has_expired(self):
700712
def involves_silent_payments(self, wallet_can_send_sp=True) -> bool:
701713
try:
702714
return any(o.is_silent_payment() for o
703-
in self.get_onchain_outputs(0, bip21_use_fallback=not wallet_can_send_sp))
715+
in self.get_onchain_outputs(0, allow_silent_payment=wallet_can_send_sp))
704716
except MissingFallbackAddress:
705717
# BIP21 URI contained only a silent payment address, and the wallet cannot send to it.
706718
# Since no fallback address was provided, we treat this as involving silent payments.
@@ -726,7 +738,7 @@ def invoice_from_payment_identifier(
726738
invoice.set_amount_msat(int(amount_sat * 1000))
727739
return invoice
728740
else:
729-
outputs = pi.get_onchain_outputs(amount_sat, bip21_use_fallback=not wallet.can_send_silent_payment())
741+
outputs = pi.get_onchain_outputs(amount_sat, allow_silent_payment=wallet.can_send_silent_payment())
730742
message = pi.bip21.get('message') if pi.bip21 else message
731743
bip70_data = pi.bip70_data if pi.bip70 else None
732744
return wallet.create_invoice(

tests/test_payment_identifier.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,14 +313,16 @@ def test_silent_payment_spk(self):
313313
f'{sp_addr} ',
314314
f' {sp_addr}',
315315
f'{sp_addr.upper()} ',
316+
f'some label<{sp_addr}>'
316317
]:
317318
pi = PaymentIdentifier(None, pi_str)
318319
self.assertTrue(pi.is_valid())
319320
self.assertTrue(pi.is_available())
320-
self.assertTrue(script_to_address(pi.spk) == DummyAddress.SILENT_PAYMENT)
321-
self.assertTrue(pi.spk_is_address) # we treat it as is_address to make sure an amount is set
321+
self.assertEqual(pi.sp_address.lower(), sp_addr.lower())
322+
self.assertEqual(pi.type, PaymentIdentifierType.SILENT_PAYMENT) # we treat it as is_address to make sure an amount is set
322323
self.assertTrue(pi.involves_silent_payments())
323324
self.assertTrue(pi.get_onchain_outputs(0)[0].is_silent_payment())
325+
self.assertEqual(pi.get_onchain_outputs(0)[0].address, DummyAddress.SILENT_PAYMENT)
324326

325327
def test_silent_payment_multiline(self):
326328
# sp_addr with same B_Scan
@@ -386,7 +388,7 @@ def test_silent_payment_bip21(self):
386388
self.assertTrue(pi.involves_silent_payments(True)) # wallet is sp-capable
387389
self.assertTrue(pi.involves_silent_payments(False)) # wallet is not sp-capable, but there is no fallback -> involves
388390
# Raise in get_onchain_outputs if fallback is requested but non is present
389-
self.assertRaises(MissingFallbackAddress, pi.get_onchain_outputs, 0, bip21_use_fallback=True)
391+
self.assertRaises(MissingFallbackAddress, pi.get_onchain_outputs, 0, allow_silent_payment=False)
390392

391393
# test with fallback in context where wallet is silent payment capable
392394
bip21 = 'bitcoin:1RustyRX2oai4EYYDpQGWvEL62BBGqN9T?sp=sp1qqvwfct0plnus9vnyd08tvvcwq49g7xfjt3fnwcyu5zc29fj969fg7q4ffc6dnhl9anhec779az46rstpp0t6kzxqmg4tkelfhrejl532ycfaxvsj&message=sp_unit_test&amount=0.001'
@@ -398,6 +400,6 @@ def test_silent_payment_bip21(self):
398400
self.assertFalse(pi.involves_silent_payments(False)) # wallet is not sp-capable, so fallback is taken -> no sp-involvement
399401
# make sure fallback is taken from get_onchain_outputs if wallet can not send sp
400402
self.assertEqual(
401-
pi.get_onchain_outputs(0, bip21_use_fallback=True)[0].address,
403+
pi.get_onchain_outputs(0, allow_silent_payment=False)[0].address,
402404
'1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'
403405
)

0 commit comments

Comments
 (0)