bip-321 0.0.4 → 0.0.5

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(
package/index.ts CHANGED
@@ -3,8 +3,17 @@ import { decode as decodeLightning } from "light-bolt11-decoder";
3
3
  import { sha256 } from "@noble/hashes/sha2.js";
4
4
  import { base58, bech32, bech32m } from "@scure/base";
5
5
 
6
+ // Type declaration for missing export in light-bolt12-decoder
7
+ declare module "light-bolt12-decoder" {
8
+ export function decode(offer: string): {
9
+ offerRequest: string;
10
+ sections: Array<{ name: string; value: unknown }>;
11
+ };
12
+ }
13
+ import { decode as decodeBolt12 } from "light-bolt12-decoder";
14
+
6
15
  export interface PaymentMethod {
7
- type: "onchain" | "lightning" | "lno" | "silent-payment" | "other";
16
+ type: "onchain" | "lightning" | "offer" | "silent-payment" | "ark";
8
17
  value: string;
9
18
  network?: "mainnet" | "testnet" | "regtest" | "signet";
10
19
  valid: boolean;
@@ -115,7 +124,7 @@ function detectAddressNetwork(
115
124
  else if (version === 0x6f || version === 0xc4) {
116
125
  return "testnet";
117
126
  }
118
- } catch (e) {
127
+ } catch {
119
128
  return undefined;
120
129
  }
121
130
  } catch {
@@ -174,12 +183,129 @@ function validateLightningInvoice(invoice: string): {
174
183
  }
175
184
  }
176
185
 
186
+ function validateBolt12Offer(offer: string): {
187
+ valid: boolean;
188
+ error?: string;
189
+ } {
190
+ try {
191
+ if (!offer || typeof offer !== "string") {
192
+ return { valid: false, error: "Empty or invalid offer" };
193
+ }
194
+
195
+ const lowerOffer = offer.toLowerCase();
196
+ if (!lowerOffer.startsWith("ln")) {
197
+ return { valid: false, error: "Invalid BOLT12 offer format" };
198
+ }
199
+
200
+ decodeBolt12(offer);
201
+ return { valid: true };
202
+ } catch (e) {
203
+ return {
204
+ valid: false,
205
+ error: `Invalid BOLT12 offer: ${e instanceof Error ? e.message : String(e)}`,
206
+ };
207
+ }
208
+ }
209
+
210
+ function validateSilentPaymentAddress(address: string): {
211
+ valid: boolean;
212
+ network?: "mainnet" | "testnet" | "regtest" | "signet";
213
+ error?: string;
214
+ } {
215
+ try {
216
+ const lowerAddress = address.toLowerCase();
217
+
218
+ let network: "mainnet" | "testnet" | undefined;
219
+ if (lowerAddress.startsWith("sp1q")) {
220
+ network = "mainnet";
221
+ } else if (lowerAddress.startsWith("tsp1q")) {
222
+ network = "testnet";
223
+ } else {
224
+ return { valid: false, error: "Invalid silent payment address prefix" };
225
+ }
226
+
227
+ const decoded = bech32m.decode(address as `${string}1${string}`, 1023);
228
+
229
+ const expectedPrefix = network === "mainnet" ? "sp" : "tsp";
230
+ if (decoded.prefix !== expectedPrefix) {
231
+ return { valid: false, error: "Invalid silent payment address prefix" };
232
+ }
233
+
234
+ // Check version (first word should be 0 for v0, which encodes as 'q')
235
+ if (decoded.words.length === 0 || decoded.words[0] !== 0) {
236
+ return { valid: false, error: "Unsupported silent payment version" };
237
+ }
238
+
239
+ // Convert from 5-bit words to bytes
240
+ const dataWords = decoded.words.slice(1);
241
+ const data = bech32m.fromWordsUnsafe(dataWords);
242
+
243
+ if (!data) {
244
+ return { valid: false, error: "Invalid silent payment address data" };
245
+ }
246
+
247
+ // BIP-352: v0 addresses must be exactly 66 bytes (33-byte scan key + 33-byte spend key)
248
+ if (data.length !== 66) {
249
+ return { valid: false, error: "Invalid silent payment address length" };
250
+ }
251
+
252
+ // Validate both public keys are valid compressed keys (0x02 or 0x03 prefix)
253
+ const scanKey = data[0];
254
+ const spendKey = data[33];
255
+ if (
256
+ (scanKey !== 0x02 && scanKey !== 0x03) ||
257
+ (spendKey !== 0x02 && spendKey !== 0x03)
258
+ ) {
259
+ return {
260
+ valid: false,
261
+ error: "Invalid public key format in silent payment address",
262
+ };
263
+ }
264
+
265
+ return { valid: true, network };
266
+ } catch (e) {
267
+ return {
268
+ valid: false,
269
+ error: `Invalid silent payment address: ${e instanceof Error ? e.message : String(e)}`,
270
+ };
271
+ }
272
+ }
273
+
274
+ function validateArkAddress(address: string): {
275
+ valid: boolean;
276
+ network?: "mainnet" | "testnet" | "regtest" | "signet";
277
+ error?: string;
278
+ } {
279
+ try {
280
+ const lowerAddress = address.toLowerCase();
281
+
282
+ if (lowerAddress.startsWith("ark1")) {
283
+ const decoded = bech32m.decode(address as `${string}1${string}`, 1023);
284
+ if (decoded.prefix === "ark") {
285
+ return { valid: true, network: "mainnet" };
286
+ }
287
+ } else if (lowerAddress.startsWith("tark1")) {
288
+ const decoded = bech32m.decode(address as `${string}1${string}`, 1023);
289
+ if (decoded.prefix === "tark") {
290
+ return { valid: true, network: "testnet" };
291
+ }
292
+ }
293
+
294
+ return { valid: false, error: "Invalid Ark address format" };
295
+ } catch (e) {
296
+ return {
297
+ valid: false,
298
+ error: `Invalid Ark address: ${e instanceof Error ? e.message : String(e)}`,
299
+ };
300
+ }
301
+ }
302
+
177
303
  function validatePopUri(popUri: string): { valid: boolean; error?: string } {
178
304
  try {
179
305
  const decoded = decodeURIComponent(popUri);
180
306
  const schemeMatch = decoded.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):?/);
181
307
 
182
- if (schemeMatch && schemeMatch[1]) {
308
+ if (schemeMatch?.[1]) {
183
309
  const scheme = schemeMatch[1].toLowerCase();
184
310
  if (FORBIDDEN_POP_SCHEMES.includes(scheme)) {
185
311
  return { valid: false, error: `Forbidden pop scheme: ${scheme}` };
@@ -268,7 +394,7 @@ export function parseBIP321(
268
394
  }
269
395
 
270
396
  const lowerKey = key.toLowerCase();
271
- const count = (seenKeys.get(lowerKey) || 0) + 1;
397
+ const count = (seenKeys.get(lowerKey) ?? 0) + 1;
272
398
  seenKeys.set(lowerKey, count);
273
399
 
274
400
  if (lowerKey === "label") {
@@ -308,7 +434,7 @@ export function parseBIP321(
308
434
  // Keep pop value encoded as per spec
309
435
  const validation = validatePopUri(value);
310
436
  if (!validation.valid) {
311
- result.errors.push(validation.error || "Invalid pop URI");
437
+ result.errors.push(validation.error ?? "Invalid pop URI");
312
438
  if (lowerKey === "req-pop") {
313
439
  result.valid = false;
314
440
  }
@@ -327,28 +453,47 @@ export function parseBIP321(
327
453
  error: validation.error,
328
454
  });
329
455
  if (!validation.valid) {
330
- result.errors.push(validation.error || "Invalid lightning invoice");
456
+ result.errors.push(validation.error ?? "Invalid lightning invoice");
331
457
  }
332
458
  } else if (lowerKey === "lno") {
333
459
  const decodedValue = decodeURIComponent(value);
460
+ const validation = validateBolt12Offer(decodedValue);
334
461
  result.paymentMethods.push({
335
- type: "lno",
462
+ type: "offer",
336
463
  value: decodedValue,
337
- valid: true,
464
+ valid: validation.valid,
465
+ error: validation.error,
338
466
  });
467
+ if (!validation.valid) {
468
+ result.errors.push(validation.error ?? "Invalid BOLT12 offer");
469
+ }
339
470
  } else if (lowerKey === "sp") {
340
471
  const decodedValue = decodeURIComponent(value);
341
- const isSilentPayment = decodedValue.toLowerCase().startsWith("sp1");
472
+ const validation = validateSilentPaymentAddress(decodedValue);
342
473
  result.paymentMethods.push({
343
474
  type: "silent-payment",
344
475
  value: decodedValue,
345
- valid: isSilentPayment,
346
- error: isSilentPayment
347
- ? undefined
348
- : "Invalid silent payment address format",
476
+ network: validation.network,
477
+ valid: validation.valid,
478
+ error: validation.error,
349
479
  });
350
- if (!isSilentPayment) {
351
- result.errors.push("Invalid silent payment address format");
480
+ if (!validation.valid) {
481
+ result.errors.push(
482
+ validation.error ?? "Invalid silent payment address",
483
+ );
484
+ }
485
+ } else if (lowerKey === "ark") {
486
+ const decodedValue = decodeURIComponent(value);
487
+ const validation = validateArkAddress(decodedValue);
488
+ result.paymentMethods.push({
489
+ type: "ark",
490
+ value: decodedValue,
491
+ network: validation.network,
492
+ valid: validation.valid,
493
+ error: validation.error,
494
+ });
495
+ if (!validation.valid) {
496
+ result.errors.push(validation.error ?? "Invalid Ark address");
352
497
  }
353
498
  } else if (
354
499
  lowerKey === "bc" ||
@@ -382,7 +527,7 @@ export function parseBIP321(
382
527
  if (!validation.valid || !networkMatches) {
383
528
  result.errors.push(
384
529
  !validation.valid
385
- ? validation.error!
530
+ ? (validation.error ?? "Invalid address")
386
531
  : `Address network mismatch for ${lowerKey} parameter`,
387
532
  );
388
533
  result.valid = false;
@@ -392,9 +537,7 @@ export function parseBIP321(
392
537
  result.errors.push(`Unknown required parameter: ${key}`);
393
538
  result.valid = false;
394
539
  } else {
395
- if (!result.optionalParams[lowerKey]) {
396
- result.optionalParams[lowerKey] = [];
397
- }
540
+ result.optionalParams[lowerKey] ??= [];
398
541
  result.optionalParams[lowerKey].push(decodeURIComponent(value));
399
542
  }
400
543
  }
@@ -442,10 +585,17 @@ export function getPaymentMethodsByNetwork(
442
585
  };
443
586
 
444
587
  for (const method of result.paymentMethods) {
445
- if (method.network && byNetwork[method.network]) {
446
- byNetwork[method.network]!.push(method);
588
+ const { network } = method;
589
+ if (network && network in byNetwork) {
590
+ const networkArray = byNetwork[network];
591
+ if (networkArray) {
592
+ networkArray.push(method);
593
+ }
447
594
  } else {
448
- byNetwork.unknown!.push(method);
595
+ const unknownArray = byNetwork.unknown;
596
+ if (unknownArray) {
597
+ unknownArray.push(method);
598
+ }
449
599
  }
450
600
  }
451
601
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bip-321",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
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"
package/oxlintrc.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "categories": {
3
- "correctness": "warn",
4
- "suspicious": "warn",
5
- "pedantic": "warn",
6
- "perf": "warn",
7
- "style": "warn",
8
- "restriction": "warn"
9
- },
10
- "rules": {
11
- "typescript/no-explicit-any": "error",
12
- "typescript/no-non-null-assertion": "error",
13
- "no-console": "warn",
14
- "no-debugger": "error",
15
- "eqeqeq": "error"
16
- },
17
- "ignorePatterns": [
18
- "**/node_modules/**",
19
- "**/dist/**",
20
- "**/build/**",
21
- "**/*.min.js"
22
- ]
23
- }