@getalby/lightning-tools 8.1.1 → 8.2.0
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 +78 -13
- package/dist/cjs/402/l402.cjs +1304 -2
- package/dist/cjs/402/l402.cjs.map +1 -1
- package/dist/cjs/402/mpp.cjs +1306 -5
- package/dist/cjs/402/mpp.cjs.map +1 -1
- package/dist/cjs/402/x402.cjs +46 -15
- package/dist/cjs/402/x402.cjs.map +1 -1
- package/dist/cjs/402.cjs +101 -10
- package/dist/cjs/402.cjs.map +1 -1
- package/dist/cjs/bip21.cjs +115 -0
- package/dist/cjs/bip21.cjs.map +1 -0
- package/dist/cjs/index.cjs +213 -10
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/402/l402.js +1304 -2
- package/dist/esm/402/l402.js.map +1 -1
- package/dist/esm/402/mpp.js +1306 -5
- package/dist/esm/402/mpp.js.map +1 -1
- package/dist/esm/402/x402.js +46 -15
- package/dist/esm/402/x402.js.map +1 -1
- package/dist/esm/402.js +98 -11
- package/dist/esm/402.js.map +1 -1
- package/dist/esm/bip21.js +112 -0
- package/dist/esm/bip21.js.map +1 -0
- package/dist/esm/index.js +208 -11
- package/dist/esm/index.js.map +1 -1
- package/dist/lightning-tools.umd.js +2 -2
- package/dist/lightning-tools.umd.js.map +1 -1
- package/dist/types/402/l402.d.ts +49 -3
- package/dist/types/402/mpp.d.ts +54 -6
- package/dist/types/402/x402.d.ts +49 -3
- package/dist/types/402.d.ts +68 -17
- package/dist/types/bip21.d.ts +46 -0
- package/dist/types/index.d.ts +112 -17
- package/package.json +6 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const BIP21_SCHEME = /^bitcoin:/i;
|
|
4
|
+
// BIP21 grammar: amountparam = "amount=" *digit [ "." *digit ]
|
|
5
|
+
// We require at least one digit on either side of the decimal point — this is
|
|
6
|
+
// slightly stricter than the spec ABNF but matches every real-world example
|
|
7
|
+
// (the spec shows "50", "50.00", "20.3"). Crucially this rejects scientific
|
|
8
|
+
// notation ("1e-3"), hex ("0x10"), commas, signs, and leading "+" / "-".
|
|
9
|
+
const BIP21_AMOUNT_RE = /^(\d+)(?:\.(\d+))?$/;
|
|
10
|
+
const SATS_PER_BTC = 100000000n;
|
|
11
|
+
const BTC_DECIMALS = 8;
|
|
12
|
+
/**
|
|
13
|
+
* Convert a BIP21-compliant decimal BTC string to an integer number of
|
|
14
|
+
* satoshis using exact decimal arithmetic (no floats). Fractional digits
|
|
15
|
+
* beyond 8 are rounded half-up to the nearest satoshi.
|
|
16
|
+
*
|
|
17
|
+
* Assumes the input has already been validated against BIP21_AMOUNT_RE.
|
|
18
|
+
*/
|
|
19
|
+
const btcStringToSats = (btc) => {
|
|
20
|
+
const match = BIP21_AMOUNT_RE.exec(btc);
|
|
21
|
+
if (!match) {
|
|
22
|
+
// Should be unreachable — caller pre-validates.
|
|
23
|
+
return Number.NaN;
|
|
24
|
+
}
|
|
25
|
+
const [, integerPart, rawFractional = ""] = match;
|
|
26
|
+
let fractionalSats;
|
|
27
|
+
if (rawFractional.length <= BTC_DECIMALS) {
|
|
28
|
+
fractionalSats = BigInt(rawFractional.padEnd(BTC_DECIMALS, "0"));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Round half-up at the satoshi boundary.
|
|
32
|
+
const truncated = rawFractional.slice(0, BTC_DECIMALS);
|
|
33
|
+
const roundDigit = rawFractional.charCodeAt(BTC_DECIMALS) - 48;
|
|
34
|
+
fractionalSats = BigInt(truncated);
|
|
35
|
+
if (roundDigit >= 5)
|
|
36
|
+
fractionalSats += 1n;
|
|
37
|
+
}
|
|
38
|
+
const totalSats = BigInt(integerPart) * SATS_PER_BTC + fractionalSats;
|
|
39
|
+
return Number(totalSats);
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Parse a BIP21 (`bitcoin:`) URI. Returns `null` if the input doesn't have the
|
|
43
|
+
* `bitcoin:` scheme.
|
|
44
|
+
*
|
|
45
|
+
* The address is returned as-is (case preserved) — callers should validate it
|
|
46
|
+
* separately if they need to ensure it's a well-formed bitcoin address.
|
|
47
|
+
*
|
|
48
|
+
* Per BIP21, parameters prefixed with `req-` are required: if the client
|
|
49
|
+
* doesn't understand any of them, the payment MUST NOT be made. The unknown
|
|
50
|
+
* required params are surfaced via `unknownRequiredParams` so callers can
|
|
51
|
+
* decide how to fail.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* parseBip21("bitcoin:bc1q...?amount=0.001&lightning=lnbc...")
|
|
55
|
+
* // => { address: "bc1q...", amount: 0.001, amountSats: 100000, lightning: "lnbc...", ... }
|
|
56
|
+
*/
|
|
57
|
+
const parseBip21 = (uri) => {
|
|
58
|
+
if (typeof uri !== "string") {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const normalized = uri.trim();
|
|
62
|
+
if (!BIP21_SCHEME.test(normalized)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const withoutScheme = normalized.replace(BIP21_SCHEME, "");
|
|
66
|
+
const queryStart = withoutScheme.indexOf("?");
|
|
67
|
+
const address = queryStart === -1 ? withoutScheme : withoutScheme.slice(0, queryStart);
|
|
68
|
+
const query = queryStart === -1 ? "" : withoutScheme.slice(queryStart + 1);
|
|
69
|
+
const params = {};
|
|
70
|
+
const unknownRequiredParams = [];
|
|
71
|
+
if (query) {
|
|
72
|
+
// URLSearchParams handles decoding and repeated params; for BIP21 last-write-wins
|
|
73
|
+
// is fine since the spec doesn't allow repeats.
|
|
74
|
+
const search = new URLSearchParams(query);
|
|
75
|
+
search.forEach((value, key) => {
|
|
76
|
+
params[key] = value;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const knownParams = new Set([
|
|
80
|
+
"amount",
|
|
81
|
+
"label",
|
|
82
|
+
"message",
|
|
83
|
+
"lightning",
|
|
84
|
+
"lno",
|
|
85
|
+
]);
|
|
86
|
+
for (const key of Object.keys(params)) {
|
|
87
|
+
if (key.startsWith("req-") && !knownParams.has(key.slice(4))) {
|
|
88
|
+
unknownRequiredParams.push(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const result = {
|
|
92
|
+
address: address.trim(),
|
|
93
|
+
params,
|
|
94
|
+
unknownRequiredParams,
|
|
95
|
+
};
|
|
96
|
+
if (params.amount !== undefined && BIP21_AMOUNT_RE.test(params.amount)) {
|
|
97
|
+
result.amount = Number(params.amount);
|
|
98
|
+
result.amountSats = btcStringToSats(params.amount);
|
|
99
|
+
}
|
|
100
|
+
if (params.label !== undefined)
|
|
101
|
+
result.label = params.label;
|
|
102
|
+
if (params.message !== undefined)
|
|
103
|
+
result.message = params.message;
|
|
104
|
+
if (params.lightning !== undefined)
|
|
105
|
+
result.lightning = params.lightning;
|
|
106
|
+
if (params.lno !== undefined)
|
|
107
|
+
result.lno = params.lno;
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
/** Returns true if the input starts with the `bitcoin:` URI scheme. */
|
|
111
|
+
const isBip21 = (uri) => typeof uri === "string" && BIP21_SCHEME.test(uri.trim());
|
|
112
|
+
|
|
113
|
+
exports.isBip21 = isBip21;
|
|
114
|
+
exports.parseBip21 = parseBip21;
|
|
115
|
+
//# sourceMappingURL=bip21.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bip21.cjs","sources":["../../src/bip21/utils.ts"],"sourcesContent":["import type { Bip21 } from \"./types\";\n\nconst BIP21_SCHEME = /^bitcoin:/i;\n\n// BIP21 grammar: amountparam = \"amount=\" *digit [ \".\" *digit ]\n// We require at least one digit on either side of the decimal point — this is\n// slightly stricter than the spec ABNF but matches every real-world example\n// (the spec shows \"50\", \"50.00\", \"20.3\"). Crucially this rejects scientific\n// notation (\"1e-3\"), hex (\"0x10\"), commas, signs, and leading \"+\" / \"-\".\nconst BIP21_AMOUNT_RE = /^(\\d+)(?:\\.(\\d+))?$/;\n\nconst SATS_PER_BTC = 100_000_000n;\nconst BTC_DECIMALS = 8;\n\n/**\n * Convert a BIP21-compliant decimal BTC string to an integer number of\n * satoshis using exact decimal arithmetic (no floats). Fractional digits\n * beyond 8 are rounded half-up to the nearest satoshi.\n *\n * Assumes the input has already been validated against BIP21_AMOUNT_RE.\n */\nconst btcStringToSats = (btc: string): number => {\n const match = BIP21_AMOUNT_RE.exec(btc);\n if (!match) {\n // Should be unreachable — caller pre-validates.\n return Number.NaN;\n }\n const [, integerPart, rawFractional = \"\"] = match;\n\n let fractionalSats: bigint;\n if (rawFractional.length <= BTC_DECIMALS) {\n fractionalSats = BigInt(rawFractional.padEnd(BTC_DECIMALS, \"0\"));\n } else {\n // Round half-up at the satoshi boundary.\n const truncated = rawFractional.slice(0, BTC_DECIMALS);\n const roundDigit = rawFractional.charCodeAt(BTC_DECIMALS) - 48;\n fractionalSats = BigInt(truncated);\n if (roundDigit >= 5) fractionalSats += 1n;\n }\n\n const totalSats = BigInt(integerPart) * SATS_PER_BTC + fractionalSats;\n return Number(totalSats);\n};\n\n/**\n * Parse a BIP21 (`bitcoin:`) URI. Returns `null` if the input doesn't have the\n * `bitcoin:` scheme.\n *\n * The address is returned as-is (case preserved) — callers should validate it\n * separately if they need to ensure it's a well-formed bitcoin address.\n *\n * Per BIP21, parameters prefixed with `req-` are required: if the client\n * doesn't understand any of them, the payment MUST NOT be made. The unknown\n * required params are surfaced via `unknownRequiredParams` so callers can\n * decide how to fail.\n *\n * @example\n * parseBip21(\"bitcoin:bc1q...?amount=0.001&lightning=lnbc...\")\n * // => { address: \"bc1q...\", amount: 0.001, amountSats: 100000, lightning: \"lnbc...\", ... }\n */\nexport const parseBip21 = (uri: string): Bip21 | null => {\n if (typeof uri !== \"string\") {\n return null;\n }\n const normalized = uri.trim();\n if (!BIP21_SCHEME.test(normalized)) {\n return null;\n }\n\n const withoutScheme = normalized.replace(BIP21_SCHEME, \"\");\n const queryStart = withoutScheme.indexOf(\"?\");\n const address =\n queryStart === -1 ? withoutScheme : withoutScheme.slice(0, queryStart);\n const query = queryStart === -1 ? \"\" : withoutScheme.slice(queryStart + 1);\n\n const params: Record<string, string> = {};\n const unknownRequiredParams: string[] = [];\n\n if (query) {\n // URLSearchParams handles decoding and repeated params; for BIP21 last-write-wins\n // is fine since the spec doesn't allow repeats.\n const search = new URLSearchParams(query);\n search.forEach((value, key) => {\n params[key] = value;\n });\n }\n\n const knownParams = new Set([\n \"amount\",\n \"label\",\n \"message\",\n \"lightning\",\n \"lno\",\n ]);\n\n for (const key of Object.keys(params)) {\n if (key.startsWith(\"req-\") && !knownParams.has(key.slice(4))) {\n unknownRequiredParams.push(key);\n }\n }\n\n const result: Bip21 = {\n address: address.trim(),\n params,\n unknownRequiredParams,\n };\n\n if (params.amount !== undefined && BIP21_AMOUNT_RE.test(params.amount)) {\n result.amount = Number(params.amount);\n result.amountSats = btcStringToSats(params.amount);\n }\n if (params.label !== undefined) result.label = params.label;\n if (params.message !== undefined) result.message = params.message;\n if (params.lightning !== undefined) result.lightning = params.lightning;\n if (params.lno !== undefined) result.lno = params.lno;\n\n return result;\n};\n\n/** Returns true if the input starts with the `bitcoin:` URI scheme. */\nexport const isBip21 = (uri: string): boolean =>\n typeof uri === \"string\" && BIP21_SCHEME.test(uri.trim());\n"],"names":[],"mappings":"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"}
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -1674,6 +1674,28 @@ class LightningAddress {
|
|
|
1674
1674
|
}
|
|
1675
1675
|
}
|
|
1676
1676
|
|
|
1677
|
+
/** Apply a previously-obtained credential to the outgoing request headers. */
|
|
1678
|
+
const applyCredentials = (headers, credentials) => {
|
|
1679
|
+
headers.set(credentials.header, credentials.value);
|
|
1680
|
+
};
|
|
1681
|
+
/** Attach payment metadata to a response and return it (typed). */
|
|
1682
|
+
const attachPayment = (response, payment) => {
|
|
1683
|
+
if (payment) {
|
|
1684
|
+
response.payment = payment;
|
|
1685
|
+
}
|
|
1686
|
+
return response;
|
|
1687
|
+
};
|
|
1688
|
+
/** Payment metadata describing a request authorized with a reused credential. */
|
|
1689
|
+
const reusedCredentialPayment = (credentials) => credentials ? { paid: false, amount: 0, credentials } : undefined;
|
|
1690
|
+
/** Satoshi amount of a BOLT11 invoice (0 when it cannot be decoded). */
|
|
1691
|
+
const getInvoiceAmount = (invoice) => {
|
|
1692
|
+
try {
|
|
1693
|
+
return new Invoice({ pr: invoice }).satoshi;
|
|
1694
|
+
}
|
|
1695
|
+
catch (_) {
|
|
1696
|
+
return 0;
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1677
1699
|
function createGuardedWallet(wallet, maxAmountSats) {
|
|
1678
1700
|
return {
|
|
1679
1701
|
payInvoice: async (args) => {
|
|
@@ -1725,6 +1747,9 @@ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1725
1747
|
const details = parseL402(l402Header);
|
|
1726
1748
|
const token = details.token || details.macaroon;
|
|
1727
1749
|
const invoice = details.invoice;
|
|
1750
|
+
// Preserve the scheme the server challenged with (L402 or LSAT) so the
|
|
1751
|
+
// retry's Authorization header matches what the server expects.
|
|
1752
|
+
const scheme = /^\s*LSAT\b/i.test(l402Header) ? "LSAT" : "L402";
|
|
1728
1753
|
if (!token) {
|
|
1729
1754
|
throw new Error("L402: missing token/macaroon in WWW-Authenticate header");
|
|
1730
1755
|
}
|
|
@@ -1732,8 +1757,16 @@ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1732
1757
|
throw new Error("L402: missing invoice in WWW-Authenticate header");
|
|
1733
1758
|
}
|
|
1734
1759
|
const invResp = await wallet.payInvoice({ invoice });
|
|
1735
|
-
|
|
1736
|
-
|
|
1760
|
+
const value = `${scheme} ${token}:${invResp.preimage}`;
|
|
1761
|
+
headers.set("Authorization", value);
|
|
1762
|
+
const response = await fetch(url, fetchArgs);
|
|
1763
|
+
return attachPayment(response, {
|
|
1764
|
+
paid: true,
|
|
1765
|
+
amount: getInvoiceAmount(invoice),
|
|
1766
|
+
feesPaid: invResp.fees_paid,
|
|
1767
|
+
preimage: invResp.preimage,
|
|
1768
|
+
credentials: { header: "Authorization", value },
|
|
1769
|
+
});
|
|
1737
1770
|
};
|
|
1738
1771
|
const fetchWithL402 = async (url, fetchArgs, options) => {
|
|
1739
1772
|
const wallet = options.wallet;
|
|
@@ -1747,6 +1780,15 @@ const fetchWithL402 = async (url, fetchArgs, options) => {
|
|
|
1747
1780
|
fetchArgs.mode = "cors";
|
|
1748
1781
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1749
1782
|
fetchArgs.headers = headers;
|
|
1783
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
1784
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
1785
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
1786
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
1787
|
+
if (options.credentials) {
|
|
1788
|
+
applyCredentials(headers, options.credentials);
|
|
1789
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
1790
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
1791
|
+
}
|
|
1750
1792
|
const initResp = await fetch(url, fetchArgs);
|
|
1751
1793
|
const header = initResp.headers.get("www-authenticate");
|
|
1752
1794
|
if (!header) {
|
|
@@ -1813,9 +1855,17 @@ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1813
1855
|
if (invoice.amountRaw != requirements.amount) {
|
|
1814
1856
|
throw new Error(`Invalid invoice amount: ${invoice.amountRaw}. expected ${requirements.amount}`);
|
|
1815
1857
|
}
|
|
1816
|
-
await wallet.payInvoice({ invoice: invoice.paymentRequest });
|
|
1817
|
-
|
|
1818
|
-
|
|
1858
|
+
const invResp = await wallet.payInvoice({ invoice: invoice.paymentRequest });
|
|
1859
|
+
const value = buildX402PaymentSignature(requirements.scheme, requirements.network, invoice.paymentRequest, requirements);
|
|
1860
|
+
headers.set("payment-signature", value);
|
|
1861
|
+
const response = await fetch(url, fetchArgs);
|
|
1862
|
+
return attachPayment(response, {
|
|
1863
|
+
paid: true,
|
|
1864
|
+
amount: invoice.satoshi,
|
|
1865
|
+
feesPaid: invResp.fees_paid,
|
|
1866
|
+
preimage: invResp.preimage,
|
|
1867
|
+
credentials: { header: "payment-signature", value },
|
|
1868
|
+
});
|
|
1819
1869
|
};
|
|
1820
1870
|
const fetchWithX402 = async (url, fetchArgs, options) => {
|
|
1821
1871
|
const wallet = options.wallet;
|
|
@@ -1826,6 +1876,15 @@ const fetchWithX402 = async (url, fetchArgs, options) => {
|
|
|
1826
1876
|
fetchArgs.mode = "cors";
|
|
1827
1877
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1828
1878
|
fetchArgs.headers = headers;
|
|
1879
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
1880
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
1881
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
1882
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
1883
|
+
if (options.credentials) {
|
|
1884
|
+
applyCredentials(headers, options.credentials);
|
|
1885
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
1886
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
1887
|
+
}
|
|
1829
1888
|
const initResp = await fetch(url, fetchArgs);
|
|
1830
1889
|
const header = initResp.headers.get("PAYMENT-REQUIRED");
|
|
1831
1890
|
if (!header) {
|
|
@@ -1972,8 +2031,16 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
|
|
|
1972
2031
|
const invResp = await wallet.payInvoice({ invoice });
|
|
1973
2032
|
// Per spec: Authorization: Payment <base64url-token> (single token, no wrapper)
|
|
1974
2033
|
const credential = buildMppCredential(challenge, invResp.preimage);
|
|
1975
|
-
|
|
1976
|
-
|
|
2034
|
+
const value = `Payment ${credential}`;
|
|
2035
|
+
headers.set("Authorization", value);
|
|
2036
|
+
const response = await fetch(url, fetchArgs);
|
|
2037
|
+
return attachPayment(response, {
|
|
2038
|
+
paid: true,
|
|
2039
|
+
amount: getInvoiceAmount(invoice),
|
|
2040
|
+
feesPaid: invResp.fees_paid,
|
|
2041
|
+
preimage: invResp.preimage,
|
|
2042
|
+
credentials: { header: "Authorization", value },
|
|
2043
|
+
});
|
|
1977
2044
|
};
|
|
1978
2045
|
/**
|
|
1979
2046
|
* Fetch a resource protected by the draft-lightning-charge-00 payment
|
|
@@ -1984,9 +2051,11 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
|
|
|
1984
2051
|
* the function pays the embedded BOLT11 invoice and retries with the
|
|
1985
2052
|
* resulting preimage as the credential.
|
|
1986
2053
|
*
|
|
1987
|
-
*
|
|
1988
|
-
*
|
|
1989
|
-
*
|
|
2054
|
+
* Pass a previous credential via `options.credentials` to reuse it (e.g. when
|
|
2055
|
+
* polling); the credential is applied and the function NEVER pays again, even
|
|
2056
|
+
* if the server still responds with a 402 (that response is returned as-is).
|
|
2057
|
+
* Note: lightning-charge typically uses consume-once challenge semantics, so a
|
|
2058
|
+
* reused credential is only accepted by servers that explicitly support it.
|
|
1990
2059
|
*/
|
|
1991
2060
|
const fetchWithMpp = async (url, fetchArgs, options) => {
|
|
1992
2061
|
const wallet = options.wallet;
|
|
@@ -2000,6 +2069,15 @@ const fetchWithMpp = async (url, fetchArgs, options) => {
|
|
|
2000
2069
|
fetchArgs.mode = "cors";
|
|
2001
2070
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
2002
2071
|
fetchArgs.headers = headers;
|
|
2072
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
2073
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
2074
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
2075
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
2076
|
+
if (options.credentials) {
|
|
2077
|
+
applyCredentials(headers, options.credentials);
|
|
2078
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
2079
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
2080
|
+
}
|
|
2003
2081
|
const initResp = await fetch(url, fetchArgs);
|
|
2004
2082
|
const wwwAuthHeader = initResp.headers.get("www-authenticate");
|
|
2005
2083
|
if (!wwwAuthHeader ||
|
|
@@ -2020,6 +2098,15 @@ const fetch402 = async (url, fetchArgs, options) => {
|
|
|
2020
2098
|
fetchArgs.mode = "cors";
|
|
2021
2099
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
2022
2100
|
fetchArgs.headers = headers;
|
|
2101
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
2102
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
2103
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
2104
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
2105
|
+
if (options.credentials) {
|
|
2106
|
+
applyCredentials(headers, options.credentials);
|
|
2107
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
2108
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
2109
|
+
}
|
|
2023
2110
|
const initResp = await fetch(url, fetchArgs);
|
|
2024
2111
|
// L402 / LSAT: dedicated scheme, dispatch directly.
|
|
2025
2112
|
const wwwAuthHeader = initResp.headers.get("www-authenticate");
|
|
@@ -2176,10 +2263,122 @@ const getFormattedFiatValue = async ({ satoshi, currency, locale, }) => {
|
|
|
2176
2263
|
});
|
|
2177
2264
|
};
|
|
2178
2265
|
|
|
2266
|
+
const BIP21_SCHEME = /^bitcoin:/i;
|
|
2267
|
+
// BIP21 grammar: amountparam = "amount=" *digit [ "." *digit ]
|
|
2268
|
+
// We require at least one digit on either side of the decimal point — this is
|
|
2269
|
+
// slightly stricter than the spec ABNF but matches every real-world example
|
|
2270
|
+
// (the spec shows "50", "50.00", "20.3"). Crucially this rejects scientific
|
|
2271
|
+
// notation ("1e-3"), hex ("0x10"), commas, signs, and leading "+" / "-".
|
|
2272
|
+
const BIP21_AMOUNT_RE = /^(\d+)(?:\.(\d+))?$/;
|
|
2273
|
+
const SATS_PER_BTC = 100000000n;
|
|
2274
|
+
const BTC_DECIMALS = 8;
|
|
2275
|
+
/**
|
|
2276
|
+
* Convert a BIP21-compliant decimal BTC string to an integer number of
|
|
2277
|
+
* satoshis using exact decimal arithmetic (no floats). Fractional digits
|
|
2278
|
+
* beyond 8 are rounded half-up to the nearest satoshi.
|
|
2279
|
+
*
|
|
2280
|
+
* Assumes the input has already been validated against BIP21_AMOUNT_RE.
|
|
2281
|
+
*/
|
|
2282
|
+
const btcStringToSats = (btc) => {
|
|
2283
|
+
const match = BIP21_AMOUNT_RE.exec(btc);
|
|
2284
|
+
if (!match) {
|
|
2285
|
+
// Should be unreachable — caller pre-validates.
|
|
2286
|
+
return Number.NaN;
|
|
2287
|
+
}
|
|
2288
|
+
const [, integerPart, rawFractional = ""] = match;
|
|
2289
|
+
let fractionalSats;
|
|
2290
|
+
if (rawFractional.length <= BTC_DECIMALS) {
|
|
2291
|
+
fractionalSats = BigInt(rawFractional.padEnd(BTC_DECIMALS, "0"));
|
|
2292
|
+
}
|
|
2293
|
+
else {
|
|
2294
|
+
// Round half-up at the satoshi boundary.
|
|
2295
|
+
const truncated = rawFractional.slice(0, BTC_DECIMALS);
|
|
2296
|
+
const roundDigit = rawFractional.charCodeAt(BTC_DECIMALS) - 48;
|
|
2297
|
+
fractionalSats = BigInt(truncated);
|
|
2298
|
+
if (roundDigit >= 5)
|
|
2299
|
+
fractionalSats += 1n;
|
|
2300
|
+
}
|
|
2301
|
+
const totalSats = BigInt(integerPart) * SATS_PER_BTC + fractionalSats;
|
|
2302
|
+
return Number(totalSats);
|
|
2303
|
+
};
|
|
2304
|
+
/**
|
|
2305
|
+
* Parse a BIP21 (`bitcoin:`) URI. Returns `null` if the input doesn't have the
|
|
2306
|
+
* `bitcoin:` scheme.
|
|
2307
|
+
*
|
|
2308
|
+
* The address is returned as-is (case preserved) — callers should validate it
|
|
2309
|
+
* separately if they need to ensure it's a well-formed bitcoin address.
|
|
2310
|
+
*
|
|
2311
|
+
* Per BIP21, parameters prefixed with `req-` are required: if the client
|
|
2312
|
+
* doesn't understand any of them, the payment MUST NOT be made. The unknown
|
|
2313
|
+
* required params are surfaced via `unknownRequiredParams` so callers can
|
|
2314
|
+
* decide how to fail.
|
|
2315
|
+
*
|
|
2316
|
+
* @example
|
|
2317
|
+
* parseBip21("bitcoin:bc1q...?amount=0.001&lightning=lnbc...")
|
|
2318
|
+
* // => { address: "bc1q...", amount: 0.001, amountSats: 100000, lightning: "lnbc...", ... }
|
|
2319
|
+
*/
|
|
2320
|
+
const parseBip21 = (uri) => {
|
|
2321
|
+
if (typeof uri !== "string") {
|
|
2322
|
+
return null;
|
|
2323
|
+
}
|
|
2324
|
+
const normalized = uri.trim();
|
|
2325
|
+
if (!BIP21_SCHEME.test(normalized)) {
|
|
2326
|
+
return null;
|
|
2327
|
+
}
|
|
2328
|
+
const withoutScheme = normalized.replace(BIP21_SCHEME, "");
|
|
2329
|
+
const queryStart = withoutScheme.indexOf("?");
|
|
2330
|
+
const address = queryStart === -1 ? withoutScheme : withoutScheme.slice(0, queryStart);
|
|
2331
|
+
const query = queryStart === -1 ? "" : withoutScheme.slice(queryStart + 1);
|
|
2332
|
+
const params = {};
|
|
2333
|
+
const unknownRequiredParams = [];
|
|
2334
|
+
if (query) {
|
|
2335
|
+
// URLSearchParams handles decoding and repeated params; for BIP21 last-write-wins
|
|
2336
|
+
// is fine since the spec doesn't allow repeats.
|
|
2337
|
+
const search = new URLSearchParams(query);
|
|
2338
|
+
search.forEach((value, key) => {
|
|
2339
|
+
params[key] = value;
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
const knownParams = new Set([
|
|
2343
|
+
"amount",
|
|
2344
|
+
"label",
|
|
2345
|
+
"message",
|
|
2346
|
+
"lightning",
|
|
2347
|
+
"lno",
|
|
2348
|
+
]);
|
|
2349
|
+
for (const key of Object.keys(params)) {
|
|
2350
|
+
if (key.startsWith("req-") && !knownParams.has(key.slice(4))) {
|
|
2351
|
+
unknownRequiredParams.push(key);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
const result = {
|
|
2355
|
+
address: address.trim(),
|
|
2356
|
+
params,
|
|
2357
|
+
unknownRequiredParams,
|
|
2358
|
+
};
|
|
2359
|
+
if (params.amount !== undefined && BIP21_AMOUNT_RE.test(params.amount)) {
|
|
2360
|
+
result.amount = Number(params.amount);
|
|
2361
|
+
result.amountSats = btcStringToSats(params.amount);
|
|
2362
|
+
}
|
|
2363
|
+
if (params.label !== undefined)
|
|
2364
|
+
result.label = params.label;
|
|
2365
|
+
if (params.message !== undefined)
|
|
2366
|
+
result.message = params.message;
|
|
2367
|
+
if (params.lightning !== undefined)
|
|
2368
|
+
result.lightning = params.lightning;
|
|
2369
|
+
if (params.lno !== undefined)
|
|
2370
|
+
result.lno = params.lno;
|
|
2371
|
+
return result;
|
|
2372
|
+
};
|
|
2373
|
+
/** Returns true if the input starts with the `bitcoin:` URI scheme. */
|
|
2374
|
+
const isBip21 = (uri) => typeof uri === "string" && BIP21_SCHEME.test(uri.trim());
|
|
2375
|
+
|
|
2179
2376
|
exports.DEFAULT_PROXY = DEFAULT_PROXY;
|
|
2180
2377
|
exports.Invoice = Invoice;
|
|
2181
2378
|
exports.LN_ADDRESS_REGEX = LN_ADDRESS_REGEX;
|
|
2182
2379
|
exports.LightningAddress = LightningAddress;
|
|
2380
|
+
exports.applyCredentials = applyCredentials;
|
|
2381
|
+
exports.attachPayment = attachPayment;
|
|
2183
2382
|
exports.createGuardedWallet = createGuardedWallet;
|
|
2184
2383
|
exports.decodeInvoice = decodeInvoice;
|
|
2185
2384
|
exports.fetch402 = fetch402;
|
|
@@ -2194,16 +2393,20 @@ exports.getFiatBtcRate = getFiatBtcRate;
|
|
|
2194
2393
|
exports.getFiatCurrencies = getFiatCurrencies;
|
|
2195
2394
|
exports.getFiatValue = getFiatValue;
|
|
2196
2395
|
exports.getFormattedFiatValue = getFormattedFiatValue;
|
|
2396
|
+
exports.getInvoiceAmount = getInvoiceAmount;
|
|
2197
2397
|
exports.getSatoshiValue = getSatoshiValue;
|
|
2398
|
+
exports.isBip21 = isBip21;
|
|
2198
2399
|
exports.isUrl = isUrl;
|
|
2199
2400
|
exports.isValidAmount = isValidAmount;
|
|
2200
2401
|
exports.issueL402Macaroon = issueL402Macaroon;
|
|
2201
2402
|
exports.makeL402AuthenticateHeader = makeL402AuthenticateHeader;
|
|
2403
|
+
exports.parseBip21 = parseBip21;
|
|
2202
2404
|
exports.parseKeysendResponse = parseKeysendResponse;
|
|
2203
2405
|
exports.parseL402 = parseL402;
|
|
2204
2406
|
exports.parseL402Authorization = parseL402Authorization;
|
|
2205
2407
|
exports.parseLnUrlPayResponse = parseLnUrlPayResponse;
|
|
2206
2408
|
exports.parseNostrResponse = parseNostrResponse;
|
|
2409
|
+
exports.reusedCredentialPayment = reusedCredentialPayment;
|
|
2207
2410
|
exports.sendBoostagram = sendBoostagram;
|
|
2208
2411
|
exports.serializeEvent = serializeEvent;
|
|
2209
2412
|
exports.validateEvent = validateEvent;
|