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 +21 -0
- package/README.md +44 -7
- package/index.test.ts +143 -33
- package/index.ts +17 -1
- package/package.json +1 -1
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
|
-
|
|
353
|
+
MIT
|
|
317
354
|
|
|
318
355
|
## Related
|
|
319
356
|
|
|
320
|
-
- [BIP-321 Specification](https://
|
|
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(
|
|
38
|
+
const result = parseBIP321(
|
|
39
|
+
`bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}`,
|
|
40
|
+
);
|
|
13
41
|
expect(result.valid).toBe(true);
|
|
14
|
-
expect(result.address).toBe(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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