|
1 | 1 | import os |
| 2 | +import asyncio |
| 3 | +from unittest.mock import patch |
2 | 4 |
|
3 | 5 | from electrum import SimpleConfig |
4 | 6 | from electrum.invoices import Invoice |
5 | 7 | from electrum.payment_identifier import (maybe_extract_lightning_payment_identifier, PaymentIdentifier, |
6 | | - PaymentIdentifierType, invoice_from_payment_identifier) |
| 8 | + PaymentIdentifierType, PaymentIdentifierState, |
| 9 | + invoice_from_payment_identifier) |
7 | 10 | from electrum.wallet import restore_wallet_from_text |
| 11 | +from electrum.lnurl import LNURL6Data, LNURL3Data, LNURLError |
8 | 12 |
|
9 | 13 | from . import ElectrumTestCase |
10 | 14 | from electrum.transaction import PartialTxOutput |
@@ -143,14 +147,112 @@ def test_bip21(self): |
143 | 147 | pi = PaymentIdentifier(None, bip21) |
144 | 148 | self.assertFalse(pi.is_valid()) |
145 | 149 |
|
146 | | - def test_lnurl(self): |
147 | | - lnurl = 'lnurl1dp68gurn8ghj7um9wfmxjcm99e5k7telwy7nxenrxvmrgdtzxsenjcm98pjnwxq96s9' |
148 | | - pi = PaymentIdentifier(None, lnurl) |
| 150 | + def test_lnurl_basic(self): |
| 151 | + """Test basic LNURL parsing without resolve""" |
| 152 | + valid_lnurl = 'lnurl1dp68gurn8ghj7um9wfmxjcm99e5k7telwy7nxenrxvmrgdtzxsenjcm98pjnwxq96s9' |
| 153 | + pi = PaymentIdentifier(None, valid_lnurl) |
149 | 154 | self.assertTrue(pi.is_valid()) |
| 155 | + self.assertEqual(PaymentIdentifierType.LNURL, pi.type) |
150 | 156 | self.assertFalse(pi.is_available()) |
151 | 157 | self.assertTrue(pi.need_resolve()) |
| 158 | + self.assertEqual(PaymentIdentifierState.NEED_RESOLVE, pi.state) |
| 159 | + |
| 160 | + # Test with lightning: prefix |
| 161 | + lightning_lnurl = f'lightning:{valid_lnurl}' |
| 162 | + pi = PaymentIdentifier(None, lightning_lnurl) |
| 163 | + self.assertTrue(pi.is_valid()) |
| 164 | + self.assertEqual(PaymentIdentifierType.LNURL, pi.type) |
| 165 | + self.assertTrue(pi.need_resolve()) |
| 166 | + |
| 167 | + @patch('electrum.payment_identifier.request_lnurl') |
| 168 | + def test_lnurl_pay_resolve(self, mock_request_lnurl): |
| 169 | + """Test LNURL-pay (LNURL6) with mocked resolve""" |
| 170 | + valid_lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TMVDE6HYMRS9ANRV46DXETQPJQCS4' |
| 171 | + |
| 172 | + # Mock lnurl-p response |
| 173 | + mock_lnurl6_data = LNURL6Data( |
| 174 | + callback_url='https://example.com/lnurl-pay', |
| 175 | + max_sendable_sat=1_000_000, |
| 176 | + min_sendable_sat=1_000, |
| 177 | + metadata_plaintext='Test payment', |
| 178 | + comment_allowed=100, |
| 179 | + ) |
| 180 | + mock_request_lnurl.return_value = mock_lnurl6_data |
| 181 | + |
| 182 | + pi = PaymentIdentifier(None, valid_lnurl) |
| 183 | + self.assertTrue(pi.need_resolve()) |
| 184 | + self.assertEqual(PaymentIdentifierType.LNURL, pi.type) |
| 185 | + |
| 186 | + async def run_resolve(): |
| 187 | + await pi._do_resolve() |
| 188 | + |
| 189 | + asyncio.run(run_resolve()) |
| 190 | + |
| 191 | + self.assertEqual(PaymentIdentifierType.LNURLP, pi.type) |
| 192 | + self.assertEqual(PaymentIdentifierState.LNURLP_FINALIZE, pi.state) |
| 193 | + self.assertTrue(pi.need_finalize()) |
| 194 | + self.assertIsNotNone(pi.lnurl_data) |
| 195 | + self.assertTrue(isinstance(pi.lnurl_data, LNURL6Data)) |
| 196 | + self.assertEqual(1_000, pi.lnurl_data.min_sendable_sat) |
| 197 | + self.assertEqual(1_000_000, pi.lnurl_data.max_sendable_sat) |
| 198 | + self.assertEqual('Test payment', pi.lnurl_data.metadata_plaintext) |
| 199 | + self.assertEqual(100, pi.lnurl_data.comment_allowed) |
| 200 | + |
| 201 | + @patch('electrum.payment_identifier.request_lnurl') |
| 202 | + def test_lnurl_withdraw_resolve(self, mock_request_lnurl): |
| 203 | + """Test LNURL-withdraw (LNURL3) with mocked resolve""" |
| 204 | + valid_lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TM4WPNHYCTYV4EJ7DFCVGENSDPH8QCRZETXVGCXGCMPVFJR' \ |
| 205 | + 'WENP8P3NJEP3XE3NQWRPXFJR2VRRVSCX2V33V5UNVC3SXP3RXCFSVFSKVWPCV3SKZWTP8YUZ7AMFW35XGUNPWUHKZURF9AMRZT' \ |
| 206 | + 'MVDE6HYMP0FETHVUNZDAMHQ7JSF4RX73TZ2VU9Z3J3GVMSLCJ57F' |
| 207 | + |
| 208 | + # Mock lnurl-w response |
| 209 | + mock_lnurl3_data = LNURL3Data( |
| 210 | + callback_url='https://example.com/lnurl-withdraw', |
| 211 | + k1='test-k1-value', |
| 212 | + default_description='Test withdrawal', |
| 213 | + min_withdrawable_sat=1_000, |
| 214 | + max_withdrawable_sat=500_000, |
| 215 | + ) |
| 216 | + mock_request_lnurl.return_value = mock_lnurl3_data |
| 217 | + |
| 218 | + pi = PaymentIdentifier(None, valid_lnurl) |
| 219 | + self.assertTrue(pi.need_resolve()) |
| 220 | + self.assertEqual(PaymentIdentifierType.LNURL, pi.type) |
| 221 | + |
| 222 | + async def run_resolve(): |
| 223 | + await pi._do_resolve() |
| 224 | + |
| 225 | + asyncio.run(run_resolve()) |
| 226 | + |
| 227 | + self.assertEqual(PaymentIdentifierType.LNURLW, pi.type) |
| 228 | + self.assertEqual(PaymentIdentifierState.LNURLW_FINALIZE, pi.state) |
| 229 | + self.assertIsNotNone(pi.lnurl_data) |
| 230 | + self.assertEqual('test-k1-value', pi.lnurl_data.k1) |
| 231 | + self.assertEqual('Test withdrawal', pi.lnurl_data.default_description) |
| 232 | + self.assertEqual(1000, pi.lnurl_data.min_withdrawable_sat) |
| 233 | + self.assertEqual(500000, pi.lnurl_data.max_withdrawable_sat) |
| 234 | + |
| 235 | + @patch('electrum.payment_identifier.request_lnurl') |
| 236 | + def test_lnurl_resolve_error(self, mock_request_lnurl): |
| 237 | + """Test LNURL resolve error handling""" |
| 238 | + lnurl = 'LNURL1DP68GURN8GHJ7MRWVF5HGUEWD3HXZERYWFJHXUEWVDHK6TM4WPNHYCTYV4EJ7DFCVGENSDPH8QCRZETXVGCXGCMPVFJR' \ |
| 239 | + 'WENP8P3NJEP3XE3NQWRPXFJR2VRRVSCX2V33V5UNVC3SXP3RXCFSVFSKVWPCV3SKZWTP8YUZ7AMFW35XGUNPWUHKZURF9AMRZT' \ |
| 240 | + 'MVDE6HYMP0FETHVUNZDAMHQ7JSF4RX73TZ2VU9Z3J3GVMSLCJ57F' |
| 241 | + |
| 242 | + # Mock LNURL error |
| 243 | + mock_request_lnurl.side_effect = LNURLError("Server error") |
| 244 | + |
| 245 | + pi = PaymentIdentifier(None, lnurl) |
| 246 | + self.assertTrue(pi.need_resolve()) |
| 247 | + |
| 248 | + async def run_resolve(): |
| 249 | + await pi._do_resolve() |
| 250 | + |
| 251 | + asyncio.run(run_resolve()) |
152 | 252 |
|
153 | | - # TODO: resolve mock |
| 253 | + self.assertEqual(PaymentIdentifierState.ERROR, pi.state) |
| 254 | + self.assertTrue(pi.is_error()) |
| 255 | + self.assertIn("Server error", pi.get_error()) |
154 | 256 |
|
155 | 257 | def test_multiline(self): |
156 | 258 | pi_str = '\n'.join([ |
|
0 commit comments