bip-321 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nitesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -105,6 +105,30 @@ console.log(result.paymentMethods[0].type); // "lightning"
105
105
  console.log(result.paymentMethods[0].network); // "mainnet"
106
106
  ```
107
107
 
108
+ ### Network Validation
109
+
110
+ ```typescript
111
+ // Ensure all payment methods are mainnet
112
+ const result = parseBIP321(
113
+ "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?lightning=lnbc...",
114
+ "mainnet"
115
+ );
116
+
117
+ if (result.valid) {
118
+ // All payment methods are guaranteed to be mainnet
119
+ console.log("All payment methods are mainnet");
120
+ }
121
+
122
+ // Reject testnet addresses when expecting mainnet
123
+ const invalid = parseBIP321(
124
+ "bitcoin:tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
125
+ "mainnet"
126
+ );
127
+
128
+ console.log(invalid.valid); // false
129
+ console.log(invalid.errors); // ["Payment method network mismatch..."]
130
+ ```
131
+
108
132
  ### Multiple Payment Methods
109
133
 
110
134
  ```typescript
@@ -142,12 +166,28 @@ if (!result.valid) {
142
166
 
143
167
  ## API Reference
144
168
 
145
- ### `parseBIP321(uri: string): BIP321ParseResult`
169
+ ### `parseBIP321(uri: string, expectedNetwork?: "mainnet" | "testnet" | "regtest" | "signet"): BIP321ParseResult`
146
170
 
147
171
  Parses a BIP-321 URI and returns detailed information about the payment request.
148
172
 
149
173
  **Parameters:**
150
174
  - `uri` - The Bitcoin URI string to parse
175
+ - `expectedNetwork` (optional) - Expected network for all payment methods. If specified, all payment methods must match this network or the URI will be marked invalid.
176
+ </text>
177
+
178
+ <old_text line=240>
179
+ ## Validation Rules
180
+
181
+ The parser enforces BIP-321 validation rules:
182
+
183
+ 1. ✅ URI must start with `bitcoin:` (case-insensitive)
184
+ 2. ✅ Address in URI path must be valid or empty
185
+ 3. ✅ `amount` must be decimal BTC (no commas)
186
+ 4. ✅ `label`, `message`, and `amount` cannot appear multiple times
187
+ 5. ✅ `pop` and `req-pop` cannot both be present
188
+ 6. ✅ Required parameters (`req-*`) must be understood or URI is invalid
189
+ 7. ✅ Network-specific parameters (`bc`, `tb`, etc.) must match address network
190
+ 8. ✅ `pop` URI scheme must not be forbidden (http, https, file, javascript, mailto)
151
191
 
152
192
  **Returns:** `BIP321ParseResult` object containing:
153
193
 
@@ -261,6 +301,7 @@ The parser enforces BIP-321 validation rules:
261
301
  6. ✅ Required parameters (`req-*`) must be understood or URI is invalid
262
302
  7. ✅ Network-specific parameters (`bc`, `tb`, etc.) must match address network
263
303
  8. ✅ `pop` URI scheme must not be forbidden (http, https, file, javascript, mailto)
304
+ 9. ✅ If `expectedNetwork` is specified, all payment methods must match that network
264
305
 
265
306
  ## Browser Usage
266
307
 
@@ -307,15 +348,11 @@ function parseQRCode(data: string) {
307
348
  }
308
349
  ```
309
350
 
310
- ## Contributing
311
-
312
- Contributions are welcome! Please feel free to submit a Pull Request.
313
-
314
351
  ## License
315
352
 
316
- BSD-2-Clause (same as BIP-321)
353
+ MIT
317
354
 
318
355
  ## Related
319
356
 
320
- - [BIP-321 Specification](https://bips.dev/321/)
357
+ - [BIP-321 Specification](https://github.com/bitcoin/bips/blob/master/bip-0321.mediawiki)
321
358
  - [BIP-21 (Original)](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki)
package/index.test.ts CHANGED
@@ -6,12 +6,40 @@ import {
6
6
  formatPaymentMethodsSummary,
7
7
  } from "./index";
8
8
 
9
+ const TEST_DATA = {
10
+ addresses: {
11
+ mainnet: {
12
+ p2pkh: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
13
+ bech32: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
14
+ taproot: "bc1pdyp8m5mhurxa9mf822jegnhu49g2zcchgcq8jzrjxg58u2lvudyqftt43a",
15
+ },
16
+ testnet: {
17
+ bech32: "tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
18
+ },
19
+ regtest: {
20
+ bech32: "bcrt1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080",
21
+ },
22
+ },
23
+ lightning: {
24
+ mainnet:
25
+ "lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
26
+ testnet:
27
+ "lntb2500n1pwxlkl5pp5g8hz28tlf950ps942lu3dknfete8yax2ctywpwjs872x9kngvvuqdqage5hyum5yp6x2um5yp5kuan0d93k2cqzyskdc5s2ltgm9kklz42x3e4tggdd9lcep2s9t2yk54gnfxg48wxushayrt52zjmua43gdnxmuc5s0c8g29ja9vnxs6x3kxgsha07htcacpmdyl64",
28
+ regtest:
29
+ "lnbcrt50u1p5s6w2zpp5juf0r9zutj4zv00kpuuqmgn246azqaq0u5kksx93p46ue94gpmrsdqqcqzzsxqyz5vqsp57u7clsm57nas7c0r2p4ujxr8whla6gxmwf44yqt9f862evjzd3ds9qxpqysgqrwvspjd8g3cfrkg2mrmxfdjcwk5nenw2qnmrys0rvkdmxes6jf5xfykunl5g9hnnahsnz0c90u7k42hmr7w90c0qkw3lllwy40mmqgsqjtyzpd",
30
+ signet:
31
+ "lntbs10u1p5s6wgtsp5d8a763exauvdk6s5gwvl8zmuapmgjq05fdv6trasjd4slvgkvzzqpp56vxdyl24hmkpz0tvqq84xdpqqeql3x7kh8tey4uum2cu8jny6djqdq4g9exkgznw3hhyefqyvenyxqzjccqp2rzjqdwy5et9ygczjl2jqmr9e5xm28u3gksjfrf0pht04uwz2lt9d59cypqelcqqq8gqqqqqqqqpqqqqqzsqqc9qxpqysgq0x0pg2s65rnp2cr35td5tq0vwgmnrghkpzt93eypqvvfu5m40pcjl9k2x2m4kqgvz2ez8tzxqgw0nyeg2w60nfky579uakd4mhr3ncgp0xwars",
32
+ },
33
+ } as const;
34
+
9
35
  describe("BIP-321 Parser", () => {
10
36
  describe("Basic Address Parsing", () => {
11
37
  test("parses simple mainnet address", () => {
12
- const result = parseBIP321("bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
38
+ const result = parseBIP321(
39
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}`,
40
+ );
13
41
  expect(result.valid).toBe(true);
14
- expect(result.address).toBe("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
42
+ expect(result.address).toBe(TEST_DATA.addresses.mainnet.p2pkh);
15
43
  expect(result.network).toBe("mainnet");
16
44
  expect(result.paymentMethods.length).toBe(1);
17
45
  expect(result.paymentMethods[0]!.type).toBe("onchain");
@@ -20,7 +48,7 @@ describe("BIP-321 Parser", () => {
20
48
 
21
49
  test("parses bech32 mainnet address", () => {
22
50
  const result = parseBIP321(
23
- "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
51
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}`,
24
52
  );
25
53
  expect(result.valid).toBe(true);
26
54
  expect(result.network).toBe("mainnet");
@@ -29,7 +57,7 @@ describe("BIP-321 Parser", () => {
29
57
 
30
58
  test("parses testnet address", () => {
31
59
  const result = parseBIP321(
32
- "bitcoin:tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
60
+ `bitcoin:${TEST_DATA.addresses.testnet.bech32}`,
33
61
  );
34
62
  expect(result.valid).toBe(true);
35
63
  expect(result.network).toBe("testnet");
@@ -37,7 +65,7 @@ describe("BIP-321 Parser", () => {
37
65
 
38
66
  test("parses uppercase URI", () => {
39
67
  const result = parseBIP321(
40
- "BITCOIN:BC1QAR0SRRR7XFKVY5L643LYDNW9RE59GTZZWF5MDQ",
68
+ `BITCOIN:${TEST_DATA.addresses.mainnet.bech32.toUpperCase()}`,
41
69
  );
42
70
  expect(result.valid).toBe(true);
43
71
  expect(result.network).toBe("mainnet");
@@ -47,7 +75,7 @@ describe("BIP-321 Parser", () => {
47
75
  describe("Query Parameters", () => {
48
76
  test("parses label parameter", () => {
49
77
  const result = parseBIP321(
50
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?label=Luke-Jr",
78
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?label=Luke-Jr`,
51
79
  );
52
80
  expect(result.valid).toBe(true);
53
81
  expect(result.label).toBe("Luke-Jr");
@@ -55,7 +83,7 @@ describe("BIP-321 Parser", () => {
55
83
 
56
84
  test("parses amount parameter", () => {
57
85
  const result = parseBIP321(
58
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=20.3",
86
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?amount=20.3`,
59
87
  );
60
88
  expect(result.valid).toBe(true);
61
89
  expect(result.amount).toBe(20.3);
@@ -63,7 +91,7 @@ describe("BIP-321 Parser", () => {
63
91
 
64
92
  test("parses message parameter", () => {
65
93
  const result = parseBIP321(
66
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?message=Donation%20for%20project%20xyz",
94
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?message=Donation%20for%20project%20xyz`,
67
95
  );
68
96
  expect(result.valid).toBe(true);
69
97
  expect(result.message).toBe("Donation for project xyz");
@@ -71,7 +99,7 @@ describe("BIP-321 Parser", () => {
71
99
 
72
100
  test("parses multiple parameters", () => {
73
101
  const result = parseBIP321(
74
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz",
102
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz`,
75
103
  );
76
104
  expect(result.valid).toBe(true);
77
105
  expect(result.amount).toBe(50);
@@ -81,7 +109,7 @@ describe("BIP-321 Parser", () => {
81
109
 
82
110
  test("rejects invalid amount with comma", () => {
83
111
  const result = parseBIP321(
84
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=50,000.00",
112
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?amount=50,000.00`,
85
113
  );
86
114
  expect(result.valid).toBe(false);
87
115
  expect(result.errors).toContain("Invalid amount format");
@@ -89,7 +117,7 @@ describe("BIP-321 Parser", () => {
89
117
 
90
118
  test("rejects multiple label parameters", () => {
91
119
  const result = parseBIP321(
92
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?label=Luke-Jr&label=Matt",
120
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?label=Luke-Jr&label=Matt`,
93
121
  );
94
122
  expect(result.valid).toBe(false);
95
123
  expect(result.errors).toContain("Multiple label parameters not allowed");
@@ -97,7 +125,7 @@ describe("BIP-321 Parser", () => {
97
125
 
98
126
  test("rejects multiple amount parameters", () => {
99
127
  const result = parseBIP321(
100
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=42&amount=10",
128
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?amount=42&amount=10`,
101
129
  );
102
130
  expect(result.valid).toBe(false);
103
131
  expect(result.errors).toContain("Multiple amount parameters not allowed");
@@ -107,7 +135,7 @@ describe("BIP-321 Parser", () => {
107
135
  describe("Lightning Invoice", () => {
108
136
  test("parses lightning invoice with fallback", () => {
109
137
  const result = parseBIP321(
110
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?lightning=lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
138
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?lightning=${TEST_DATA.lightning.mainnet}`,
111
139
  );
112
140
  expect(result.valid).toBe(true);
113
141
  expect(result.paymentMethods.length).toBe(2);
@@ -121,7 +149,7 @@ describe("BIP-321 Parser", () => {
121
149
 
122
150
  test("parses lightning invoice without fallback", () => {
123
151
  const result = parseBIP321(
124
- "bitcoin:?lightning=lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
152
+ `bitcoin:?lightning=${TEST_DATA.lightning.mainnet}`,
125
153
  );
126
154
  expect(result.valid).toBe(true);
127
155
  expect(result.address).toBeUndefined();
@@ -131,7 +159,7 @@ describe("BIP-321 Parser", () => {
131
159
 
132
160
  test("detects signet lightning invoice", () => {
133
161
  const result = parseBIP321(
134
- "bitcoin:?lightning=lntbs10u1p5s6wgtsp5d8a763exauvdk6s5gwvl8zmuapmgjq05fdv6trasjd4slvgkvzzqpp56vxdyl24hmkpz0tvqq84xdpqqeql3x7kh8tey4uum2cu8jny6djqdq4g9exkgznw3hhyefqyvenyxqzjccqp2rzjqdwy5et9ygczjl2jqmr9e5xm28u3gksjfrf0pht04uwz2lt9d59cypqelcqqq8gqqqqqqqqpqqqqqzsqqc9qxpqysgq0x0pg2s65rnp2cr35td5tq0vwgmnrghkpzt93eypqvvfu5m40pcjl9k2x2m4kqgvz2ez8tzxqgw0nyeg2w60nfky579uakd4mhr3ncgp0xwars",
162
+ `bitcoin:?lightning=${TEST_DATA.lightning.signet}`,
135
163
  );
136
164
  expect(result.valid).toBe(true);
137
165
  expect(result.paymentMethods[0]!.network).toBe("signet");
@@ -139,7 +167,7 @@ describe("BIP-321 Parser", () => {
139
167
 
140
168
  test("detects regtest lightning invoice", () => {
141
169
  const result = parseBIP321(
142
- "bitcoin:?lightning=lnbcrt50u1p5s6w2zpp5juf0r9zutj4zv00kpuuqmgn246azqaq0u5kksx93p46ue94gpmrsdqqcqzzsxqyz5vqsp57u7clsm57nas7c0r2p4ujxr8whla6gxmwf44yqt9f862evjzd3ds9qxpqysgqrwvspjd8g3cfrkg2mrmxfdjcwk5nenw2qnmrys0rvkdmxes6jf5xfykunl5g9hnnahsnz0c90u7k42hmr7w90c0qkw3lllwy40mmqgsqjtyzpd",
170
+ `bitcoin:?lightning=${TEST_DATA.lightning.regtest}`,
143
171
  );
144
172
  expect(result.valid).toBe(true);
145
173
  expect(result.paymentMethods[0]!.network).toBe("regtest");
@@ -147,7 +175,7 @@ describe("BIP-321 Parser", () => {
147
175
 
148
176
  test("detects testnet lightning invoice", () => {
149
177
  const result = parseBIP321(
150
- "bitcoin:?lightning=lntb2500n1pwxlkl5pp5g8hz28tlf950ps942lu3dknfete8yax2ctywpwjs872x9kngvvuqdqage5hyum5yp6x2um5yp5kuan0d93k2cqzyskdc5s2ltgm9kklz42x3e4tggdd9lcep2s9t2yk54gnfxg48wxushayrt52zjmua43gdnxmuc5s0c8g29ja9vnxs6x3kxgsha07htcacpmdyl64",
178
+ `bitcoin:?lightning=${TEST_DATA.lightning.testnet}`,
151
179
  );
152
180
  expect(result.valid).toBe(true);
153
181
  expect(result.paymentMethods[0]!.network).toBe("testnet");
@@ -155,7 +183,7 @@ describe("BIP-321 Parser", () => {
155
183
 
156
184
  test("rejects testnet address in bc parameter", () => {
157
185
  const result = parseBIP321(
158
- "bitcoin:?bc=tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
186
+ `bitcoin:?bc=${TEST_DATA.addresses.testnet.bech32}`,
159
187
  );
160
188
  expect(result.valid).toBe(false);
161
189
  expect(result.errors.some((e) => e.includes("network mismatch"))).toBe(
@@ -191,7 +219,7 @@ describe("BIP-321 Parser", () => {
191
219
  describe("Network-specific Parameters", () => {
192
220
  test("parses bc parameter for mainnet", () => {
193
221
  const result = parseBIP321(
194
- "bitcoin:?bc=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
222
+ `bitcoin:?bc=${TEST_DATA.addresses.mainnet.bech32}`,
195
223
  );
196
224
  expect(result.valid).toBe(true);
197
225
  expect(result.paymentMethods[0]!.network).toBe("mainnet");
@@ -199,7 +227,7 @@ describe("BIP-321 Parser", () => {
199
227
 
200
228
  test("parses tb parameter for testnet", () => {
201
229
  const result = parseBIP321(
202
- "bitcoin:?tb=tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
230
+ `bitcoin:?tb=${TEST_DATA.addresses.testnet.bech32}`,
203
231
  );
204
232
  expect(result.valid).toBe(true);
205
233
  expect(result.paymentMethods[0]!.network).toBe("testnet");
@@ -207,7 +235,7 @@ describe("BIP-321 Parser", () => {
207
235
 
208
236
  test("rejects testnet address in bc parameter", () => {
209
237
  const result = parseBIP321(
210
- "bitcoin:?bc=tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
238
+ `bitcoin:?bc=${TEST_DATA.addresses.testnet.bech32}`,
211
239
  );
212
240
  expect(result.valid).toBe(false);
213
241
  expect(result.errors.some((e) => e.includes("network mismatch"))).toBe(
@@ -217,7 +245,7 @@ describe("BIP-321 Parser", () => {
217
245
 
218
246
  test("parses multiple segwit versions", () => {
219
247
  const result = parseBIP321(
220
- "bitcoin:?bc=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq&bc=bc1pdyp8m5mhurxa9mf822jegnhu49g2zcchgcq8jzrjxg58u2lvudyqftt43a",
248
+ `bitcoin:?bc=${TEST_DATA.addresses.mainnet.bech32}&bc=${TEST_DATA.addresses.mainnet.taproot}`,
221
249
  );
222
250
  expect(result.valid).toBe(true);
223
251
  expect(result.paymentMethods.length).toBe(2);
@@ -229,7 +257,7 @@ describe("BIP-321 Parser", () => {
229
257
  describe("Proof of Payment", () => {
230
258
  test("parses pop parameter", () => {
231
259
  const result = parseBIP321(
232
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?pop=initiatingapp%3a",
260
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?pop=initiatingapp%3a`,
233
261
  );
234
262
  expect(result.valid).toBe(true);
235
263
  expect(result.pop).toBe("initiatingapp%3a");
@@ -238,7 +266,7 @@ describe("BIP-321 Parser", () => {
238
266
 
239
267
  test("parses req-pop parameter", () => {
240
268
  const result = parseBIP321(
241
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?req-pop=callbackuri%3a",
269
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?req-pop=callbackuri%3a`,
242
270
  );
243
271
  expect(result.valid).toBe(true);
244
272
  expect(result.pop).toBe("callbackuri%3a");
@@ -247,7 +275,7 @@ describe("BIP-321 Parser", () => {
247
275
 
248
276
  test("rejects forbidden http scheme in pop", () => {
249
277
  const result = parseBIP321(
250
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?pop=https%3aiwantyouripaddress.com",
278
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?pop=https%3aiwantyouripaddress.com`,
251
279
  );
252
280
  expect(
253
281
  result.errors.some((e) => e.includes("Forbidden pop scheme")),
@@ -256,14 +284,14 @@ describe("BIP-321 Parser", () => {
256
284
 
257
285
  test("rejects payment when req-pop uses forbidden scheme", () => {
258
286
  const result = parseBIP321(
259
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?req-pop=https%3aevilwebsite.com",
287
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?req-pop=https%3aevilwebsite.com`,
260
288
  );
261
289
  expect(result.valid).toBe(false);
262
290
  });
263
291
 
264
292
  test("rejects multiple pop parameters", () => {
265
293
  const result = parseBIP321(
266
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?pop=callback%3a&req-pop=callback%3a",
294
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?pop=callback%3a&req-pop=callback%3a`,
267
295
  );
268
296
  expect(result.valid).toBe(false);
269
297
  expect(result.errors.some((e) => e.includes("Multiple pop"))).toBe(true);
@@ -273,7 +301,7 @@ describe("BIP-321 Parser", () => {
273
301
  describe("Required Parameters", () => {
274
302
  test("rejects unknown required parameters", () => {
275
303
  const result = parseBIP321(
276
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?req-somethingyoudontunderstand=50",
304
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?req-somethingyoudontunderstand=50`,
277
305
  );
278
306
  expect(result.valid).toBe(false);
279
307
  expect(result.requiredParams.length).toBeGreaterThan(0);
@@ -281,7 +309,7 @@ describe("BIP-321 Parser", () => {
281
309
 
282
310
  test("accepts unknown optional parameters", () => {
283
311
  const result = parseBIP321(
284
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?somethingyoudontunderstand=50",
312
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?somethingyoudontunderstand=50`,
285
313
  );
286
314
  expect(result.valid).toBe(true);
287
315
  expect(result.optionalParams.somethingyoudontunderstand).toEqual(["50"]);
@@ -315,7 +343,7 @@ describe("BIP-321 Parser", () => {
315
343
  describe("Helper Functions", () => {
316
344
  test("getPaymentMethodsByNetwork groups correctly", () => {
317
345
  const result = parseBIP321(
318
- "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?tb=tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
346
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}?tb=${TEST_DATA.addresses.testnet.bech32}`,
319
347
  );
320
348
  const byNetwork = getPaymentMethodsByNetwork(result);
321
349
  expect(byNetwork.mainnet!.length).toBe(1);
@@ -324,7 +352,7 @@ describe("BIP-321 Parser", () => {
324
352
 
325
353
  test("getValidPaymentMethods filters correctly", () => {
326
354
  const result = parseBIP321(
327
- "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?lightning=invalidinvoice",
355
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}?lightning=invalidinvoice`,
328
356
  );
329
357
  const valid = getValidPaymentMethods(result);
330
358
  expect(valid.length).toBe(1);
@@ -333,7 +361,7 @@ describe("BIP-321 Parser", () => {
333
361
 
334
362
  test("formatPaymentMethodsSummary produces output", () => {
335
363
  const result = parseBIP321(
336
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=1.5&label=Test",
364
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?amount=1.5&label=Test`,
337
365
  );
338
366
  const summary = formatPaymentMethodsSummary(result);
339
367
  expect(summary).toContain("Valid: true");
@@ -345,11 +373,93 @@ describe("BIP-321 Parser", () => {
345
373
  describe("Case Insensitivity", () => {
346
374
  test("handles mixed case in parameters", () => {
347
375
  const result = parseBIP321(
348
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?AMOUNT=1.5&Label=Test",
376
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?AMOUNT=1.5&Label=Test`,
349
377
  );
350
378
  expect(result.valid).toBe(true);
351
379
  expect(result.amount).toBe(1.5);
352
380
  expect(result.label).toBe("Test");
353
381
  });
354
382
  });
383
+
384
+ describe("Network Validation", () => {
385
+ test("accepts mainnet address when expecting mainnet", () => {
386
+ const result = parseBIP321(
387
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}`,
388
+ "mainnet",
389
+ );
390
+ expect(result.valid).toBe(true);
391
+ expect(result.paymentMethods[0]!.network).toBe("mainnet");
392
+ expect(result.paymentMethods[0]!.valid).toBe(true);
393
+ });
394
+
395
+ test("rejects testnet address when expecting mainnet", () => {
396
+ const result = parseBIP321(
397
+ `bitcoin:${TEST_DATA.addresses.testnet.bech32}`,
398
+ "mainnet",
399
+ );
400
+ expect(result.valid).toBe(false);
401
+ expect(result.errors.some((e) => e.includes("network mismatch"))).toBe(
402
+ true,
403
+ );
404
+ expect(result.paymentMethods[0]!.valid).toBe(false);
405
+ });
406
+
407
+ test("accepts testnet lightning invoice when expecting testnet", () => {
408
+ const result = parseBIP321(
409
+ `bitcoin:?lightning=${TEST_DATA.lightning.testnet}`,
410
+ "testnet",
411
+ );
412
+ expect(result.valid).toBe(true);
413
+ expect(result.paymentMethods[0]!.network).toBe("testnet");
414
+ });
415
+
416
+ test("rejects mainnet lightning invoice when expecting testnet", () => {
417
+ const result = parseBIP321(
418
+ `bitcoin:?lightning=${TEST_DATA.lightning.mainnet}`,
419
+ "testnet",
420
+ );
421
+ expect(result.valid).toBe(false);
422
+ expect(result.errors.some((e) => e.includes("expected testnet"))).toBe(
423
+ true,
424
+ );
425
+ });
426
+
427
+ test("rejects mixed networks when expecting specific network", () => {
428
+ const result = parseBIP321(
429
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}?tb=${TEST_DATA.addresses.testnet.bech32}`,
430
+ "mainnet",
431
+ );
432
+ expect(result.valid).toBe(false);
433
+ expect(result.paymentMethods[0]!.valid).toBe(true);
434
+ expect(result.paymentMethods[1]!.valid).toBe(false);
435
+ });
436
+
437
+ test("accepts regtest address when expecting regtest", () => {
438
+ const result = parseBIP321(
439
+ `bitcoin:${TEST_DATA.addresses.regtest.bech32}`,
440
+ "regtest",
441
+ );
442
+ expect(result.valid).toBe(true);
443
+ expect(result.paymentMethods[0]!.network).toBe("regtest");
444
+ });
445
+
446
+ test("works without network parameter (no validation)", () => {
447
+ const result = parseBIP321(
448
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}?tb=${TEST_DATA.addresses.testnet.bech32}`,
449
+ );
450
+ expect(result.valid).toBe(true);
451
+ expect(result.paymentMethods[0]!.network).toBe("mainnet");
452
+ expect(result.paymentMethods[1]!.network).toBe("testnet");
453
+ });
454
+
455
+ test("validates all payment methods against expected network", () => {
456
+ const result = parseBIP321(
457
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}?lightning=${TEST_DATA.lightning.mainnet}`,
458
+ "mainnet",
459
+ );
460
+ expect(result.valid).toBe(true);
461
+ expect(result.paymentMethods.length).toBe(2);
462
+ expect(result.paymentMethods.every((pm) => pm.valid)).toBe(true);
463
+ });
464
+ });
355
465
  });
package/index.ts CHANGED
@@ -192,7 +192,10 @@ function validatePopUri(popUri: string): { valid: boolean; error?: string } {
192
192
  }
193
193
  }
194
194
 
195
- export function parseBIP321(uri: string): BIP321ParseResult {
195
+ export function parseBIP321(
196
+ uri: string,
197
+ expectedNetwork?: "mainnet" | "testnet" | "regtest" | "signet",
198
+ ): BIP321ParseResult {
196
199
  const result: BIP321ParseResult = {
197
200
  paymentMethods: [],
198
201
  requiredParams: [],
@@ -402,6 +405,19 @@ export function parseBIP321(uri: string): BIP321ParseResult {
402
405
  result.valid = false;
403
406
  }
404
407
 
408
+ if (expectedNetwork) {
409
+ for (const method of result.paymentMethods) {
410
+ if (method.network && method.network !== expectedNetwork) {
411
+ result.errors.push(
412
+ `Payment method network mismatch: expected ${expectedNetwork}, got ${method.network}`,
413
+ );
414
+ result.valid = false;
415
+ method.valid = false;
416
+ method.error = `Network mismatch: expected ${expectedNetwork}`;
417
+ }
418
+ }
419
+ }
420
+
405
421
  if (result.popRequired && result.pop) {
406
422
  const hasValidPaymentMethod = result.paymentMethods.some((pm) => pm.valid);
407
423
  if (!hasValidPaymentMethod) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bip-321",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "A TypeScript/JavaScript library for parsing BIP-321 Bitcoin URI scheme with support for multiple payment methods",
5
5
  "type": "module",
6
6
  "main": "./index.ts",