tests: add more lnurl testing for PaymentIdentifier
What changed, and why it matters
This commit only adds new automated tests for how Electrum parses and resolves LNURL payment strings. It does not change any production wallet code, so it cannot introduce a security vulnerability or fix one on its own.
No security action required; review as normal test coverage improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff expands tests/test_payment_identifier.py with additional unit tests for LNURL parsing, LNURL-pay (LNURL6) resolution, LNURL-withdraw (LNURL3) resolution, and error handling, using mocked request_lnurl responses. No runtime/payment logic is modified.
Changed components
tests/test_payment_identifier.pyInspect captured patch +107 / −5
diff --git a/tests/test_payment_identifier.py b/tests/test_payment_identifier.py
index 6f4b4f5..04f59d9 100644
--- a/tests/test_payment_identifier.py
+++ b/tests/test_payment_identifier.py
@@ -1,9 +1,13 @@
import os
+import asyncio
+from unittest.mock import patch
from electrum import SimpleConfig
from electrum.invoices import Invoice
from electrum.payment_identifier import (maybe_extract_lightning_payment_identifier, PaymentIdentifier,
- PaymentIdentifierType, invoice_from_payment_identifier)
+ PaymentIdentifierType, PaymentIdentifierState,
+ invoice_from_payment_identifier)
+from electrum.lnurl import LNURL6Data, LNURL3Data, LNURLError
from electrum.transaction import PartialTxOutput
from . import ElectrumTestCase
@@ -143,14 +147,112 @@ class TestPaymentIdentifier(ElectrumTestCase):
pi = PaymentIdentifier(None, bip21)
self.assertFalse(pi.is_valid())
- def test_lnurl(self):
- lnurl = 'lnurl1dp68gurn8ghj7um9wfmxjcm99e5k7telwy7nxenrxvmrgdtzxsenjcm98pjnwxq96s9'
- pi = PaymentIdentifier(None, lnurl)
+ def test_lnurl_basic(self):
+ """Test basic LNURL parsing without resolve"""
+ valid_lnurl = 'lnurl1dp68gurn8ghj7um9wfmxjcm99e5k7telwy7nxenrxvmrgdtzxsenjcm98pjnwxq96s9'
+ pi = PaymentIdentifier(None, valid_lnurl)
self.assertTrue(pi.is_valid())
+ self.assertEqual(PaymentIdentifierType.LNURL, pi.type)
self.assertFalse(pi.is_available())
self.assertTrue(pi.need_resolve())
+ self.assertEqual(PaymentIdentifierState.NEED_RESOLVE, pi.state)
+
+ # Test with lightning: prefix
+ lightning_lnurl = f'lightning:{valid_lnurl}'
+ pi = PaymentIdentifier(None, lightning_lnurl)
+ self.assertTrue(pi.is_valid())
+ self.assertEqual(PaymentIdentifierType.LNURL, pi.type)
+ self.assertTrue(pi.need_resolve())
+
+ @patch('electrum.payment_identifier.request_lnurl')
+ def test_lnurl_pay_resolve(self, mock_request_lnurl):
+ """Test LNURL-pay (LNURL6) with mocked resolve"""
+ valid_lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TMVDE6HYMRS9ANRV46DXETQPJQCS4'
+
+ # Mock lnurl-p response
+ mock_lnurl6_data = LNURL6Data(
+ callback_url='https://example.com/lnurl-pay',
+ max_sendable_sat=1_000_000,
+ min_sendable_sat=1_000,
+ metadata_plaintext='Test payment',
+ comment_allowed=100,
+ )
+ mock_request_lnurl.return_value = mock_lnurl6_data
+
+ pi = PaymentIdentifier(None, valid_lnurl)
+ self.assertTrue(pi.need_resolve())
+ self.assertEqual(PaymentIdentifierType.LNURL, pi.type)
+
+ async def run_resolve():
+ await pi._do_resolve()
+
+ asyncio.run(run_resolve())
+
+ self.assertEqual(PaymentIdentifierType.LNURLP, pi.type)
+ self.assertEqual(PaymentIdentifierState.LNURLP_FINALIZE, pi.state)
+ self.assertTrue(pi.need_finalize())
+ self.assertIsNotNone(pi.lnurl_data)
+ self.assertTrue(isinstance(pi.lnurl_data, LNURL6Data))
+ self.assertEqual(1_000, pi.lnurl_data.min_sendable_sat)
+ self.assertEqual(1_000_000, pi.lnurl_data.max_sendable_sat)
+ self.assertEqual('Test payment', pi.lnurl_data.metadata_plaintext)
+ self.assertEqual(100, pi.lnurl_data.comment_allowed)
+
+ @patch('electrum.payment_identifier.request_lnurl')
+ def test_lnurl_withdraw_resolve(self, mock_request_lnurl):
+ """Test LNURL-withdraw (LNURL3) with mocked resolve"""
+ valid_lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TM4WPNHYCTYV4EJ7DFCVGENSDPH8QCRZETXVGCXGCMPVFJR' \
+ 'WENP8P3NJEP3XE3NQWRPXFJR2VRRVSCX2V33V5UNVC3SXP3RXCFSVFSKVWPCV3SKZWTP8YUZ7AMFW35XGUNPWUHKZURF9AMRZT' \
+ 'MVDE6HYMP0FETHVUNZDAMHQ7JSF4RX73TZ2VU9Z3J3GVMSLCJ57F'
+
+ # Mock lnurl-w response
+ mock_lnurl3_data = LNURL3Data(
+ callback_url='https://example.com/lnurl-withdraw',
+ k1='test-k1-value',
+ default_description='Test withdrawal',
+ min_withdrawable_sat=1_000,
+ max_withdrawable_sat=500_000,
+ )
+ mock_request_lnurl.return_value = mock_lnurl3_data
+
+ pi = PaymentIdentifier(None, valid_lnurl)
+ self.assertTrue(pi.need_resolve())
+ self.assertEqual(PaymentIdentifierType.LNURL, pi.type)
+
+ async def run_resolve():
+ await pi._do_resolve()
+
+ asyncio.run(run_resolve())
+
+ self.assertEqual(PaymentIdentifierType.LNURLW, pi.type)
+ self.assertEqual(PaymentIdentifierState.LNURLW_FINALIZE, pi.state)
+ self.assertIsNotNone(pi.lnurl_data)
+ self.assertEqual('test-k1-value', pi.lnurl_data.k1)
+ self.assertEqual('Test withdrawal', pi.lnurl_data.default_description)
+ self.assertEqual(1000, pi.lnurl_data.min_withdrawable_sat)
+ self.assertEqual(500000, pi.lnurl_data.max_withdrawable_sat)
+
+ @patch('electrum.payment_identifier.request_lnurl')
+ def test_lnurl_resolve_error(self, mock_request_lnurl):
+ """Test LNURL resolve error handling"""
+ lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TM4WPNHYCTYV4EJ7DFCVGENSDPH8QCRZETXVGCXGCMPVFJR' \
+ 'WENP8P3NJEP3XE3NQWRPXFJR2VRRVSCX2V33V5UNVC3SXP3RXCFSVFSKVWPCV3SKZWTP8YUZ7AMFW35XGUNPWUHKZURF9AMRZT' \
+ 'MVDE6HYMP0FETHVUNZDAMHQ7JSF4RX73TZ2VU9Z3J3GVMSLCJ57F'
+
+ # Mock LNURL error
+ mock_request_lnurl.side_effect = LNURLError("Server error")
+
+ pi = PaymentIdentifier(None, lnurl)
+ self.assertTrue(pi.need_resolve())
+
+ async def run_resolve():
+ await pi._do_resolve()
+
+ asyncio.run(run_resolve())
- # TODO: resolve mock
+ self.assertEqual(PaymentIdentifierState.ERROR, pi.state)
+ self.assertTrue(pi.is_error())
+ self.assertIn("Server error", pi.get_error())
def test_multiline(self):
pi_str = '\n'.join([
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.