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.
- 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 +104 -8
- package/index.ts +172 -22
- package/package.json +8 -4
- 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(
|
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" | "
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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: "
|
|
462
|
+
type: "offer",
|
|
336
463
|
value: decodedValue,
|
|
337
|
-
valid:
|
|
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
|
|
472
|
+
const validation = validateSilentPaymentAddress(decodedValue);
|
|
342
473
|
result.paymentMethods.push({
|
|
343
474
|
type: "silent-payment",
|
|
344
475
|
value: decodedValue,
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
: "Invalid silent payment address format",
|
|
476
|
+
network: validation.network,
|
|
477
|
+
valid: validation.valid,
|
|
478
|
+
error: validation.error,
|
|
349
479
|
});
|
|
350
|
-
if (!
|
|
351
|
-
result.errors.push(
|
|
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
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
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"
|
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
|
-
}
|