bip-321 0.0.2 → 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/AGENTS.md ADDED
@@ -0,0 +1,261 @@
1
+ # BIP-321 Parser - Agent Development Guide
2
+
3
+ This document provides guidance for AI agents and developers working on the BIP-321 Bitcoin URI parser library.
4
+
5
+ ## Project Overview
6
+
7
+ A TypeScript/JavaScript library for parsing BIP-321 Bitcoin URIs with support for multiple payment methods (on-chain, Lightning, BOLT12, Silent Payments). Works natively in Node.js, browsers, and React Native without build tools.
8
+
9
+ ## Tech Stack
10
+
11
+ ### Runtime
12
+ - **Bun** - Primary development runtime
13
+ - Use `bun test` instead of `jest` or `vitest`
14
+ - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
15
+ - Use `bun install` for package management
16
+
17
+ ### Key Dependencies
18
+
19
+ **Why These Specific Dependencies:**
20
+
21
+ 1. **`@scure/base`** (^2.0.0)
22
+ - Pure JavaScript base58, bech32, and bech32m encoding
23
+ - Works natively in all environments (Node.js, browser, React Native)
24
+ - No browserify or build tools needed
25
+ - Used for Base58 address decoding and bech32/bech32m validation
26
+
27
+ 2. **`@noble/hashes`** (^2.0.1)
28
+ - Pure JavaScript cryptographic hashing (SHA-256)
29
+ - Properly exports all modules (no import warnings)
30
+ - Used for Base58Check checksum validation
31
+ - Import path: `@noble/hashes/sha2.js` (with .js extension)
32
+
33
+ 3. **`bitcoinjs-lib`** (^7.0.0)
34
+ - Standard Bitcoin library for address validation
35
+ - Note: Taproot addresses require manual bech32m validation (ECC lib not initialized)
36
+ - Fallback to `@scure/base` for taproot validation
37
+
38
+ 4. **`light-bolt11-decoder`** (^3.2.0)
39
+ - Lightweight Lightning BOLT11 invoice parser
40
+ - No heavy dependencies
41
+
42
+ ### What We AVOID
43
+
44
+ - ❌ `bs58` - Requires browserify for browsers
45
+ - ❌ `bs58check` - Has module export warnings with `@noble/hashes`
46
+ - ❌ `express` or heavy server frameworks
47
+ - ❌ Build tools (webpack, browserify, rollup)
48
+
49
+ ## Development Commands
50
+
51
+ ```bash
52
+ # Run tests
53
+ bun test
54
+
55
+ # Run TypeScript type check
56
+ bun run check
57
+ # or
58
+ bunx tsc --noEmit
59
+
60
+ # Run examples
61
+ bun example.ts
62
+
63
+ # Run linter
64
+ bun run lint
65
+ ```
66
+
67
+ ## Project Structure
68
+
69
+ ```
70
+ bip-321/
71
+ ├── index.ts # Main parser implementation
72
+ ├── index.test.ts # Comprehensive test suite (39 tests)
73
+ ├── example.ts # Usage examples
74
+ ├── README.md # User documentation
75
+ ├── CLAUDE.md # Bun-specific development rules
76
+ ├── AGENTS.md # This file
77
+ ├── package.json # Dependencies and scripts
78
+ └── tsconfig.json # TypeScript configuration
79
+ ```
80
+
81
+ ## Key Design Decisions
82
+
83
+ ### 1. Network Detection Strategy
84
+
85
+ ```typescript
86
+ // Order matters for Lightning invoices!
87
+ // Check "lnbcrt" before "lnbc" to avoid false positives
88
+ if (lowerInvoice.startsWith("lnbcrt")) return "regtest";
89
+ else if (lowerInvoice.startsWith("lnbc")) return "mainnet";
90
+ else if (lowerInvoice.startsWith("lntbs")) return "signet";
91
+ else if (lowerInvoice.startsWith("lntb")) return "testnet";
92
+ ```
93
+
94
+ ### 2. Taproot Address Validation
95
+
96
+ bitcoinjs-lib requires ECC library initialization for taproot validation, so we use manual bech32m validation:
97
+
98
+ ```typescript
99
+ // Fallback to manual bech32/bech32m validation for taproot
100
+ const decoded = lowerAddress.startsWith("bc1p")
101
+ ? bech32m.decode(address as `${string}1${string}`, 90)
102
+ : bech32.decode(address as `${string}1${string}`, 90);
103
+ ```
104
+
105
+ ### 3. Base58Check Validation
106
+
107
+ Manual implementation using `@scure/base` + `@noble/hashes` to avoid dependencies with browser compatibility issues:
108
+
109
+ ```typescript
110
+ const decoded = base58.decode(address);
111
+ const payload = decoded.slice(0, -4);
112
+ const checksum = decoded.slice(-4);
113
+ const hash = sha256(sha256(payload)); // Double SHA-256
114
+ // Verify checksum matches
115
+ ```
116
+
117
+ ### 4. Query Parameter Parsing
118
+
119
+ Manual parsing instead of `URLSearchParams` to preserve encoded values for `pop` parameter:
120
+
121
+ ```typescript
122
+ // Parse manually to preserve encoded values for pop parameter
123
+ const paramPairs = queryString.split("&");
124
+ // Keep pop value encoded as per BIP-321 spec
125
+ ```
126
+
127
+ ## Supported Payment Methods
128
+
129
+ | Type | Parameter | Validation | Notes |
130
+ |------|-----------|------------|-------|
131
+ | On-chain | `address`, `bc`, `tb`, `bcrt` | Full validation with network check | P2PKH, P2SH, Segwit, Taproot |
132
+ | Lightning | `lightning` | BOLT11 decode + network detection | Mainnet, testnet, regtest, signet |
133
+ | BOLT12 | `lno` | Accept any value | Offers (minimal validation) |
134
+ | Silent Payments | `sp` | Prefix check (`sp1`) | BIP-352 addresses |
135
+
136
+ **Removed:** BIP-351 Private Payments (`pay` parameter) - Unused spec, nobody knows what it is
137
+
138
+ ## Testing Guidelines
139
+
140
+ ### Test Structure
141
+ - 39 tests covering all functionality
142
+ - Use `test()` and `expect()` from `bun:test`
143
+ - Group related tests with `describe()`
144
+ - Use non-null assertions (`!`) where values are guaranteed to exist
145
+
146
+ ### Example Test Pattern
147
+ ```typescript
148
+ test("parses mainnet address", () => {
149
+ const result = parseBIP321("bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
150
+ expect(result.valid).toBe(true);
151
+ expect(result.network).toBe("mainnet");
152
+ expect(result.paymentMethods[0]!.type).toBe("onchain");
153
+ });
154
+ ```
155
+
156
+ ## TypeScript Type Safety
157
+
158
+ All code must pass strict TypeScript checking with zero errors:
159
+
160
+ ```bash
161
+ bunx tsc --noEmit # Must pass with 0 errors
162
+ ```
163
+
164
+ ### Common Type Patterns
165
+
166
+ ```typescript
167
+ // Use non-null assertions for array access where guaranteed
168
+ result.paymentMethods[0]!.type
169
+
170
+ // Use optional chaining for potentially undefined values
171
+ result.paymentMethods[0]?.network
172
+
173
+ // Use nullish coalescing for fallbacks
174
+ byNetwork.mainnet?.length || 0
175
+ ```
176
+
177
+ ## BIP-321 Compliance Rules
178
+
179
+ 1. ✅ URI must start with `bitcoin:` (case-insensitive)
180
+ 2. ✅ `label`, `message`, `amount`, `pop` cannot appear multiple times
181
+ 3. ✅ `pop` and `req-pop` cannot both be present
182
+ 4. ✅ Required parameters (`req-*`) must be understood or URI is invalid
183
+ 5. ✅ Network-specific parameters must match address network
184
+ 6. ✅ `pop` scheme must not be forbidden (http, https, file, javascript, mailto)
185
+ 7. ✅ `amount` must be decimal BTC (no commas)
186
+ 8. ✅ At least one valid payment method required
187
+
188
+ ## Common Pitfalls
189
+
190
+ ### ❌ Don't Do This
191
+ ```typescript
192
+ // Wrong: Using .js imports breaks TypeScript
193
+ import { sha256 } from "@noble/hashes/sha2";
194
+
195
+ // Wrong: bs58 requires browserify
196
+ import bs58 from "bs58";
197
+
198
+ // Wrong: Checking lnbc before lnbcrt
199
+ if (invoice.startsWith("lnbc")) return "mainnet";
200
+ ```
201
+
202
+ ### ✅ Do This
203
+ ```typescript
204
+ // Correct: Use .js extension for @noble/hashes
205
+ import { sha256 } from "@noble/hashes/sha2.js";
206
+
207
+ // Correct: Use @scure/base (works everywhere)
208
+ import { base58 } from "@scure/base";
209
+
210
+ // Correct: Check longer prefix first
211
+ if (invoice.startsWith("lnbcrt")) return "regtest";
212
+ else if (invoice.startsWith("lnbc")) return "mainnet";
213
+ ```
214
+
215
+ ## Adding New Payment Methods
216
+
217
+ 1. Add to `PaymentMethod` type union
218
+ 2. Add parameter parsing in `parseBIP321()`
219
+ 3. Implement validation function if needed
220
+ 4. Add network detection if applicable
221
+ 5. Write tests for new method
222
+ 6. Update README documentation
223
+ 7. Add example usage
224
+
225
+ ## Browser Compatibility
226
+
227
+ All dependencies work natively in browsers via ES modules:
228
+
229
+ ```html
230
+ <script type="module">
231
+ import { parseBIP321 } from './index.js';
232
+ // No build tools needed!
233
+ </script>
234
+ ```
235
+
236
+ **No browserify, webpack, rollup, or any build tools required.**
237
+
238
+ ## Performance Considerations
239
+
240
+ - Address validation is synchronous and fast
241
+ - Lightning invoice decoding is the slowest operation
242
+ - No async operations in the entire codebase
243
+ - Manual Base58 validation is faster than external libraries
244
+
245
+ ## Contributing Guidelines
246
+
247
+ 1. All changes must pass TypeScript type check (`bun run check`)
248
+ 2. All tests must pass (`bun test`)
249
+ 3. Add tests for new functionality
250
+ 4. Update README for API changes
251
+ 5. Keep dependencies minimal and browser-compatible
252
+ 6. No build tools or polyfills
253
+ 7. Maintain cross-platform compatibility (Node.js, browser, React Native)
254
+
255
+ ## Resources
256
+
257
+ - [BIP-321 Specification](https://bips.dev/321/)
258
+ - [BIP-21 (Original)](https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki)
259
+ - [BIP-352 Silent Payments](https://github.com/bitcoin/bips/blob/master/bip-0352.mediawiki)
260
+ - [@scure/base Documentation](https://github.com/paulmillr/scure-base)
261
+ - [@noble/hashes Documentation](https://github.com/paulmillr/noble-hashes)
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
@@ -5,30 +5,23 @@ A TypeScript/JavaScript library for parsing BIP-321 Bitcoin URI scheme. This lib
5
5
  ## Features
6
6
 
7
7
  - ✅ **Complete BIP-321 compliance** - Implements the full BIP-321 specification
8
- - ✅ **Multiple payment methods** - Supports on-chain, Lightning (BOLT11), BOLT12 offers, silent payments, and private payments
8
+ - ✅ **Multiple payment methods** - Supports on-chain, Lightning (BOLT11), BOLT12 offers, and silent payments
9
9
  - ✅ **Network detection** - Automatically detects mainnet, testnet, regtest, and signet networks
10
10
  - ✅ **Address validation** - Validates Bitcoin addresses (P2PKH, P2SH, Segwit v0, Taproot)
11
11
  - ✅ **Lightning invoice validation** - Validates BOLT11 Lightning invoices
12
- - ✅ **Cross-platform** - Works in Node.js, browsers, and React Native
13
- - ✅ **TypeScript support** - Fully typed with TypeScript definitions
14
- - ✅ **Proof of payment** - Supports pop/req-pop parameters for payment callbacks
15
- - ✅ **Comprehensive error handling** - Clear error messages for invalid URIs
16
12
 
17
13
  ```typescript
18
14
  import { parseBIP321, type BIP321ParseResult, type PaymentMethod } from "bip-321";
19
15
 
20
- // Fully typed result with autocomplete support
21
16
  const result: BIP321ParseResult = parseBIP321("bitcoin:...");
22
17
 
23
- // TypeScript knows all available properties
24
18
  result.valid; // boolean
25
19
  result.network; // "mainnet" | "testnet" | "regtest" | "signet" | undefined
26
20
  result.paymentMethods; // PaymentMethod[]
27
21
  result.errors; // string[]
28
22
 
29
- // Payment methods are also fully typed
30
23
  result.paymentMethods.forEach((method: PaymentMethod) => {
31
- method.type; // "onchain" | "lightning" | "lno" | "silent-payment" | "private-payment" | "other"
24
+ method.type; // "onchain" | "lightning" | "lno" | "silent-payment" | "other"
32
25
  method.network; // "mainnet" | "testnet" | "regtest" | "signet" | undefined
33
26
  method.valid; // boolean
34
27
  });
@@ -46,16 +39,6 @@ Or with npm:
46
39
  npm install bip-321
47
40
  ```
48
41
 
49
- ### Note on Dependencies
50
-
51
- This library uses modern, browser-native dependencies:
52
- - **`@scure/base`** - Pure JavaScript base58, bech32, and bech32m encoding (no browserify needed)
53
- - **`@noble/hashes`** - Pure JavaScript cryptographic hashing
54
- - **`bitcoinjs-lib`** - Bitcoin address validation
55
- - **`light-bolt11-decoder`** - Lightning invoice parsing
56
-
57
- All dependencies work natively in Node.js, browsers, and React Native without any build tools or polyfills required.
58
-
59
42
  ## Quick Start
60
43
 
61
44
  ```typescript
@@ -122,6 +105,30 @@ console.log(result.paymentMethods[0].type); // "lightning"
122
105
  console.log(result.paymentMethods[0].network); // "mainnet"
123
106
  ```
124
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
+
125
132
  ### Multiple Payment Methods
126
133
 
127
134
  ```typescript
@@ -159,12 +166,28 @@ if (!result.valid) {
159
166
 
160
167
  ## API Reference
161
168
 
162
- ### `parseBIP321(uri: string): BIP321ParseResult`
169
+ ### `parseBIP321(uri: string, expectedNetwork?: "mainnet" | "testnet" | "regtest" | "signet"): BIP321ParseResult`
163
170
 
164
171
  Parses a BIP-321 URI and returns detailed information about the payment request.
165
172
 
166
173
  **Parameters:**
167
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)
168
191
 
169
192
  **Returns:** `BIP321ParseResult` object containing:
170
193
 
@@ -198,7 +221,7 @@ interface BIP321ParseResult {
198
221
 
199
222
  ```typescript
200
223
  interface PaymentMethod {
201
- type: "onchain" | "lightning" | "lno" | "silent-payment" | "private-payment" | "other";
224
+ type: "onchain" | "lightning" | "lno" | "silent-payment" | "other";
202
225
  value: string; // The actual address/invoice value
203
226
  network?: "mainnet" | "testnet" | "regtest" | "signet";
204
227
  valid: boolean; // Whether this payment method is valid
@@ -250,7 +273,6 @@ console.log(summary);
250
273
  | Lightning | `lightning` | BOLT11 Lightning invoices |
251
274
  | BOLT12 | `lno` | Lightning BOLT12 offers |
252
275
  | Silent Payments | `sp` | BIP352 Silent Payment addresses |
253
- | Private Payments | `pay` | BIP351 Private Payment addresses |
254
276
 
255
277
  ## Network Detection
256
278
 
@@ -279,6 +301,7 @@ The parser enforces BIP-321 validation rules:
279
301
  6. ✅ Required parameters (`req-*`) must be understood or URI is invalid
280
302
  7. ✅ Network-specific parameters (`bc`, `tb`, etc.) must match address network
281
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
282
305
 
283
306
  ## Browser Usage
284
307
 
@@ -325,15 +348,11 @@ function parseQRCode(data: string) {
325
348
  }
326
349
  ```
327
350
 
328
- ## Contributing
329
-
330
- Contributions are welcome! Please feel free to submit a Pull Request.
331
-
332
351
  ## License
333
352
 
334
- BSD-2-Clause (same as BIP-321)
353
+ MIT
335
354
 
336
355
  ## Related
337
356
 
338
- - [BIP-321 Specification](https://bips.dev/321/)
357
+ - [BIP-321 Specification](https://github.com/bitcoin/bips/blob/master/bip-0321.mediawiki)
339
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(
@@ -186,18 +214,12 @@ describe("BIP-321 Parser", () => {
186
214
  expect(result.valid).toBe(true);
187
215
  expect(result.paymentMethods.length).toBe(2);
188
216
  });
189
-
190
- test("parses private payment address", () => {
191
- const result = parseBIP321("bitcoin:?pay=bip351address");
192
- expect(result.valid).toBe(true);
193
- expect(result.paymentMethods[0]!.type).toBe("private-payment");
194
- });
195
217
  });
196
218
 
197
219
  describe("Network-specific Parameters", () => {
198
220
  test("parses bc parameter for mainnet", () => {
199
221
  const result = parseBIP321(
200
- "bitcoin:?bc=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
222
+ `bitcoin:?bc=${TEST_DATA.addresses.mainnet.bech32}`,
201
223
  );
202
224
  expect(result.valid).toBe(true);
203
225
  expect(result.paymentMethods[0]!.network).toBe("mainnet");
@@ -205,7 +227,7 @@ describe("BIP-321 Parser", () => {
205
227
 
206
228
  test("parses tb parameter for testnet", () => {
207
229
  const result = parseBIP321(
208
- "bitcoin:?tb=tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
230
+ `bitcoin:?tb=${TEST_DATA.addresses.testnet.bech32}`,
209
231
  );
210
232
  expect(result.valid).toBe(true);
211
233
  expect(result.paymentMethods[0]!.network).toBe("testnet");
@@ -213,7 +235,7 @@ describe("BIP-321 Parser", () => {
213
235
 
214
236
  test("rejects testnet address in bc parameter", () => {
215
237
  const result = parseBIP321(
216
- "bitcoin:?bc=tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
238
+ `bitcoin:?bc=${TEST_DATA.addresses.testnet.bech32}`,
217
239
  );
218
240
  expect(result.valid).toBe(false);
219
241
  expect(result.errors.some((e) => e.includes("network mismatch"))).toBe(
@@ -223,7 +245,7 @@ describe("BIP-321 Parser", () => {
223
245
 
224
246
  test("parses multiple segwit versions", () => {
225
247
  const result = parseBIP321(
226
- "bitcoin:?bc=bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq&bc=bc1pdyp8m5mhurxa9mf822jegnhu49g2zcchgcq8jzrjxg58u2lvudyqftt43a",
248
+ `bitcoin:?bc=${TEST_DATA.addresses.mainnet.bech32}&bc=${TEST_DATA.addresses.mainnet.taproot}`,
227
249
  );
228
250
  expect(result.valid).toBe(true);
229
251
  expect(result.paymentMethods.length).toBe(2);
@@ -235,7 +257,7 @@ describe("BIP-321 Parser", () => {
235
257
  describe("Proof of Payment", () => {
236
258
  test("parses pop parameter", () => {
237
259
  const result = parseBIP321(
238
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?pop=initiatingapp%3a",
260
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?pop=initiatingapp%3a`,
239
261
  );
240
262
  expect(result.valid).toBe(true);
241
263
  expect(result.pop).toBe("initiatingapp%3a");
@@ -244,7 +266,7 @@ describe("BIP-321 Parser", () => {
244
266
 
245
267
  test("parses req-pop parameter", () => {
246
268
  const result = parseBIP321(
247
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?req-pop=callbackuri%3a",
269
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?req-pop=callbackuri%3a`,
248
270
  );
249
271
  expect(result.valid).toBe(true);
250
272
  expect(result.pop).toBe("callbackuri%3a");
@@ -253,7 +275,7 @@ describe("BIP-321 Parser", () => {
253
275
 
254
276
  test("rejects forbidden http scheme in pop", () => {
255
277
  const result = parseBIP321(
256
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?pop=https%3aiwantyouripaddress.com",
278
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?pop=https%3aiwantyouripaddress.com`,
257
279
  );
258
280
  expect(
259
281
  result.errors.some((e) => e.includes("Forbidden pop scheme")),
@@ -262,14 +284,14 @@ describe("BIP-321 Parser", () => {
262
284
 
263
285
  test("rejects payment when req-pop uses forbidden scheme", () => {
264
286
  const result = parseBIP321(
265
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?req-pop=https%3aevilwebsite.com",
287
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?req-pop=https%3aevilwebsite.com`,
266
288
  );
267
289
  expect(result.valid).toBe(false);
268
290
  });
269
291
 
270
292
  test("rejects multiple pop parameters", () => {
271
293
  const result = parseBIP321(
272
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?pop=callback%3a&req-pop=callback%3a",
294
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?pop=callback%3a&req-pop=callback%3a`,
273
295
  );
274
296
  expect(result.valid).toBe(false);
275
297
  expect(result.errors.some((e) => e.includes("Multiple pop"))).toBe(true);
@@ -279,7 +301,7 @@ describe("BIP-321 Parser", () => {
279
301
  describe("Required Parameters", () => {
280
302
  test("rejects unknown required parameters", () => {
281
303
  const result = parseBIP321(
282
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?req-somethingyoudontunderstand=50",
304
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?req-somethingyoudontunderstand=50`,
283
305
  );
284
306
  expect(result.valid).toBe(false);
285
307
  expect(result.requiredParams.length).toBeGreaterThan(0);
@@ -287,7 +309,7 @@ describe("BIP-321 Parser", () => {
287
309
 
288
310
  test("accepts unknown optional parameters", () => {
289
311
  const result = parseBIP321(
290
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?somethingyoudontunderstand=50",
312
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?somethingyoudontunderstand=50`,
291
313
  );
292
314
  expect(result.valid).toBe(true);
293
315
  expect(result.optionalParams.somethingyoudontunderstand).toEqual(["50"]);
@@ -321,7 +343,7 @@ describe("BIP-321 Parser", () => {
321
343
  describe("Helper Functions", () => {
322
344
  test("getPaymentMethodsByNetwork groups correctly", () => {
323
345
  const result = parseBIP321(
324
- "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?tb=tb1qghfhmd4zh7ncpmxl3qzhmq566jk8ckq4gafnmg",
346
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}?tb=${TEST_DATA.addresses.testnet.bech32}`,
325
347
  );
326
348
  const byNetwork = getPaymentMethodsByNetwork(result);
327
349
  expect(byNetwork.mainnet!.length).toBe(1);
@@ -330,7 +352,7 @@ describe("BIP-321 Parser", () => {
330
352
 
331
353
  test("getValidPaymentMethods filters correctly", () => {
332
354
  const result = parseBIP321(
333
- "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?lightning=invalidinvoice",
355
+ `bitcoin:${TEST_DATA.addresses.mainnet.bech32}?lightning=invalidinvoice`,
334
356
  );
335
357
  const valid = getValidPaymentMethods(result);
336
358
  expect(valid.length).toBe(1);
@@ -339,7 +361,7 @@ describe("BIP-321 Parser", () => {
339
361
 
340
362
  test("formatPaymentMethodsSummary produces output", () => {
341
363
  const result = parseBIP321(
342
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=1.5&label=Test",
364
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?amount=1.5&label=Test`,
343
365
  );
344
366
  const summary = formatPaymentMethodsSummary(result);
345
367
  expect(summary).toContain("Valid: true");
@@ -351,11 +373,93 @@ describe("BIP-321 Parser", () => {
351
373
  describe("Case Insensitivity", () => {
352
374
  test("handles mixed case in parameters", () => {
353
375
  const result = parseBIP321(
354
- "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?AMOUNT=1.5&Label=Test",
376
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?AMOUNT=1.5&Label=Test`,
355
377
  );
356
378
  expect(result.valid).toBe(true);
357
379
  expect(result.amount).toBe(1.5);
358
380
  expect(result.label).toBe("Test");
359
381
  });
360
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
+ });
361
465
  });
package/index.ts CHANGED
@@ -4,13 +4,7 @@ import { sha256 } from "@noble/hashes/sha2.js";
4
4
  import { base58, bech32, bech32m } from "@scure/base";
5
5
 
6
6
  export interface PaymentMethod {
7
- type:
8
- | "onchain"
9
- | "lightning"
10
- | "lno"
11
- | "silent-payment"
12
- | "private-payment"
13
- | "other";
7
+ type: "onchain" | "lightning" | "lno" | "silent-payment" | "other";
14
8
  value: string;
15
9
  network?: "mainnet" | "testnet" | "regtest" | "signet";
16
10
  valid: boolean;
@@ -198,7 +192,10 @@ function validatePopUri(popUri: string): { valid: boolean; error?: string } {
198
192
  }
199
193
  }
200
194
 
201
- export function parseBIP321(uri: string): BIP321ParseResult {
195
+ export function parseBIP321(
196
+ uri: string,
197
+ expectedNetwork?: "mainnet" | "testnet" | "regtest" | "signet",
198
+ ): BIP321ParseResult {
202
199
  const result: BIP321ParseResult = {
203
200
  paymentMethods: [],
204
201
  requiredParams: [],
@@ -353,13 +350,6 @@ export function parseBIP321(uri: string): BIP321ParseResult {
353
350
  if (!isSilentPayment) {
354
351
  result.errors.push("Invalid silent payment address format");
355
352
  }
356
- } else if (lowerKey === "pay") {
357
- const decodedValue = decodeURIComponent(value);
358
- result.paymentMethods.push({
359
- type: "private-payment",
360
- value: decodedValue,
361
- valid: true,
362
- });
363
353
  } else if (
364
354
  lowerKey === "bc" ||
365
355
  lowerKey === "tb" ||
@@ -415,6 +405,19 @@ export function parseBIP321(uri: string): BIP321ParseResult {
415
405
  result.valid = false;
416
406
  }
417
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
+
418
421
  if (result.popRequired && result.pop) {
419
422
  const hasValidPaymentMethod = result.paymentMethods.some((pm) => pm.valid);
420
423
  if (!hasValidPaymentMethod) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bip-321",
3
- "version": "0.0.2",
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",
package/CLAUDE.md DELETED
@@ -1,107 +0,0 @@
1
- ---
2
-
3
- Default to using Bun instead of Node.js.
4
-
5
- - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
6
- - Use `bun test` instead of `jest` or `vitest`
7
- - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
8
- - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
9
- - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
10
- - Bun automatically loads .env, so don't use dotenv.
11
-
12
- ## APIs
13
-
14
- - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
15
- - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
16
- - `Bun.redis` for Redis. Don't use `ioredis`.
17
- - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
18
- - `WebSocket` is built-in. Don't use `ws`.
19
- - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
20
- - Bun.$`ls` instead of execa.
21
-
22
- ## Testing
23
-
24
- Use `bun test` to run tests.
25
-
26
- ```ts#index.test.ts
27
- import { test, expect } from "bun:test";
28
-
29
- test("hello world", () => {
30
- expect(1).toBe(1);
31
- });
32
- ```
33
-
34
- ## Frontend
35
-
36
- Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
37
-
38
- Server:
39
-
40
- ```ts#index.ts
41
- import index from "./index.html"
42
-
43
- Bun.serve({
44
- routes: {
45
- "/": index,
46
- "/api/users/:id": {
47
- GET: (req) => {
48
- return new Response(JSON.stringify({ id: req.params.id }));
49
- },
50
- },
51
- },
52
- // optional websocket support
53
- websocket: {
54
- open: (ws) => {
55
- ws.send("Hello, world!");
56
- },
57
- message: (ws, message) => {
58
- ws.send(message);
59
- },
60
- close: (ws) => {
61
- // handle close
62
- }
63
- },
64
- development: {
65
- hmr: true,
66
- console: true,
67
- }
68
- })
69
- ```
70
-
71
- HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
72
-
73
- ```html#index.html
74
- <html>
75
- <body>
76
- <h1>Hello, world!</h1>
77
- <script type="module" src="./frontend.tsx"></script>
78
- </body>
79
- </html>
80
- ```
81
-
82
- With the following `frontend.tsx`:
83
-
84
- ```tsx#frontend.tsx
85
- import React from "react";
86
-
87
- // import .css files directly and it works
88
- import './index.css';
89
-
90
- import { createRoot } from "react-dom/client";
91
-
92
- const root = createRoot(document.body);
93
-
94
- export default function Frontend() {
95
- return <h1>Hello, world!</h1>;
96
- }
97
-
98
- root.render(<Frontend />);
99
- ```
100
-
101
- Then, run index.ts
102
-
103
- ```sh
104
- bun --hot ./index.ts
105
- ```
106
-
107
- For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.