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.
- package/README.md +33 -5
- package/bun.lock +467 -11
- package/eslint.config.js +94 -0
- package/example.ts +3 -3
- package/index.test.ts +144 -8
- package/index.ts +68 -196
- package/package.json +8 -4
- package/validators.ts +311 -0
- package/oxlintrc.json +0 -23
package/eslint.config.js
ADDED
|
@@ -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
|
|
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
|
|
75
|
-
console.log(`Testnet methods: ${byNetwork.testnet?.length
|
|
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]
|
|
46
|
-
expect(result.paymentMethods[0]
|
|
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]
|
|
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("
|
|
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(
|
|
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
|
-
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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" | "
|
|
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)
|
|
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
|
|
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
|
|
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: "
|
|
174
|
+
type: "offer",
|
|
336
175
|
value: decodedValue,
|
|
337
|
-
valid:
|
|
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
|
|
184
|
+
const validation = validateSilentPaymentAddress(decodedValue);
|
|
342
185
|
result.paymentMethods.push({
|
|
343
186
|
type: "silent-payment",
|
|
344
187
|
value: decodedValue,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
: "Invalid silent payment address format",
|
|
188
|
+
network: validation.network,
|
|
189
|
+
valid: validation.valid,
|
|
190
|
+
error: validation.error,
|
|
349
191
|
});
|
|
350
|
-
if (!
|
|
351
|
-
result.errors.push(
|
|
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
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
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"
|