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 +261 -0
- package/LICENSE +21 -0
- package/README.md +47 -28
- package/index.test.ts +143 -39
- package/index.ts +18 -15
- package/package.json +1 -1
- package/CLAUDE.md +0 -107
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,
|
|
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" | "
|
|
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" | "
|
|
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
|
-
|
|
353
|
+
MIT
|
|
335
354
|
|
|
336
355
|
## Related
|
|
337
356
|
|
|
338
|
-
- [BIP-321 Specification](https://
|
|
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(
|
|
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(
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
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`.
|