bip-321 0.0.4 → 0.0.6

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.
@@ -0,0 +1,94 @@
1
+ import tseslint from "@typescript-eslint/eslint-plugin";
2
+ import tsparser from "@typescript-eslint/parser";
3
+ import importPlugin from "eslint-plugin-import";
4
+
5
+ export default [
6
+ {
7
+ ignores: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/*.min.js"],
8
+ },
9
+ {
10
+ files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
11
+ languageOptions: {
12
+ parser: tsparser,
13
+ parserOptions: {
14
+ ecmaVersion: "latest",
15
+ sourceType: "module",
16
+ project: "./tsconfig.json",
17
+ },
18
+ },
19
+ plugins: {
20
+ "@typescript-eslint": tseslint,
21
+ import: importPlugin,
22
+ },
23
+ rules: {
24
+ "@typescript-eslint/no-explicit-any": "error",
25
+ "@typescript-eslint/no-non-null-assertion": "error",
26
+ "@typescript-eslint/no-unused-vars": [
27
+ "error",
28
+ { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
29
+ ],
30
+ "@typescript-eslint/no-floating-promises": "error",
31
+ "@typescript-eslint/no-misused-promises": "error",
32
+ "@typescript-eslint/await-thenable": "error",
33
+ "@typescript-eslint/no-unnecessary-condition": "warn",
34
+ "@typescript-eslint/prefer-nullish-coalescing": "error",
35
+ "@typescript-eslint/prefer-optional-chain": "error",
36
+ "@typescript-eslint/no-unsafe-assignment": "error",
37
+ "@typescript-eslint/no-unsafe-member-access": "error",
38
+ "@typescript-eslint/no-unsafe-call": "error",
39
+ "@typescript-eslint/no-unsafe-return": "error",
40
+ "@typescript-eslint/no-unsafe-argument": "error",
41
+ "@typescript-eslint/restrict-template-expressions": "error",
42
+ "@typescript-eslint/no-base-to-string": "error",
43
+ "@typescript-eslint/require-await": "error",
44
+ "@typescript-eslint/switch-exhaustiveness-check": "error",
45
+
46
+ "no-console": "warn",
47
+ "no-debugger": "error",
48
+ eqeqeq: ["error", "always"],
49
+ "no-var": "error",
50
+ "prefer-const": "error",
51
+ "prefer-arrow-callback": "error",
52
+ "no-throw-literal": "error",
53
+ "no-return-await": "error",
54
+ "require-await": "off",
55
+ "no-async-promise-executor": "error",
56
+ "no-promise-executor-return": "error",
57
+
58
+ "no-eval": "error",
59
+ "no-implied-eval": "error",
60
+ "no-new-func": "error",
61
+ "no-new-wrappers": "error",
62
+ "no-useless-concat": "error",
63
+ "prefer-template": "error",
64
+ "no-lonely-if": "error",
65
+ "no-else-return": "error",
66
+ "no-unneeded-ternary": "error",
67
+ "prefer-destructuring": [
68
+ "warn",
69
+ { array: false, object: true },
70
+ { enforceForRenamedProperties: false },
71
+ ],
72
+
73
+ "import/no-unresolved": "off",
74
+ "import/named": "error",
75
+ "import/no-duplicates": "error",
76
+ "import/no-cycle": "error",
77
+ "import/no-self-import": "error",
78
+
79
+ "no-await-in-loop": "warn",
80
+ },
81
+ },
82
+ {
83
+ files: ["example.ts", "example.js"],
84
+ rules: {
85
+ "no-console": "off",
86
+ },
87
+ },
88
+ {
89
+ files: ["*.test.ts", "*.test.js", "*.spec.ts", "*.spec.js"],
90
+ rules: {
91
+ "@typescript-eslint/no-non-null-assertion": "off",
92
+ },
93
+ },
94
+ ];
package/example.ts CHANGED
@@ -47,7 +47,7 @@ const example4 = parseBIP321(
47
47
  "bitcoin:?lightning=lnbc15u1p3xnhl2pp5jptserfk3zk4qy42tlucycrfwxhydvlemu9pqr93tuzlv9cc7g3sdqsvfhkcap3xyhx7un8cqzpgxqzjcsp5f8c52y2stc300gl6s4xswtjpc37hrnnr3c9wvtgjfuvqmpm35evq9qyyssqy4lgd8tj637qcjp05rdpxxykjenthxftej7a2zzmwrmrl70fyj9hvj0rewhzj7jfyuwkwcg9g2jpwtk3wkjtwnkdks84hsnu8xps5vsq4gj5hs",
48
48
  );
49
49
  console.log(`Valid: ${example4.valid}`);
50
- console.log(`Address in path: ${example4.address || "none"}`);
50
+ console.log(`Address in path: ${example4.address ?? "none"}`);
51
51
  console.log(`Payment type: ${example4.paymentMethods[0]?.type}`);
52
52
  console.log(`Network: ${example4.paymentMethods[0]?.network}`);
53
53
  console.log();
@@ -71,8 +71,8 @@ const example6 = parseBIP321(
71
71
  );
72
72
  const byNetwork = getPaymentMethodsByNetwork(example6);
73
73
  console.log(`Valid: ${example6.valid}`);
74
- console.log(`Mainnet methods: ${byNetwork.mainnet?.length || 0}`);
75
- console.log(`Testnet methods: ${byNetwork.testnet?.length || 0}`);
74
+ console.log(`Mainnet methods: ${byNetwork.mainnet?.length ?? 0}`);
75
+ console.log(`Testnet methods: ${byNetwork.testnet?.length ?? 0}`);
76
76
  console.log();
77
77
 
78
78
  // Example 7: Testnet lightning invoice
package/index.test.ts CHANGED
@@ -30,6 +30,18 @@ const TEST_DATA = {
30
30
  signet:
31
31
  "lntbs10u1p5s6wgtsp5d8a763exauvdk6s5gwvl8zmuapmgjq05fdv6trasjd4slvgkvzzqpp56vxdyl24hmkpz0tvqq84xdpqqeql3x7kh8tey4uum2cu8jny6djqdq4g9exkgznw3hhyefqyvenyxqzjccqp2rzjqdwy5et9ygczjl2jqmr9e5xm28u3gksjfrf0pht04uwz2lt9d59cypqelcqqq8gqqqqqqqqpqqqqqzsqqc9qxpqysgq0x0pg2s65rnp2cr35td5tq0vwgmnrghkpzt93eypqvvfu5m40pcjl9k2x2m4kqgvz2ez8tzxqgw0nyeg2w60nfky579uakd4mhr3ncgp0xwars",
32
32
  },
33
+ ark: {
34
+ mainnet:
35
+ "ark1pwh9vsmezqqpjy9akejayl2vvcse6he97rn40g84xrlvrlnhayuuyefrp9nse2yspqqjl5wpy",
36
+ testnet:
37
+ "tark1pm6sr0fpzqqpnzzwxf209kju4qavs4gtumxk30yv2u5ncrvtp72z34axcvrydtdqpqq5838km",
38
+ },
39
+ silentPayment: {
40
+ mainnet:
41
+ "sp1qqvgwll3hawztz50nx5mcs70ytam4z068c2cw0z37zcg5yj23h65kcqamhqcxal0gerzly0jnkv7x0ar3sjhmh0n5yugyj3kd7ahzfsdw5590ajuk",
42
+ testnet:
43
+ "tsp1qq2svvt45f2rzmfr4vwhgvjfjgna92h09g9a9ttpvmz5x5wmscsepyqhkk6tjxzr6v0vj3q87gcrqjq73z6ljylgk4m6vphvkpg4afzwp4ve0nr78",
44
+ },
33
45
  } as const;
34
46
 
35
47
  describe("BIP-321 Parser", () => {
@@ -42,8 +54,8 @@ describe("BIP-321 Parser", () => {
42
54
  expect(result.address).toBe(TEST_DATA.addresses.mainnet.p2pkh);
43
55
  expect(result.network).toBe("mainnet");
44
56
  expect(result.paymentMethods.length).toBe(1);
45
- expect(result.paymentMethods[0]!.type).toBe("onchain");
46
- expect(result.paymentMethods[0]!.valid).toBe(true);
57
+ expect(result.paymentMethods[0]?.type).toBe("onchain");
58
+ expect(result.paymentMethods[0]?.valid).toBe(true);
47
59
  });
48
60
 
49
61
  test("parses bech32 mainnet address", () => {
@@ -52,7 +64,7 @@ describe("BIP-321 Parser", () => {
52
64
  );
53
65
  expect(result.valid).toBe(true);
54
66
  expect(result.network).toBe("mainnet");
55
- expect(result.paymentMethods[0]!.valid).toBe(true);
67
+ expect(result.paymentMethods[0]?.valid).toBe(true);
56
68
  });
57
69
 
58
70
  test("parses testnet address", () => {
@@ -193,29 +205,113 @@ describe("BIP-321 Parser", () => {
193
205
  });
194
206
 
195
207
  describe("Alternative Payment Methods", () => {
196
- test("parses BOLT12 offer", () => {
208
+ test("parses valid BOLT12 offer", () => {
209
+ const result = parseBIP321("bitcoin:?lno=lno1qqqq02k20d");
210
+ expect(result.valid).toBe(true);
211
+ expect(result.paymentMethods.length).toBe(1);
212
+ expect(result.paymentMethods[0]!.type).toBe("offer");
213
+ expect(result.paymentMethods[0]!.valid).toBe(true);
214
+ });
215
+
216
+ test("rejects invalid BOLT12 offer", () => {
197
217
  const result = parseBIP321("bitcoin:?lno=lno1bogusoffer");
218
+ expect(result.paymentMethods.length).toBe(1);
219
+ expect(result.paymentMethods[0]!.type).toBe("offer");
220
+ expect(result.paymentMethods[0]!.valid).toBe(false);
221
+ expect(result.errors.some((e) => e.includes("BOLT12 offer"))).toBe(true);
222
+ });
223
+
224
+ test("parses mainnet silent payment address", () => {
225
+ const result = parseBIP321(
226
+ `bitcoin:?sp=${TEST_DATA.silentPayment.mainnet}`,
227
+ );
198
228
  expect(result.valid).toBe(true);
199
229
  expect(result.paymentMethods.length).toBe(1);
200
- expect(result.paymentMethods[0]!.type).toBe("lno");
230
+ expect(result.paymentMethods[0]!.type).toBe("silent-payment");
231
+ expect(result.paymentMethods[0]!.network).toBe("mainnet");
232
+ expect(result.paymentMethods[0]!.valid).toBe(true);
201
233
  });
202
234
 
203
- test("parses silent payment address", () => {
204
- const result = parseBIP321("bitcoin:?sp=sp1qsilentpayment");
235
+ test("parses testnet silent payment address", () => {
236
+ const result = parseBIP321(
237
+ `bitcoin:?sp=${TEST_DATA.silentPayment.testnet}`,
238
+ );
205
239
  expect(result.valid).toBe(true);
206
240
  expect(result.paymentMethods.length).toBe(1);
207
241
  expect(result.paymentMethods[0]!.type).toBe("silent-payment");
242
+ expect(result.paymentMethods[0]!.network).toBe("testnet");
243
+ expect(result.paymentMethods[0]!.valid).toBe(true);
244
+ });
245
+
246
+ test("rejects invalid silent payment address", () => {
247
+ const result = parseBIP321("bitcoin:?sp=sp1qinvalid");
248
+ expect(result.paymentMethods.length).toBe(1);
249
+ expect(result.paymentMethods[0]!.type).toBe("silent-payment");
250
+ expect(result.paymentMethods[0]!.valid).toBe(false);
251
+ expect(result.errors.some((e) => e.includes("silent payment"))).toBe(
252
+ true,
253
+ );
208
254
  });
209
255
 
210
256
  test("parses multiple payment methods", () => {
211
257
  const result = parseBIP321(
212
- "bitcoin:?lno=lno1bogusoffer&sp=sp1qsilentpayment",
258
+ `bitcoin:?lno=lno1qqqq02k20d&sp=${TEST_DATA.silentPayment.mainnet}`,
213
259
  );
214
260
  expect(result.valid).toBe(true);
215
261
  expect(result.paymentMethods.length).toBe(2);
216
262
  });
217
263
  });
218
264
 
265
+ describe("Ark Addresses", () => {
266
+ test("parses mainnet Ark address", () => {
267
+ const result = parseBIP321(`bitcoin:?ark=${TEST_DATA.ark.mainnet}`);
268
+ expect(result.valid).toBe(true);
269
+ expect(result.paymentMethods.length).toBe(1);
270
+ expect(result.paymentMethods[0]!.type).toBe("ark");
271
+ expect(result.paymentMethods[0]!.network).toBe("mainnet");
272
+ expect(result.paymentMethods[0]!.valid).toBe(true);
273
+ });
274
+
275
+ test("parses testnet Ark address", () => {
276
+ const result = parseBIP321(`bitcoin:?ark=${TEST_DATA.ark.testnet}`);
277
+ expect(result.valid).toBe(true);
278
+ expect(result.paymentMethods.length).toBe(1);
279
+ expect(result.paymentMethods[0]!.type).toBe("ark");
280
+ expect(result.paymentMethods[0]!.network).toBe("testnet");
281
+ expect(result.paymentMethods[0]!.valid).toBe(true);
282
+ });
283
+
284
+ test("rejects invalid Ark address", () => {
285
+ const result = parseBIP321("bitcoin:?ark=invalid_ark_address");
286
+ expect(result.paymentMethods.length).toBe(1);
287
+ expect(result.paymentMethods[0]!.valid).toBe(false);
288
+ expect(result.errors.some((e) => e.includes("Ark address"))).toBe(true);
289
+ });
290
+
291
+ test("parses Ark with Bitcoin address", () => {
292
+ const result = parseBIP321(
293
+ `bitcoin:${TEST_DATA.addresses.mainnet.p2pkh}?ark=${TEST_DATA.ark.mainnet}`,
294
+ );
295
+ expect(result.valid).toBe(true);
296
+ expect(result.paymentMethods.length).toBe(2);
297
+ expect(result.paymentMethods[0]!.type).toBe("onchain");
298
+ expect(result.paymentMethods[1]!.type).toBe("ark");
299
+ expect(result.paymentMethods[0]!.network).toBe("mainnet");
300
+ expect(result.paymentMethods[1]!.network).toBe("mainnet");
301
+ });
302
+
303
+ test("validates Ark network matches expected network", () => {
304
+ const result = parseBIP321(
305
+ `bitcoin:?ark=${TEST_DATA.ark.mainnet}`,
306
+ "testnet",
307
+ );
308
+ expect(result.valid).toBe(false);
309
+ expect(result.errors.some((e) => e.includes("network mismatch"))).toBe(
310
+ true,
311
+ );
312
+ });
313
+ });
314
+
219
315
  describe("Network-specific Parameters", () => {
220
316
  test("parses bc parameter for mainnet", () => {
221
317
  const result = parseBIP321(
@@ -461,5 +557,45 @@ describe("BIP-321 Parser", () => {
461
557
  expect(result.paymentMethods.length).toBe(2);
462
558
  expect(result.paymentMethods.every((pm) => pm.valid)).toBe(true);
463
559
  });
560
+
561
+ test("accepts Ark testnet address when expecting regtest", () => {
562
+ const result = parseBIP321(
563
+ `bitcoin:?ark=${TEST_DATA.ark.testnet}`,
564
+ "regtest",
565
+ );
566
+ expect(result.valid).toBe(true);
567
+ expect(result.paymentMethods[0]!.type).toBe("ark");
568
+ expect(result.paymentMethods[0]!.valid).toBe(true);
569
+ });
570
+
571
+ test("accepts Ark testnet address when expecting signet", () => {
572
+ const result = parseBIP321(
573
+ `bitcoin:?ark=${TEST_DATA.ark.testnet}`,
574
+ "signet",
575
+ );
576
+ expect(result.valid).toBe(true);
577
+ expect(result.paymentMethods[0]!.type).toBe("ark");
578
+ expect(result.paymentMethods[0]!.valid).toBe(true);
579
+ });
580
+
581
+ test("accepts silent payment testnet address when expecting regtest", () => {
582
+ const result = parseBIP321(
583
+ `bitcoin:?sp=${TEST_DATA.silentPayment.testnet}`,
584
+ "regtest",
585
+ );
586
+ expect(result.valid).toBe(true);
587
+ expect(result.paymentMethods[0]!.type).toBe("silent-payment");
588
+ expect(result.paymentMethods[0]!.valid).toBe(true);
589
+ });
590
+
591
+ test("accepts silent payment testnet address when expecting signet", () => {
592
+ const result = parseBIP321(
593
+ `bitcoin:?sp=${TEST_DATA.silentPayment.testnet}`,
594
+ "signet",
595
+ );
596
+ expect(result.valid).toBe(true);
597
+ expect(result.paymentMethods[0]!.type).toBe("silent-payment");
598
+ expect(result.paymentMethods[0]!.valid).toBe(true);
599
+ });
464
600
  });
465
601
  });
package/index.ts CHANGED
@@ -1,10 +1,14 @@
1
- import * as bitcoin from "bitcoinjs-lib";
2
- import { decode as decodeLightning } from "light-bolt11-decoder";
3
- import { sha256 } from "@noble/hashes/sha2.js";
4
- import { base58, bech32, bech32m } from "@scure/base";
1
+ import {
2
+ validateBitcoinAddress,
3
+ validateLightningInvoice,
4
+ validateBolt12Offer,
5
+ validateSilentPaymentAddress,
6
+ validateArkAddress,
7
+ validatePopUri,
8
+ } from "./validators";
5
9
 
6
10
  export interface PaymentMethod {
7
- type: "onchain" | "lightning" | "lno" | "silent-payment" | "other";
11
+ type: "onchain" | "lightning" | "offer" | "silent-payment" | "ark";
8
12
  value: string;
9
13
  network?: "mainnet" | "testnet" | "regtest" | "signet";
10
14
  valid: boolean;
@@ -26,172 +30,6 @@ export interface BIP321ParseResult {
26
30
  errors: string[];
27
31
  }
28
32
 
29
- const FORBIDDEN_POP_SCHEMES = ["http", "https", "file", "javascript", "mailto"];
30
-
31
- function detectAddressNetwork(
32
- address: string,
33
- ): "mainnet" | "testnet" | "regtest" | "signet" | undefined {
34
- try {
35
- const lowerAddress = address.toLowerCase();
36
-
37
- // Bech32/Bech32m addresses
38
- if (lowerAddress.startsWith("bc1")) {
39
- try {
40
- // Try using bitcoinjs-lib first (works for non-taproot)
41
- bitcoin.address.toOutputScript(address, bitcoin.networks.bitcoin);
42
- return "mainnet";
43
- } catch {
44
- // Fallback to manual bech32/bech32m validation for taproot
45
- try {
46
- const decoded = lowerAddress.startsWith("bc1p")
47
- ? bech32m.decode(address as `${string}1${string}`, 90)
48
- : bech32.decode(address as `${string}1${string}`, 90);
49
- if (decoded.prefix === "bc") {
50
- return "mainnet";
51
- }
52
- } catch {
53
- return undefined;
54
- }
55
- }
56
- } else if (lowerAddress.startsWith("tb1")) {
57
- try {
58
- bitcoin.address.toOutputScript(address, bitcoin.networks.testnet);
59
- return "testnet";
60
- } catch {
61
- try {
62
- const decoded = lowerAddress.startsWith("tb1p")
63
- ? bech32m.decode(address as `${string}1${string}`, 90)
64
- : bech32.decode(address as `${string}1${string}`, 90);
65
- if (decoded.prefix === "tb") {
66
- return "testnet";
67
- }
68
- } catch {
69
- return undefined;
70
- }
71
- }
72
- } else if (lowerAddress.startsWith("bcrt1")) {
73
- try {
74
- bitcoin.address.toOutputScript(address, bitcoin.networks.regtest);
75
- return "regtest";
76
- } catch {
77
- try {
78
- const decoded = lowerAddress.startsWith("bcrt1p")
79
- ? bech32m.decode(address as `${string}1${string}`, 90)
80
- : bech32.decode(address as `${string}1${string}`, 90);
81
- if (decoded.prefix === "bcrt") {
82
- return "regtest";
83
- }
84
- } catch {
85
- return undefined;
86
- }
87
- }
88
- }
89
-
90
- // Base58 addresses (P2PKH, P2SH) - manual validation with checksum
91
- try {
92
- const decoded = base58.decode(address);
93
- if (decoded.length < 25) {
94
- return undefined;
95
- }
96
-
97
- // Verify checksum
98
- const payload = decoded.slice(0, -4);
99
- const checksum = decoded.slice(-4);
100
- const hash = sha256(sha256(payload));
101
-
102
- for (let i = 0; i < 4; i++) {
103
- if (checksum[i] !== hash[i]) {
104
- return undefined;
105
- }
106
- }
107
-
108
- const version = payload[0];
109
-
110
- // Mainnet: P2PKH (0x00), P2SH (0x05)
111
- if (version === 0x00 || version === 0x05) {
112
- return "mainnet";
113
- }
114
- // Testnet: P2PKH (0x6f), P2SH (0xc4)
115
- else if (version === 0x6f || version === 0xc4) {
116
- return "testnet";
117
- }
118
- } catch (e) {
119
- return undefined;
120
- }
121
- } catch {
122
- return undefined;
123
- }
124
- return undefined;
125
- }
126
-
127
- function validateBitcoinAddress(address: string): {
128
- valid: boolean;
129
- network?: "mainnet" | "testnet" | "regtest" | "signet";
130
- error?: string;
131
- } {
132
- if (!address) {
133
- return { valid: false, error: "Empty address" };
134
- }
135
-
136
- const network = detectAddressNetwork(address);
137
- if (!network) {
138
- return { valid: false, error: "Invalid bitcoin address" };
139
- }
140
-
141
- return { valid: true, network };
142
- }
143
-
144
- function validateLightningInvoice(invoice: string): {
145
- valid: boolean;
146
- network?: "mainnet" | "testnet" | "regtest" | "signet";
147
- error?: string;
148
- } {
149
- try {
150
- const _decoded = decodeLightning(invoice);
151
- let network: "mainnet" | "testnet" | "regtest" | "signet" | undefined;
152
-
153
- const lowerInvoice = invoice.toLowerCase();
154
- // Check order matters - lnbcrt before lnbc, lntbs before lntb
155
- if (lowerInvoice.startsWith("lnbcrt")) {
156
- network = "regtest";
157
- } else if (lowerInvoice.startsWith("lnbc")) {
158
- network = "mainnet";
159
- } else if (lowerInvoice.startsWith("lntbs")) {
160
- network = "signet";
161
- } else if (
162
- lowerInvoice.startsWith("lntb") ||
163
- lowerInvoice.startsWith("lnbt")
164
- ) {
165
- network = "testnet";
166
- }
167
-
168
- return { valid: true, network };
169
- } catch (e) {
170
- return {
171
- valid: false,
172
- error: `Invalid lightning invoice: ${e instanceof Error ? e.message : String(e)}`,
173
- };
174
- }
175
- }
176
-
177
- function validatePopUri(popUri: string): { valid: boolean; error?: string } {
178
- try {
179
- const decoded = decodeURIComponent(popUri);
180
- const schemeMatch = decoded.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):?/);
181
-
182
- if (schemeMatch && schemeMatch[1]) {
183
- const scheme = schemeMatch[1].toLowerCase();
184
- if (FORBIDDEN_POP_SCHEMES.includes(scheme)) {
185
- return { valid: false, error: `Forbidden pop scheme: ${scheme}` };
186
- }
187
- }
188
-
189
- return { valid: true };
190
- } catch {
191
- return { valid: false, error: "Invalid pop URI encoding" };
192
- }
193
- }
194
-
195
33
  export function parseBIP321(
196
34
  uri: string,
197
35
  expectedNetwork?: "mainnet" | "testnet" | "regtest" | "signet",
@@ -268,7 +106,7 @@ export function parseBIP321(
268
106
  }
269
107
 
270
108
  const lowerKey = key.toLowerCase();
271
- const count = (seenKeys.get(lowerKey) || 0) + 1;
109
+ const count = (seenKeys.get(lowerKey) ?? 0) + 1;
272
110
  seenKeys.set(lowerKey, count);
273
111
 
274
112
  if (lowerKey === "label") {
@@ -308,7 +146,7 @@ export function parseBIP321(
308
146
  // Keep pop value encoded as per spec
309
147
  const validation = validatePopUri(value);
310
148
  if (!validation.valid) {
311
- result.errors.push(validation.error || "Invalid pop URI");
149
+ result.errors.push(validation.error ?? "Invalid pop URI");
312
150
  if (lowerKey === "req-pop") {
313
151
  result.valid = false;
314
152
  }
@@ -327,28 +165,47 @@ export function parseBIP321(
327
165
  error: validation.error,
328
166
  });
329
167
  if (!validation.valid) {
330
- result.errors.push(validation.error || "Invalid lightning invoice");
168
+ result.errors.push(validation.error ?? "Invalid lightning invoice");
331
169
  }
332
170
  } else if (lowerKey === "lno") {
333
171
  const decodedValue = decodeURIComponent(value);
172
+ const validation = validateBolt12Offer(decodedValue);
334
173
  result.paymentMethods.push({
335
- type: "lno",
174
+ type: "offer",
336
175
  value: decodedValue,
337
- valid: true,
176
+ valid: validation.valid,
177
+ error: validation.error,
338
178
  });
179
+ if (!validation.valid) {
180
+ result.errors.push(validation.error ?? "Invalid BOLT12 offer");
181
+ }
339
182
  } else if (lowerKey === "sp") {
340
183
  const decodedValue = decodeURIComponent(value);
341
- const isSilentPayment = decodedValue.toLowerCase().startsWith("sp1");
184
+ const validation = validateSilentPaymentAddress(decodedValue);
342
185
  result.paymentMethods.push({
343
186
  type: "silent-payment",
344
187
  value: decodedValue,
345
- valid: isSilentPayment,
346
- error: isSilentPayment
347
- ? undefined
348
- : "Invalid silent payment address format",
188
+ network: validation.network,
189
+ valid: validation.valid,
190
+ error: validation.error,
349
191
  });
350
- if (!isSilentPayment) {
351
- result.errors.push("Invalid silent payment address format");
192
+ if (!validation.valid) {
193
+ result.errors.push(
194
+ validation.error ?? "Invalid silent payment address",
195
+ );
196
+ }
197
+ } else if (lowerKey === "ark") {
198
+ const decodedValue = decodeURIComponent(value);
199
+ const validation = validateArkAddress(decodedValue);
200
+ result.paymentMethods.push({
201
+ type: "ark",
202
+ value: decodedValue,
203
+ network: validation.network,
204
+ valid: validation.valid,
205
+ error: validation.error,
206
+ });
207
+ if (!validation.valid) {
208
+ result.errors.push(validation.error ?? "Invalid Ark address");
352
209
  }
353
210
  } else if (
354
211
  lowerKey === "bc" ||
@@ -382,7 +239,7 @@ export function parseBIP321(
382
239
  if (!validation.valid || !networkMatches) {
383
240
  result.errors.push(
384
241
  !validation.valid
385
- ? validation.error!
242
+ ? (validation.error ?? "Invalid address")
386
243
  : `Address network mismatch for ${lowerKey} parameter`,
387
244
  );
388
245
  result.valid = false;
@@ -392,9 +249,7 @@ export function parseBIP321(
392
249
  result.errors.push(`Unknown required parameter: ${key}`);
393
250
  result.valid = false;
394
251
  } else {
395
- if (!result.optionalParams[lowerKey]) {
396
- result.optionalParams[lowerKey] = [];
397
- }
252
+ result.optionalParams[lowerKey] ??= [];
398
253
  result.optionalParams[lowerKey].push(decodeURIComponent(value));
399
254
  }
400
255
  }
@@ -408,12 +263,22 @@ export function parseBIP321(
408
263
  if (expectedNetwork) {
409
264
  for (const method of result.paymentMethods) {
410
265
  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}`;
266
+ // For Ark and Silent Payments, testnet covers testnet/signet/regtest
267
+ const isTestnetCompatible =
268
+ (method.type === "ark" || method.type === "silent-payment") &&
269
+ method.network === "testnet" &&
270
+ (expectedNetwork === "testnet" ||
271
+ expectedNetwork === "signet" ||
272
+ expectedNetwork === "regtest");
273
+
274
+ if (!isTestnetCompatible) {
275
+ result.errors.push(
276
+ `Payment method network mismatch: expected ${expectedNetwork}, got ${method.network}`,
277
+ );
278
+ result.valid = false;
279
+ method.valid = false;
280
+ method.error = `Network mismatch: expected ${expectedNetwork}`;
281
+ }
417
282
  }
418
283
  }
419
284
  }
@@ -442,10 +307,17 @@ export function getPaymentMethodsByNetwork(
442
307
  };
443
308
 
444
309
  for (const method of result.paymentMethods) {
445
- if (method.network && byNetwork[method.network]) {
446
- byNetwork[method.network]!.push(method);
310
+ const { network } = method;
311
+ if (network && network in byNetwork) {
312
+ const networkArray = byNetwork[network];
313
+ if (networkArray) {
314
+ networkArray.push(method);
315
+ }
447
316
  } else {
448
- byNetwork.unknown!.push(method);
317
+ const unknownArray = byNetwork.unknown;
318
+ if (unknownArray) {
319
+ unknownArray.push(method);
320
+ }
449
321
  }
450
322
  }
451
323
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bip-321",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
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",
@@ -16,7 +16,7 @@
16
16
  "test": "bun test",
17
17
  "example": "bun example.ts",
18
18
  "check": "tsc --noEmit",
19
- "lint": "oxlint"
19
+ "lint": "eslint ."
20
20
  },
21
21
  "keywords": [
22
22
  "bitcoin",
@@ -45,11 +45,15 @@
45
45
  "@noble/hashes": "^2.0.1",
46
46
  "@scure/base": "^2.0.0",
47
47
  "bitcoinjs-lib": "^7.0.0",
48
- "light-bolt11-decoder": "^3.2.0"
48
+ "light-bolt11-decoder": "^3.2.0",
49
+ "light-bolt12-decoder": "^1.0.3"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/bun": "latest",
52
- "oxlint": "^1.26.0"
53
+ "@typescript-eslint/eslint-plugin": "^8.46.3",
54
+ "@typescript-eslint/parser": "^8.46.3",
55
+ "eslint": "^9.39.1",
56
+ "eslint-plugin-import": "^2.32.0"
53
57
  },
54
58
  "peerDependencies": {
55
59
  "typescript": "^5"