parseBip21.ts raw
1 export type Bip21Data = {
2 address: string;
3 amountSat?: number;
4 label?: string;
5 message?: string;
6 lightning?: string; // BOLT11 invoice
7 };
8
9 export function parseBip21(uri: string): Bip21Data {
10 // Strip the bitcoin: scheme (handles bitcoin:, bitcoin://, BITCOIN:, etc.)
11 const withoutScheme = uri.replace(/^bitcoin:\/?\/?/i, "");
12 if (!withoutScheme) {
13 throw new Error("Invalid BIP21 URI: missing address");
14 }
15
16 const separatorIndex = withoutScheme.indexOf("?");
17 const address =
18 separatorIndex >= 0
19 ? withoutScheme.slice(0, separatorIndex)
20 : withoutScheme;
21
22 const result: Bip21Data = { address };
23
24 if (separatorIndex >= 0) {
25 const rawParams = new URLSearchParams(
26 withoutScheme.slice(separatorIndex + 1)
27 );
28 // BIP-21 query parameter keys are case-insensitive
29 const params = new Map<string, string>();
30 for (const [key, value] of rawParams) {
31 const lower = key.toLowerCase();
32 if (!params.has(lower)) {
33 params.set(lower, value);
34 }
35 }
36
37 const amountBtc = params.get("amount");
38 if (amountBtc) {
39 result.amountSat = Math.round(parseFloat(amountBtc) * 100_000_000);
40 }
41
42 const label = params.get("label");
43 if (label) {
44 result.label = label;
45 }
46
47 const message = params.get("message");
48 if (message) {
49 result.message = message;
50 }
51
52 const lightning = params.get("lightning");
53 if (lightning) {
54 result.lightning = lightning;
55 }
56 }
57
58 return result;
59 }
60