thunder-bridge 0.8.1 → 0.8.3
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 +37 -13
- package/dist/index.cjs +247 -360
- package/dist/index.d.cts +20 -323
- package/dist/index.d.ts +20 -323
- package/dist/index.js +243 -359
- package/dist/rail-Cyc5zTtA.d.cts +375 -0
- package/dist/rail-Cyc5zTtA.d.ts +375 -0
- package/dist/server.cjs +805 -0
- package/dist/server.d.cts +64 -0
- package/dist/server.d.ts +64 -0
- package/dist/server.js +767 -0
- package/package.json +13 -4
package/dist/index.js
CHANGED
|
@@ -34,10 +34,8 @@ var ProblemError = class extends Error {
|
|
|
34
34
|
this.detail = problem.detail ?? null;
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
|
-
var NAMESPACE = "urn:problem-type:thunder-bridge:";
|
|
38
|
-
var NAMESPACE_BEFORE_THE_RENAME = "urn:problem-type:thunder-bridge-direct:";
|
|
39
37
|
function isProblemType(problem, type) {
|
|
40
|
-
return problem.type === type
|
|
38
|
+
return problem.type === type;
|
|
41
39
|
}
|
|
42
40
|
var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
|
|
43
41
|
var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
|
|
@@ -348,10 +346,13 @@ function sameOrigin(one, other) {
|
|
|
348
346
|
return false;
|
|
349
347
|
}
|
|
350
348
|
}
|
|
349
|
+
function publicAddress(literal) {
|
|
350
|
+
return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
|
|
351
|
+
}
|
|
351
352
|
function hostIsPublic(host) {
|
|
352
|
-
if (host.startsWith("[")) return
|
|
353
|
+
if (host.startsWith("[")) return publicAddress(host.slice(1, -1));
|
|
353
354
|
const name = host.replace(/\.$/, "");
|
|
354
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return
|
|
355
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return publicAddress(name);
|
|
355
356
|
const labels = name.split(".");
|
|
356
357
|
if (labels.length < 2 || labels.some((label) => label === "")) return false;
|
|
357
358
|
return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
|
|
@@ -616,6 +617,16 @@ var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
|
|
|
616
617
|
var RECONNECT_DELAY_MS = 3e3;
|
|
617
618
|
var RECONNECT_CAP_MS = 3e4;
|
|
618
619
|
var UNANSWERED_ATTEMPTS = 5;
|
|
620
|
+
var TRIGGER_MIN_CHARS = 16;
|
|
621
|
+
var STRANGER_PROBE = "is-this-gateway-yours";
|
|
622
|
+
function unguessable(trigger) {
|
|
623
|
+
if (trigger.length < TRIGGER_MIN_CHARS) {
|
|
624
|
+
throw new Error(
|
|
625
|
+
`a trigger secret is the only thing guarding its stream, and nothing rate limits a guess at it, so it has to be at least ${TRIGGER_MIN_CHARS} characters nobody can predict`
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
return trigger;
|
|
629
|
+
}
|
|
619
630
|
function backoffMs(firstDelay, attempt) {
|
|
620
631
|
const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
|
|
621
632
|
return Math.round(grown * (0.5 + Math.random() / 2));
|
|
@@ -624,6 +635,7 @@ var ThunderBridge = class {
|
|
|
624
635
|
baseUrl;
|
|
625
636
|
verify;
|
|
626
637
|
token;
|
|
638
|
+
strangers = null;
|
|
627
639
|
constructor(baseUrl, options) {
|
|
628
640
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
629
641
|
this.verify = options?.verify ?? true;
|
|
@@ -637,6 +649,21 @@ var ThunderBridge = class {
|
|
|
637
649
|
get isPrivate() {
|
|
638
650
|
return this.token !== null;
|
|
639
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Whether the gateway turns away a caller carrying no token, asked by making
|
|
654
|
+
* one unauthenticated read it would have to refuse. `isPrivate` answers only
|
|
655
|
+
* whether you configured a token, which is your side of the arrangement and
|
|
656
|
+
* says nothing about the gateway's, so a made-up token against a public
|
|
657
|
+
* instance reads as private and is not. Asked once and remembered, because an
|
|
658
|
+
* instance does not change its mind. Anything other than a refusal counts as
|
|
659
|
+
* open, so an unreachable gateway fails closed
|
|
660
|
+
*/
|
|
661
|
+
async refusesStrangers() {
|
|
662
|
+
this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
|
|
663
|
+
headers: { accept: "application/json" }
|
|
664
|
+
}).then((answer) => answer.status === 401).catch(() => false);
|
|
665
|
+
return await this.strangers;
|
|
666
|
+
}
|
|
640
667
|
/**
|
|
641
668
|
* Ask the gateway for an invoice payable to the first address on your list
|
|
642
669
|
* that can issue a provable one, throws `NoWalletAvailableError` when none can
|
|
@@ -648,7 +675,10 @@ var ThunderBridge = class {
|
|
|
648
675
|
const response = await fetch(`${this.baseUrl}/incoming-payments`, {
|
|
649
676
|
method: "POST",
|
|
650
677
|
headers,
|
|
651
|
-
body: createRequestBody(
|
|
678
|
+
body: createRequestBody(
|
|
679
|
+
params,
|
|
680
|
+
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
|
|
681
|
+
)
|
|
652
682
|
});
|
|
653
683
|
if (!response.ok) throw await problemFrom(response);
|
|
654
684
|
const payment = await paymentFrom(response);
|
|
@@ -890,7 +920,10 @@ var ThunderBridge = class {
|
|
|
890
920
|
const response = await fetch(`${this.baseUrl}/watched-payments`, {
|
|
891
921
|
method: "POST",
|
|
892
922
|
headers: this.sending(),
|
|
893
|
-
body: watchRequestBody(
|
|
923
|
+
body: watchRequestBody(
|
|
924
|
+
params,
|
|
925
|
+
params.trigger ? sha256Hex(unguessable(params.trigger)) : null
|
|
926
|
+
)
|
|
894
927
|
});
|
|
895
928
|
if (!response.ok) throw await problemFrom(response);
|
|
896
929
|
const watched = triggerEventFromWire(await response.json().catch(() => null));
|
|
@@ -909,7 +942,7 @@ var ThunderBridge = class {
|
|
|
909
942
|
*/
|
|
910
943
|
followTrigger(secret, options) {
|
|
911
944
|
const base = this.baseUrl.replace(/^http/, "ws");
|
|
912
|
-
const direct = `${base}/ws/triggers/${encodeURIComponent(secret)}`;
|
|
945
|
+
const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}`;
|
|
913
946
|
const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
|
|
914
947
|
let socket = null;
|
|
915
948
|
let retry;
|
|
@@ -1116,290 +1149,6 @@ function asBuffer(bytes) {
|
|
|
1116
1149
|
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1117
1150
|
}
|
|
1118
1151
|
|
|
1119
|
-
// ../core/refusal.ts
|
|
1120
|
-
var WalletRefused = class extends Error {
|
|
1121
|
-
reason;
|
|
1122
|
-
constructor(reason, message) {
|
|
1123
|
-
super(message);
|
|
1124
|
-
this.reason = reason;
|
|
1125
|
-
}
|
|
1126
|
-
};
|
|
1127
|
-
var NoWalletAvailable = class extends Error {
|
|
1128
|
-
wallets;
|
|
1129
|
-
constructor(wallets) {
|
|
1130
|
-
super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
|
|
1131
|
-
this.wallets = wallets;
|
|
1132
|
-
}
|
|
1133
|
-
};
|
|
1134
|
-
|
|
1135
|
-
// ../core/lnurl.ts
|
|
1136
|
-
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1137
|
-
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1138
|
-
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1139
|
-
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1140
|
-
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1141
|
-
var LNURL_HRP = "lnurl";
|
|
1142
|
-
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
1143
|
-
async function resolve(addresses, amountMsat) {
|
|
1144
|
-
const served = await firstThatServes(
|
|
1145
|
-
addresses,
|
|
1146
|
-
(address, deadline) => resolveAddress(address, amountMsat, deadline)
|
|
1147
|
-
);
|
|
1148
|
-
return served.won;
|
|
1149
|
-
}
|
|
1150
|
-
async function firstThatServes(addresses, attempt) {
|
|
1151
|
-
const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
1152
|
-
const refusals3 = [];
|
|
1153
|
-
for (const address of addresses) {
|
|
1154
|
-
if (deadline.aborted) break;
|
|
1155
|
-
try {
|
|
1156
|
-
return { won: await attempt(address, deadline), refusals: refusals3 };
|
|
1157
|
-
} catch (refusal) {
|
|
1158
|
-
if (!(refusal instanceof WalletRefused)) throw refusal;
|
|
1159
|
-
console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
|
|
1160
|
-
refusals3.push({ address, reason: refusal.reason });
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
throw new NoWalletAvailable(refusals3);
|
|
1164
|
-
}
|
|
1165
|
-
async function probe(address, amountMsat, deadline) {
|
|
1166
|
-
const [user, domain] = splitAddress(address);
|
|
1167
|
-
if (cannotReleaseAPreimage(domain)) {
|
|
1168
|
-
throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
|
|
1169
|
-
}
|
|
1170
|
-
const pay = await answered(
|
|
1171
|
-
`https://${domain}/.well-known/lnurlp/${user}`,
|
|
1172
|
-
deadline
|
|
1173
|
-
);
|
|
1174
|
-
if (pay.tag !== "payRequest" || !pay.callback) {
|
|
1175
|
-
throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
|
|
1176
|
-
}
|
|
1177
|
-
if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
|
|
1178
|
-
throw new WalletRefused(
|
|
1179
|
-
"amount-not-accepted",
|
|
1180
|
-
`${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
|
|
1181
|
-
);
|
|
1182
|
-
}
|
|
1183
|
-
return { address, pay };
|
|
1184
|
-
}
|
|
1185
|
-
async function resolveAddress(address, amountMsat, deadline) {
|
|
1186
|
-
const { pay } = await probe(address, amountMsat, deadline);
|
|
1187
|
-
const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
|
|
1188
|
-
if (!invoice.pr) throw new WalletRefused("unreachable", `${address} returned no invoice`);
|
|
1189
|
-
if (!invoice.verify || !publicHttps(invoice.verify)) {
|
|
1190
|
-
throw new WalletRefused(
|
|
1191
|
-
"cannot-prove-delivery",
|
|
1192
|
-
`${address} publishes no usable LUD-21 verify URL`
|
|
1193
|
-
);
|
|
1194
|
-
}
|
|
1195
|
-
const issued = decodeIssued(address, invoice.pr);
|
|
1196
|
-
if (issued.amountMsat !== amountMsat) {
|
|
1197
|
-
throw new WalletRefused(
|
|
1198
|
-
"invoice-refused",
|
|
1199
|
-
`${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
|
|
1203
|
-
throw new WalletRefused(
|
|
1204
|
-
"invoice-refused",
|
|
1205
|
-
`${address} does not bind its invoice to the metadata it serves`
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
return {
|
|
1209
|
-
address,
|
|
1210
|
-
bolt11: invoice.pr,
|
|
1211
|
-
verifyUrl: invoice.verify,
|
|
1212
|
-
paymentHash: issued.paymentHash,
|
|
1213
|
-
expiresAt: issued.expiresAt
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
async function answered(url, deadline) {
|
|
1217
|
-
try {
|
|
1218
|
-
return await fetchJson(url, deadline);
|
|
1219
|
-
} catch (cause) {
|
|
1220
|
-
throw new WalletRefused("unreachable", String(cause));
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
function decodeIssued(address, bolt11) {
|
|
1224
|
-
const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
|
|
1225
|
-
if (paymentHash === null || expiresAt === null) {
|
|
1226
|
-
throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
|
|
1227
|
-
}
|
|
1228
|
-
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
1229
|
-
}
|
|
1230
|
-
function cannotReleaseAPreimage(host) {
|
|
1231
|
-
const lowered = host.toLowerCase();
|
|
1232
|
-
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
1233
|
-
(known) => lowered === known || lowered.endsWith(`.${known}`)
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
function toLnurl(endpoint) {
|
|
1237
|
-
const scheme = parsedScheme(endpoint);
|
|
1238
|
-
if (scheme !== "https:" && scheme !== "http:") {
|
|
1239
|
-
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1240
|
-
}
|
|
1241
|
-
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1242
|
-
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1243
|
-
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1244
|
-
}
|
|
1245
|
-
function parsedScheme(endpoint) {
|
|
1246
|
-
try {
|
|
1247
|
-
return new URL(endpoint).protocol;
|
|
1248
|
-
} catch {
|
|
1249
|
-
return null;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
function toWords(bytes) {
|
|
1253
|
-
const words = [];
|
|
1254
|
-
let accumulator = 0;
|
|
1255
|
-
let bits = 0;
|
|
1256
|
-
for (const byte of bytes) {
|
|
1257
|
-
accumulator = accumulator << 8 | byte;
|
|
1258
|
-
bits += 8;
|
|
1259
|
-
while (bits >= 5) {
|
|
1260
|
-
bits -= 5;
|
|
1261
|
-
words.push(accumulator >> bits & 31);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1265
|
-
return words;
|
|
1266
|
-
}
|
|
1267
|
-
function checksumWords(hrp, words) {
|
|
1268
|
-
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1269
|
-
const values = [
|
|
1270
|
-
...expanded.map((code) => code >> 5),
|
|
1271
|
-
0,
|
|
1272
|
-
...expanded.map((code) => code & 31),
|
|
1273
|
-
...words,
|
|
1274
|
-
...CHECKSUM_SLOTS.map(() => 0)
|
|
1275
|
-
];
|
|
1276
|
-
const polymod = bech32Polymod(values) ^ 1;
|
|
1277
|
-
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1278
|
-
}
|
|
1279
|
-
function bech32Polymod(values) {
|
|
1280
|
-
let checksum = 1;
|
|
1281
|
-
for (const value of values) {
|
|
1282
|
-
const top = checksum >> 25;
|
|
1283
|
-
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1284
|
-
for (let bit = 0; bit < 5; bit++) {
|
|
1285
|
-
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
return checksum;
|
|
1289
|
-
}
|
|
1290
|
-
function withAmount(callback, amountMsat) {
|
|
1291
|
-
const separator = callback.includes("?") ? "&" : "?";
|
|
1292
|
-
return `${callback}${separator}amount=${amountMsat}`;
|
|
1293
|
-
}
|
|
1294
|
-
function splitAddress(address) {
|
|
1295
|
-
const [user, domain, ...rest] = address.split("@");
|
|
1296
|
-
if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
|
|
1297
|
-
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
1298
|
-
}
|
|
1299
|
-
return [user, domain];
|
|
1300
|
-
}
|
|
1301
|
-
async function fetchJson(url, deadline) {
|
|
1302
|
-
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
1303
|
-
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
|
|
1304
|
-
const response = await fetch(url, {
|
|
1305
|
-
signal: deadline ? AbortSignal.any([deadline, capped]) : capped,
|
|
1306
|
-
headers: { accept: "application/json" }
|
|
1307
|
-
});
|
|
1308
|
-
if (!response.ok) throw new Error(`${url} answered ${response.status}`);
|
|
1309
|
-
return await response.json();
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// src/trigger.ts
|
|
1313
|
-
var NONCE_BYTES = 16;
|
|
1314
|
-
function lnurlPayEndpoint(config) {
|
|
1315
|
-
return async (request) => {
|
|
1316
|
-
const url = new URL(request.url);
|
|
1317
|
-
const asked = url.searchParams.get("to");
|
|
1318
|
-
try {
|
|
1319
|
-
return asked === null ? await offer(config, url) : await mint(config, url, asked);
|
|
1320
|
-
} catch (failure) {
|
|
1321
|
-
return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
|
|
1322
|
-
}
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
async function offer(config, url) {
|
|
1326
|
-
const amountMsat = await config.amountMsat();
|
|
1327
|
-
const quote = await config.gateway.createQuote({
|
|
1328
|
-
lnAddresses: config.lnAddresses,
|
|
1329
|
-
amountMsat
|
|
1330
|
-
});
|
|
1331
|
-
const nonce = randomNonce();
|
|
1332
|
-
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
1333
|
-
callback.searchParams.set("to", quote.lnAddress);
|
|
1334
|
-
callback.searchParams.set("msat", String(amountMsat));
|
|
1335
|
-
callback.searchParams.set("n", nonce);
|
|
1336
|
-
callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
|
|
1337
|
-
return Response.json({
|
|
1338
|
-
tag: "payRequest",
|
|
1339
|
-
callback: callback.toString(),
|
|
1340
|
-
metadata: quote.metadata,
|
|
1341
|
-
minSendable: amountMsat,
|
|
1342
|
-
maxSendable: amountMsat
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
|
-
async function mint(config, url, address) {
|
|
1346
|
-
const amountMsat = Number(url.searchParams.get("msat"));
|
|
1347
|
-
const nonce = url.searchParams.get("n") ?? "";
|
|
1348
|
-
const signature = url.searchParams.get("sig") ?? "";
|
|
1349
|
-
const expected = await sign(config.secret, address, amountMsat, nonce);
|
|
1350
|
-
if (!equalInConstantTime(signature, expected)) return refuse("this callback was not signed here");
|
|
1351
|
-
const wanted = url.searchParams.get("amount");
|
|
1352
|
-
if (wanted !== null && Number(wanted) !== amountMsat) {
|
|
1353
|
-
return refuse(`this trigger costs exactly ${amountMsat} msat`);
|
|
1354
|
-
}
|
|
1355
|
-
const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
|
|
1356
|
-
return Response.json({
|
|
1357
|
-
status: "OK",
|
|
1358
|
-
pr: minted.bolt11,
|
|
1359
|
-
routes: [],
|
|
1360
|
-
verify: minted.verifyUrl
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
|
-
async function mintThroughGateway(config, address, amountMsat, nonce) {
|
|
1364
|
-
const payment = await config.gateway.createPayment(
|
|
1365
|
-
{ lnAddresses: [address], amountMsat },
|
|
1366
|
-
{ idempotencyKey: nonce, trigger: config.watchSecret }
|
|
1367
|
-
);
|
|
1368
|
-
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
1369
|
-
}
|
|
1370
|
-
async function mintBlind(config, address, amountMsat) {
|
|
1371
|
-
const resolved = await resolve([address], amountMsat);
|
|
1372
|
-
const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
|
|
1373
|
-
const locked = config.sealed;
|
|
1374
|
-
try {
|
|
1375
|
-
await config.gateway.watchPayment({
|
|
1376
|
-
paymentHash: resolved.paymentHash,
|
|
1377
|
-
verifyUrl: resolved.verifyUrl,
|
|
1378
|
-
expiresAt: resolved.expiresAt,
|
|
1379
|
-
trigger: config.watchSecret,
|
|
1380
|
-
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
1381
|
-
});
|
|
1382
|
-
} catch (refused) {
|
|
1383
|
-
if (!alreadyWatched(refused)) throw refused;
|
|
1384
|
-
}
|
|
1385
|
-
return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
|
|
1386
|
-
}
|
|
1387
|
-
function alreadyWatched(refused) {
|
|
1388
|
-
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
1389
|
-
}
|
|
1390
|
-
function sign(secret, address, amountMsat, nonce) {
|
|
1391
|
-
return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
|
|
1392
|
-
}
|
|
1393
|
-
function randomNonce() {
|
|
1394
|
-
const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
1395
|
-
let hex = "";
|
|
1396
|
-
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
1397
|
-
return hex;
|
|
1398
|
-
}
|
|
1399
|
-
function refuse(reason) {
|
|
1400
|
-
return Response.json({ status: "ERROR", reason });
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
1152
|
// src/currency.ts
|
|
1404
1153
|
var ISO_4217_MINOR_UNITS = {
|
|
1405
1154
|
BHD: 3,
|
|
@@ -1450,6 +1199,7 @@ var FORBIDDEN_IN_SPD = "*";
|
|
|
1450
1199
|
async function bankTransfer(params) {
|
|
1451
1200
|
const currency = params.currency ?? DEFAULT_CURRENCY;
|
|
1452
1201
|
refuseUnusable(params, currency);
|
|
1202
|
+
await refuseAnOpenGateway(params);
|
|
1453
1203
|
const spd = shortPaymentDescriptor(params, currency);
|
|
1454
1204
|
const subject = subjectOf(params.reference, params.amountMinor, currency);
|
|
1455
1205
|
const polling = new URL(params.verifyUrl);
|
|
@@ -1516,12 +1266,14 @@ function shortPaymentDescriptor(params, currency) {
|
|
|
1516
1266
|
function major(amountMinor, currency) {
|
|
1517
1267
|
return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
|
|
1518
1268
|
}
|
|
1269
|
+
async function refuseAnOpenGateway(params) {
|
|
1270
|
+
if (params.allowPublicGateway === true) return;
|
|
1271
|
+
if (await params.gateway.refusesStrangers()) return;
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
"this gateway serves callers with no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Point at one that answers 401 to a stranger, or say allowPublicGateway"
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1519
1276
|
function refuseUnusable(params, currency) {
|
|
1520
|
-
if (!params.gateway.isPrivate && params.allowPublicGateway !== true) {
|
|
1521
|
-
throw new Error(
|
|
1522
|
-
"this gateway has no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Give it a token or say allowPublicGateway"
|
|
1523
|
-
);
|
|
1524
|
-
}
|
|
1525
1277
|
if (!IBAN.test(params.iban)) throw new Error(`${params.iban} is not an IBAN`);
|
|
1526
1278
|
if (!Number.isInteger(params.amountMinor) || params.amountMinor <= 0) {
|
|
1527
1279
|
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
@@ -1560,6 +1312,7 @@ var WHERE_A_PAYER_WRITES = [
|
|
|
1560
1312
|
USER_IDENTIFICATION,
|
|
1561
1313
|
PAYER_REFERENCE
|
|
1562
1314
|
];
|
|
1315
|
+
var UTC_OFFSET = /^[+-]\d{4}$/;
|
|
1563
1316
|
function fioStatement(config) {
|
|
1564
1317
|
const named = typeof config.token === "string" ? [config.token] : config.token;
|
|
1565
1318
|
if (named.length === 0) throw new Error("fioStatement needs at least one token to read with");
|
|
@@ -1612,9 +1365,21 @@ function asCredit(transaction) {
|
|
|
1612
1365
|
amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
|
|
1613
1366
|
currency: textIn(transaction[CURRENCY]),
|
|
1614
1367
|
reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
|
|
1615
|
-
bookedAt:
|
|
1368
|
+
bookedAt: bookedAtIn(transaction[BOOKED_AT])
|
|
1616
1369
|
};
|
|
1617
1370
|
}
|
|
1371
|
+
function bookedAtIn(cell) {
|
|
1372
|
+
const value = cell?.value;
|
|
1373
|
+
if (typeof value === "number") return Math.floor(value / MILLIS);
|
|
1374
|
+
const at = Date.parse(asIso8601(textIn(cell)));
|
|
1375
|
+
return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
|
|
1376
|
+
}
|
|
1377
|
+
function asIso8601(booked) {
|
|
1378
|
+
const day = booked.slice(0, DATE_CHARS);
|
|
1379
|
+
const zone = booked.slice(DATE_CHARS);
|
|
1380
|
+
if (!UTC_OFFSET.test(zone)) return `${day}T00:00:00Z`;
|
|
1381
|
+
return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
1382
|
+
}
|
|
1618
1383
|
function numberIn(cell) {
|
|
1619
1384
|
const value = Number(cell?.value ?? Number.NaN);
|
|
1620
1385
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -1627,6 +1392,180 @@ function asDate(unix) {
|
|
|
1627
1392
|
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
1628
1393
|
}
|
|
1629
1394
|
|
|
1395
|
+
// ../core/lnurl.ts
|
|
1396
|
+
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1397
|
+
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1398
|
+
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1399
|
+
var LNURL_HRP = "lnurl";
|
|
1400
|
+
function toLnurl(endpoint) {
|
|
1401
|
+
const scheme = parsedScheme(endpoint);
|
|
1402
|
+
if (scheme !== "https:" && scheme !== "http:") {
|
|
1403
|
+
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1404
|
+
}
|
|
1405
|
+
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1406
|
+
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1407
|
+
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1408
|
+
}
|
|
1409
|
+
function parsedScheme(endpoint) {
|
|
1410
|
+
try {
|
|
1411
|
+
return new URL(endpoint).protocol;
|
|
1412
|
+
} catch {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
function toWords(bytes) {
|
|
1417
|
+
const words = [];
|
|
1418
|
+
let accumulator = 0;
|
|
1419
|
+
let bits = 0;
|
|
1420
|
+
for (const byte of bytes) {
|
|
1421
|
+
accumulator = accumulator << 8 | byte;
|
|
1422
|
+
bits += 8;
|
|
1423
|
+
while (bits >= 5) {
|
|
1424
|
+
bits -= 5;
|
|
1425
|
+
words.push(accumulator >> bits & 31);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1429
|
+
return words;
|
|
1430
|
+
}
|
|
1431
|
+
function checksumWords(hrp, words) {
|
|
1432
|
+
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1433
|
+
const values = [
|
|
1434
|
+
...expanded.map((code) => code >> 5),
|
|
1435
|
+
0,
|
|
1436
|
+
...expanded.map((code) => code & 31),
|
|
1437
|
+
...words,
|
|
1438
|
+
...CHECKSUM_SLOTS.map(() => 0)
|
|
1439
|
+
];
|
|
1440
|
+
const polymod = bech32Polymod(values) ^ 1;
|
|
1441
|
+
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1442
|
+
}
|
|
1443
|
+
function bech32Polymod(values) {
|
|
1444
|
+
let checksum = 1;
|
|
1445
|
+
for (const value of values) {
|
|
1446
|
+
const top = checksum >> 25;
|
|
1447
|
+
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1448
|
+
for (let bit = 0; bit < 5; bit++) {
|
|
1449
|
+
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return checksum;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/qr.ts
|
|
1456
|
+
import { encode } from "uqr";
|
|
1457
|
+
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
1458
|
+
function encodeForQr(destination) {
|
|
1459
|
+
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1460
|
+
return `LIGHTNING:${scanned}`;
|
|
1461
|
+
}
|
|
1462
|
+
function invoiceToSvg(destination, options) {
|
|
1463
|
+
return svgOf(encodeForQr(destination), options);
|
|
1464
|
+
}
|
|
1465
|
+
function invoiceToDataUrl(destination, options) {
|
|
1466
|
+
return asDataUrl(invoiceToSvg(destination, options));
|
|
1467
|
+
}
|
|
1468
|
+
function lnurlToSvg(endpoint, options) {
|
|
1469
|
+
return svgOf(toLnurl(endpoint), options);
|
|
1470
|
+
}
|
|
1471
|
+
function lnurlToDataUrl(endpoint, options) {
|
|
1472
|
+
return asDataUrl(lnurlToSvg(endpoint, options));
|
|
1473
|
+
}
|
|
1474
|
+
function spdToSvg(spd, options) {
|
|
1475
|
+
return svgOf(spd, options);
|
|
1476
|
+
}
|
|
1477
|
+
function spdToDataUrl(spd, options) {
|
|
1478
|
+
return asDataUrl(spdToSvg(spd, options));
|
|
1479
|
+
}
|
|
1480
|
+
function qrToSvg(payload, options) {
|
|
1481
|
+
return svgOf(payload, options);
|
|
1482
|
+
}
|
|
1483
|
+
function qrToDataUrl(payload, options) {
|
|
1484
|
+
return asDataUrl(qrToSvg(payload, options));
|
|
1485
|
+
}
|
|
1486
|
+
function svgOf(scanned, options) {
|
|
1487
|
+
const size = options?.size ?? 256;
|
|
1488
|
+
const color = options?.color ?? "#000";
|
|
1489
|
+
if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
|
|
1490
|
+
return renderSvg(scanned, size, color);
|
|
1491
|
+
}
|
|
1492
|
+
function asDataUrl(svg) {
|
|
1493
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1494
|
+
}
|
|
1495
|
+
function isLnAddress(destination) {
|
|
1496
|
+
const at = destination.indexOf("@");
|
|
1497
|
+
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1498
|
+
}
|
|
1499
|
+
function renderSvg(content, size, color) {
|
|
1500
|
+
const { data } = encode(content);
|
|
1501
|
+
const modules = data.length;
|
|
1502
|
+
const cellSize = size / modules;
|
|
1503
|
+
let path = "";
|
|
1504
|
+
for (let y = 0; y < modules; y++) {
|
|
1505
|
+
for (let x = 0; x < modules; x++) {
|
|
1506
|
+
if (data[y][x]) {
|
|
1507
|
+
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return [
|
|
1512
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
1513
|
+
`<rect width="100%" height="100%" fill="white"/>`,
|
|
1514
|
+
`<path d="${path}" fill="${color}"/>`,
|
|
1515
|
+
`</svg>`
|
|
1516
|
+
].join("");
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// src/rail.ts
|
|
1520
|
+
var BANK = "bank";
|
|
1521
|
+
var LIGHTNING = "lightning";
|
|
1522
|
+
function bankRail(config) {
|
|
1523
|
+
return async (order) => {
|
|
1524
|
+
const expiresAt = config.expiresAt(order);
|
|
1525
|
+
const transfer = await bankTransfer({
|
|
1526
|
+
gateway: config.gateway,
|
|
1527
|
+
secret: config.secret,
|
|
1528
|
+
iban: config.iban,
|
|
1529
|
+
verifyUrl: config.verifyUrl,
|
|
1530
|
+
reference: order.reference,
|
|
1531
|
+
amountMinor: order.amountMinor,
|
|
1532
|
+
currency: order.currency,
|
|
1533
|
+
expiresAt,
|
|
1534
|
+
trigger: config.trigger,
|
|
1535
|
+
sealed: await config.sealed?.(order),
|
|
1536
|
+
variableSymbol: config.variableSymbol?.(order),
|
|
1537
|
+
allowPublicGateway: config.allowPublicGateway
|
|
1538
|
+
});
|
|
1539
|
+
return {
|
|
1540
|
+
id: transfer.id,
|
|
1541
|
+
rail: config.name ?? BANK,
|
|
1542
|
+
scan: transfer.spd,
|
|
1543
|
+
qr: transfer.spd,
|
|
1544
|
+
expiresAt
|
|
1545
|
+
};
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function lightningRail(config) {
|
|
1549
|
+
return async (order) => {
|
|
1550
|
+
const payment = await config.gateway.createPayment(
|
|
1551
|
+
{
|
|
1552
|
+
lnAddresses: config.lnAddresses,
|
|
1553
|
+
amountMsat: await config.amountMsat(order),
|
|
1554
|
+
webhookUrl: config.webhookUrl,
|
|
1555
|
+
webhookSecret: config.webhookSecret
|
|
1556
|
+
},
|
|
1557
|
+
{ idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
|
|
1558
|
+
);
|
|
1559
|
+
return {
|
|
1560
|
+
id: payment.id,
|
|
1561
|
+
rail: config.name ?? LIGHTNING,
|
|
1562
|
+
scan: payment.bolt11,
|
|
1563
|
+
qr: encodeForQr(payment.bolt11),
|
|
1564
|
+
expiresAt: payment.expiresAt
|
|
1565
|
+
};
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1630
1569
|
// src/price.ts
|
|
1631
1570
|
var MSAT_PER_BTC2 = 1e11;
|
|
1632
1571
|
var BASIS_POINTS = 1e4;
|
|
@@ -1749,64 +1688,6 @@ function refusals2(answers) {
|
|
|
1749
1688
|
).join("; ");
|
|
1750
1689
|
}
|
|
1751
1690
|
|
|
1752
|
-
// src/qr.ts
|
|
1753
|
-
import { encode } from "uqr";
|
|
1754
|
-
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
1755
|
-
function encodeForQr(destination) {
|
|
1756
|
-
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1757
|
-
return `LIGHTNING:${scanned}`;
|
|
1758
|
-
}
|
|
1759
|
-
function invoiceToSvg(destination, options) {
|
|
1760
|
-
return svgOf(encodeForQr(destination), options);
|
|
1761
|
-
}
|
|
1762
|
-
function invoiceToDataUrl(destination, options) {
|
|
1763
|
-
return asDataUrl(invoiceToSvg(destination, options));
|
|
1764
|
-
}
|
|
1765
|
-
function lnurlToSvg(endpoint, options) {
|
|
1766
|
-
return svgOf(toLnurl(endpoint), options);
|
|
1767
|
-
}
|
|
1768
|
-
function lnurlToDataUrl(endpoint, options) {
|
|
1769
|
-
return asDataUrl(lnurlToSvg(endpoint, options));
|
|
1770
|
-
}
|
|
1771
|
-
function spdToSvg(spd, options) {
|
|
1772
|
-
return svgOf(spd, options);
|
|
1773
|
-
}
|
|
1774
|
-
function spdToDataUrl(spd, options) {
|
|
1775
|
-
return asDataUrl(spdToSvg(spd, options));
|
|
1776
|
-
}
|
|
1777
|
-
function svgOf(scanned, options) {
|
|
1778
|
-
const size = options?.size ?? 256;
|
|
1779
|
-
const color = options?.color ?? "#000";
|
|
1780
|
-
if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
|
|
1781
|
-
return renderSvg(scanned, size, color);
|
|
1782
|
-
}
|
|
1783
|
-
function asDataUrl(svg) {
|
|
1784
|
-
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1785
|
-
}
|
|
1786
|
-
function isLnAddress(destination) {
|
|
1787
|
-
const at = destination.indexOf("@");
|
|
1788
|
-
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1789
|
-
}
|
|
1790
|
-
function renderSvg(content, size, color) {
|
|
1791
|
-
const { data } = encode(content);
|
|
1792
|
-
const modules = data.length;
|
|
1793
|
-
const cellSize = size / modules;
|
|
1794
|
-
let path = "";
|
|
1795
|
-
for (let y = 0; y < modules; y++) {
|
|
1796
|
-
for (let x = 0; x < modules; x++) {
|
|
1797
|
-
if (data[y][x]) {
|
|
1798
|
-
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
return [
|
|
1803
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
1804
|
-
`<rect width="100%" height="100%" fill="white"/>`,
|
|
1805
|
-
`<path d="${path}" fill="${color}"/>`,
|
|
1806
|
-
`</svg>`
|
|
1807
|
-
].join("");
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
1691
|
// src/webhook.ts
|
|
1811
1692
|
var SIGNATURE_HEADER = "x-signature";
|
|
1812
1693
|
var TIMESTAMP_HEADER = "x-timestamp";
|
|
@@ -1815,7 +1696,7 @@ var DEFAULT_TOLERANCE_SECS = 300;
|
|
|
1815
1696
|
async function verifyWebhookSignature(body, signature, secret, timestamp, options = {}) {
|
|
1816
1697
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1817
1698
|
const received = signature.startsWith(SIGNATURE_PREFIX) ? signature.slice(SIGNATURE_PREFIX.length) : signature;
|
|
1818
|
-
return equalInConstantTime(await
|
|
1699
|
+
return equalInConstantTime(await sign(secret, timestamp, body), received.toLowerCase());
|
|
1819
1700
|
}
|
|
1820
1701
|
async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
1821
1702
|
if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
|
|
@@ -1837,7 +1718,7 @@ function recent(timestamp, toleranceSecs) {
|
|
|
1837
1718
|
if (!Number.isFinite(sent)) return false;
|
|
1838
1719
|
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1839
1720
|
}
|
|
1840
|
-
async function
|
|
1721
|
+
async function sign(secret, timestamp, body) {
|
|
1841
1722
|
return hmacHex(secret, signed(timestamp, body));
|
|
1842
1723
|
}
|
|
1843
1724
|
function signed(timestamp, body) {
|
|
@@ -1860,6 +1741,7 @@ export {
|
|
|
1860
1741
|
REQUEST_IN_FLIGHT,
|
|
1861
1742
|
ThunderBridge,
|
|
1862
1743
|
UnverifiedRecipientError,
|
|
1744
|
+
bankRail,
|
|
1863
1745
|
bankTransfer,
|
|
1864
1746
|
bankVerifyEndpoint,
|
|
1865
1747
|
bitstamp,
|
|
@@ -1872,7 +1754,7 @@ export {
|
|
|
1872
1754
|
isProblemType,
|
|
1873
1755
|
isProvablyPaid,
|
|
1874
1756
|
kraken,
|
|
1875
|
-
|
|
1757
|
+
lightningRail,
|
|
1876
1758
|
lnurlToDataUrl,
|
|
1877
1759
|
lnurlToSvg,
|
|
1878
1760
|
medianOf,
|
|
@@ -1884,6 +1766,8 @@ export {
|
|
|
1884
1766
|
preimageMatchesHash,
|
|
1885
1767
|
proveOrigin,
|
|
1886
1768
|
proveSettlement,
|
|
1769
|
+
qrToDataUrl,
|
|
1770
|
+
qrToSvg,
|
|
1887
1771
|
seal,
|
|
1888
1772
|
spdToDataUrl,
|
|
1889
1773
|
spdToSvg,
|