thunder-bridge 0.8.2 → 0.8.4
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 +51 -6
- package/dist/index.cjs +97 -408
- package/dist/index.d.cts +18 -434
- package/dist/index.d.ts +18 -434
- package/dist/index.js +95 -406
- package/dist/rail-QZtUN1D-.d.cts +381 -0
- package/dist/rail-QZtUN1D-.d.ts +381 -0
- package/dist/server.cjs +807 -0
- package/dist/server.d.cts +64 -0
- package/dist/server.d.ts +64 -0
- package/dist/server.js +769 -0
- package/package.json +11 -2
package/dist/index.js
CHANGED
|
@@ -71,8 +71,8 @@ function hexToBytes(hex) {
|
|
|
71
71
|
}
|
|
72
72
|
return bytes;
|
|
73
73
|
}
|
|
74
|
-
function isHex(
|
|
75
|
-
return /^[0-9a-f]+$/i.test(
|
|
74
|
+
function isHex(text2) {
|
|
75
|
+
return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
// ../core/sha256.ts
|
|
@@ -166,8 +166,8 @@ function sha256(bytes) {
|
|
|
166
166
|
}
|
|
167
167
|
return asBytes(state);
|
|
168
168
|
}
|
|
169
|
-
function sha256Hex(
|
|
170
|
-
return bytesToHex(sha256(new TextEncoder().encode(
|
|
169
|
+
function sha256Hex(text2) {
|
|
170
|
+
return bytesToHex(sha256(new TextEncoder().encode(text2)));
|
|
171
171
|
}
|
|
172
172
|
function withPadding(bytes) {
|
|
173
173
|
const padded = new Uint8Array(
|
|
@@ -541,7 +541,8 @@ function watchRequestBody(params, trigger) {
|
|
|
541
541
|
verify_url: params.verifyUrl,
|
|
542
542
|
expires_at: expiresAt.toISOString(),
|
|
543
543
|
trigger: trigger ?? void 0,
|
|
544
|
-
sealed: params.sealed
|
|
544
|
+
sealed: params.sealed,
|
|
545
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl, secret: params.webhookSecret } : void 0
|
|
545
546
|
});
|
|
546
547
|
}
|
|
547
548
|
function triggerEventFromWire(body) {
|
|
@@ -1114,10 +1115,10 @@ function toBase64Url(bytes) {
|
|
|
1114
1115
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1115
1116
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1116
1117
|
}
|
|
1117
|
-
function fromBase64Url(
|
|
1118
|
-
if (
|
|
1118
|
+
function fromBase64Url(text2) {
|
|
1119
|
+
if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
|
|
1119
1120
|
try {
|
|
1120
|
-
const binary = atob(
|
|
1121
|
+
const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
|
|
1121
1122
|
const bytes = new Uint8Array(binary.length);
|
|
1122
1123
|
for (let at = 0; at < bytes.length; at++) bytes[at] = binary.charCodeAt(at);
|
|
1123
1124
|
return bytes;
|
|
@@ -1149,368 +1150,6 @@ function asBuffer(bytes) {
|
|
|
1149
1150
|
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1150
1151
|
}
|
|
1151
1152
|
|
|
1152
|
-
// ../core/outbound.ts
|
|
1153
|
-
import { lookup } from "dns/promises";
|
|
1154
|
-
import { setTimeout as sleep } from "timers/promises";
|
|
1155
|
-
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1156
|
-
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
1157
|
-
var HOPS_FOLLOWED = 2;
|
|
1158
|
-
var REDIRECTS = [301, 302, 303, 307, 308];
|
|
1159
|
-
var KEEPS_THE_METHOD = [307, 308];
|
|
1160
|
-
var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
|
|
1161
|
-
var BODY_LIMIT_BYTES = 262144;
|
|
1162
|
-
async function ask(url, sent = {}) {
|
|
1163
|
-
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
|
|
1164
|
-
const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
|
|
1165
|
-
let target = url;
|
|
1166
|
-
let carried = sent;
|
|
1167
|
-
for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
|
|
1168
|
-
await refuseUnlessPublic(target);
|
|
1169
|
-
const response = await fetch(target, {
|
|
1170
|
-
method: carried.method ?? "GET",
|
|
1171
|
-
headers: carried.headers ?? {},
|
|
1172
|
-
body: carried.body,
|
|
1173
|
-
redirect: "manual",
|
|
1174
|
-
signal
|
|
1175
|
-
});
|
|
1176
|
-
if (!REDIRECTS.includes(response.status)) return await answerOf(response);
|
|
1177
|
-
const location = response.headers.get("location");
|
|
1178
|
-
if (location === null) {
|
|
1179
|
-
throw new Error(`${target} answered ${response.status} without saying where to`);
|
|
1180
|
-
}
|
|
1181
|
-
await response.body?.cancel();
|
|
1182
|
-
const next = new URL(location, target).toString();
|
|
1183
|
-
carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
|
|
1184
|
-
carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
|
|
1185
|
-
target = next;
|
|
1186
|
-
}
|
|
1187
|
-
throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
|
|
1188
|
-
}
|
|
1189
|
-
async function resolvesNothingPrivate(url) {
|
|
1190
|
-
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
1191
|
-
const found = await Promise.race([
|
|
1192
|
-
lookup(host, { all: true, verbatim: true }).catch(() => []),
|
|
1193
|
-
sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
|
|
1194
|
-
]);
|
|
1195
|
-
return found !== null && found.every((one) => publicAddress(one.address));
|
|
1196
|
-
}
|
|
1197
|
-
async function refuseUnlessPublic(url) {
|
|
1198
|
-
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
1199
|
-
if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
|
|
1200
|
-
}
|
|
1201
|
-
async function answerOf(response) {
|
|
1202
|
-
const { status, ok } = response;
|
|
1203
|
-
const reader = response.body?.getReader();
|
|
1204
|
-
if (!reader) return { status, ok, body: "", truncated: false };
|
|
1205
|
-
const read = [];
|
|
1206
|
-
let bytes = 0;
|
|
1207
|
-
while (bytes <= BODY_LIMIT_BYTES) {
|
|
1208
|
-
const { done, value } = await reader.read();
|
|
1209
|
-
if (done) return { status, ok, body: text2(read), truncated: false };
|
|
1210
|
-
read.push(value);
|
|
1211
|
-
bytes += value.length;
|
|
1212
|
-
}
|
|
1213
|
-
await reader.cancel();
|
|
1214
|
-
return { status, ok, body: text2(read), truncated: true };
|
|
1215
|
-
}
|
|
1216
|
-
function withoutBody(sent) {
|
|
1217
|
-
const headers = { ...sent.headers };
|
|
1218
|
-
delete headers["content-type"];
|
|
1219
|
-
return { ...sent, method: "GET", headers, body: void 0 };
|
|
1220
|
-
}
|
|
1221
|
-
function withoutCredentials(sent) {
|
|
1222
|
-
const headers = { ...sent.headers };
|
|
1223
|
-
for (const named of CREDENTIALS) delete headers[named];
|
|
1224
|
-
return { ...sent, headers };
|
|
1225
|
-
}
|
|
1226
|
-
function text2(chunks) {
|
|
1227
|
-
const decoder = new TextDecoder();
|
|
1228
|
-
let decoded = "";
|
|
1229
|
-
for (const chunk of chunks) decoded += decoder.decode(chunk, { stream: true });
|
|
1230
|
-
return decoded + decoder.decode();
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// ../core/refusal.ts
|
|
1234
|
-
var WalletRefused = class extends Error {
|
|
1235
|
-
reason;
|
|
1236
|
-
constructor(reason, message) {
|
|
1237
|
-
super(message);
|
|
1238
|
-
this.reason = reason;
|
|
1239
|
-
}
|
|
1240
|
-
};
|
|
1241
|
-
var NoWalletAvailable = class extends Error {
|
|
1242
|
-
wallets;
|
|
1243
|
-
constructor(wallets) {
|
|
1244
|
-
super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
|
|
1245
|
-
this.wallets = wallets;
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
// ../core/lnurl.ts
|
|
1250
|
-
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1251
|
-
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1252
|
-
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1253
|
-
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1254
|
-
var LNURL_HRP = "lnurl";
|
|
1255
|
-
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
1256
|
-
async function resolve(addresses, amountMsat) {
|
|
1257
|
-
const served = await firstThatServes(
|
|
1258
|
-
addresses,
|
|
1259
|
-
(address, deadline) => resolveAddress(address, amountMsat, deadline)
|
|
1260
|
-
);
|
|
1261
|
-
return served.won;
|
|
1262
|
-
}
|
|
1263
|
-
async function firstThatServes(addresses, attempt) {
|
|
1264
|
-
const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
1265
|
-
const refusals3 = [];
|
|
1266
|
-
for (const address of addresses) {
|
|
1267
|
-
if (deadline.aborted) break;
|
|
1268
|
-
try {
|
|
1269
|
-
return { won: await attempt(address, deadline), refusals: refusals3 };
|
|
1270
|
-
} catch (refusal) {
|
|
1271
|
-
if (!(refusal instanceof WalletRefused)) throw refusal;
|
|
1272
|
-
console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
|
|
1273
|
-
refusals3.push({ address, reason: refusal.reason });
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
throw new NoWalletAvailable(refusals3);
|
|
1277
|
-
}
|
|
1278
|
-
async function probe(address, amountMsat, deadline) {
|
|
1279
|
-
const [user, domain] = splitAddress(address);
|
|
1280
|
-
if (cannotReleaseAPreimage(domain)) {
|
|
1281
|
-
throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
|
|
1282
|
-
}
|
|
1283
|
-
const pay = await answered(
|
|
1284
|
-
`https://${domain}/.well-known/lnurlp/${user}`,
|
|
1285
|
-
deadline
|
|
1286
|
-
);
|
|
1287
|
-
if (pay.tag !== "payRequest" || !pay.callback) {
|
|
1288
|
-
throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
|
|
1289
|
-
}
|
|
1290
|
-
if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
|
|
1291
|
-
throw new WalletRefused(
|
|
1292
|
-
"amount-not-accepted",
|
|
1293
|
-
`${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
|
|
1294
|
-
);
|
|
1295
|
-
}
|
|
1296
|
-
return { address, pay };
|
|
1297
|
-
}
|
|
1298
|
-
async function resolveAddress(address, amountMsat, deadline) {
|
|
1299
|
-
const { pay } = await probe(address, amountMsat, deadline);
|
|
1300
|
-
const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
|
|
1301
|
-
if (!invoice.pr) throw new WalletRefused("unreachable", `${address} returned no invoice`);
|
|
1302
|
-
if (!invoice.verify || !publicHttps(invoice.verify)) {
|
|
1303
|
-
throw new WalletRefused(
|
|
1304
|
-
"cannot-prove-delivery",
|
|
1305
|
-
`${address} publishes no usable LUD-21 verify URL`
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
const issued = decodeIssued(address, invoice.pr);
|
|
1309
|
-
if (issued.amountMsat !== amountMsat) {
|
|
1310
|
-
throw new WalletRefused(
|
|
1311
|
-
"invoice-refused",
|
|
1312
|
-
`${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
|
|
1313
|
-
);
|
|
1314
|
-
}
|
|
1315
|
-
if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
|
|
1316
|
-
throw new WalletRefused(
|
|
1317
|
-
"invoice-refused",
|
|
1318
|
-
`${address} does not bind its invoice to the metadata it serves`
|
|
1319
|
-
);
|
|
1320
|
-
}
|
|
1321
|
-
return {
|
|
1322
|
-
address,
|
|
1323
|
-
bolt11: invoice.pr,
|
|
1324
|
-
verifyUrl: invoice.verify,
|
|
1325
|
-
paymentHash: issued.paymentHash,
|
|
1326
|
-
expiresAt: issued.expiresAt
|
|
1327
|
-
};
|
|
1328
|
-
}
|
|
1329
|
-
async function answered(url, deadline) {
|
|
1330
|
-
try {
|
|
1331
|
-
return await fetchJson(url, deadline);
|
|
1332
|
-
} catch (cause) {
|
|
1333
|
-
throw new WalletRefused("unreachable", String(cause));
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
function decodeIssued(address, bolt11) {
|
|
1337
|
-
const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
|
|
1338
|
-
if (paymentHash === null || expiresAt === null) {
|
|
1339
|
-
throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
|
|
1340
|
-
}
|
|
1341
|
-
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
1342
|
-
}
|
|
1343
|
-
function cannotReleaseAPreimage(host) {
|
|
1344
|
-
const lowered = host.toLowerCase();
|
|
1345
|
-
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
1346
|
-
(known) => lowered === known || lowered.endsWith(`.${known}`)
|
|
1347
|
-
);
|
|
1348
|
-
}
|
|
1349
|
-
function toLnurl(endpoint) {
|
|
1350
|
-
const scheme = parsedScheme(endpoint);
|
|
1351
|
-
if (scheme !== "https:" && scheme !== "http:") {
|
|
1352
|
-
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1353
|
-
}
|
|
1354
|
-
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1355
|
-
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1356
|
-
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1357
|
-
}
|
|
1358
|
-
function parsedScheme(endpoint) {
|
|
1359
|
-
try {
|
|
1360
|
-
return new URL(endpoint).protocol;
|
|
1361
|
-
} catch {
|
|
1362
|
-
return null;
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
function toWords(bytes) {
|
|
1366
|
-
const words = [];
|
|
1367
|
-
let accumulator = 0;
|
|
1368
|
-
let bits = 0;
|
|
1369
|
-
for (const byte of bytes) {
|
|
1370
|
-
accumulator = accumulator << 8 | byte;
|
|
1371
|
-
bits += 8;
|
|
1372
|
-
while (bits >= 5) {
|
|
1373
|
-
bits -= 5;
|
|
1374
|
-
words.push(accumulator >> bits & 31);
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1378
|
-
return words;
|
|
1379
|
-
}
|
|
1380
|
-
function checksumWords(hrp, words) {
|
|
1381
|
-
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1382
|
-
const values = [
|
|
1383
|
-
...expanded.map((code) => code >> 5),
|
|
1384
|
-
0,
|
|
1385
|
-
...expanded.map((code) => code & 31),
|
|
1386
|
-
...words,
|
|
1387
|
-
...CHECKSUM_SLOTS.map(() => 0)
|
|
1388
|
-
];
|
|
1389
|
-
const polymod = bech32Polymod(values) ^ 1;
|
|
1390
|
-
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1391
|
-
}
|
|
1392
|
-
function bech32Polymod(values) {
|
|
1393
|
-
let checksum = 1;
|
|
1394
|
-
for (const value of values) {
|
|
1395
|
-
const top = checksum >> 25;
|
|
1396
|
-
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1397
|
-
for (let bit = 0; bit < 5; bit++) {
|
|
1398
|
-
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
return checksum;
|
|
1402
|
-
}
|
|
1403
|
-
function withAmount(callback, amountMsat) {
|
|
1404
|
-
const separator = callback.includes("?") ? "&" : "?";
|
|
1405
|
-
return `${callback}${separator}amount=${amountMsat}`;
|
|
1406
|
-
}
|
|
1407
|
-
function splitAddress(address) {
|
|
1408
|
-
const [user, domain, ...rest] = address.split("@");
|
|
1409
|
-
if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
|
|
1410
|
-
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
1411
|
-
}
|
|
1412
|
-
return [user, domain];
|
|
1413
|
-
}
|
|
1414
|
-
async function fetchJson(url, deadline) {
|
|
1415
|
-
const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
|
|
1416
|
-
if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
|
|
1417
|
-
if (answer.truncated) {
|
|
1418
|
-
throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
|
|
1419
|
-
}
|
|
1420
|
-
return JSON.parse(answer.body);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
// src/trigger.ts
|
|
1424
|
-
var NONCE_BYTES = 16;
|
|
1425
|
-
function lnurlPayEndpoint(config) {
|
|
1426
|
-
return async (request) => {
|
|
1427
|
-
const url = new URL(request.url);
|
|
1428
|
-
const asked = url.searchParams.get("to");
|
|
1429
|
-
try {
|
|
1430
|
-
return asked === null ? await offer(config, url) : await mint(config, url, asked);
|
|
1431
|
-
} catch (failure) {
|
|
1432
|
-
return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
|
|
1433
|
-
}
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
async function offer(config, url) {
|
|
1437
|
-
const amountMsat = await config.amountMsat();
|
|
1438
|
-
const quote = await config.gateway.createQuote({
|
|
1439
|
-
lnAddresses: config.lnAddresses,
|
|
1440
|
-
amountMsat
|
|
1441
|
-
});
|
|
1442
|
-
const nonce = randomNonce();
|
|
1443
|
-
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
1444
|
-
callback.searchParams.set("to", quote.lnAddress);
|
|
1445
|
-
callback.searchParams.set("msat", String(amountMsat));
|
|
1446
|
-
callback.searchParams.set("n", nonce);
|
|
1447
|
-
callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
|
|
1448
|
-
return Response.json({
|
|
1449
|
-
tag: "payRequest",
|
|
1450
|
-
callback: callback.toString(),
|
|
1451
|
-
metadata: quote.metadata,
|
|
1452
|
-
minSendable: amountMsat,
|
|
1453
|
-
maxSendable: amountMsat
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
async function mint(config, url, address) {
|
|
1457
|
-
const amountMsat = Number(url.searchParams.get("msat"));
|
|
1458
|
-
const nonce = url.searchParams.get("n") ?? "";
|
|
1459
|
-
const signature = url.searchParams.get("sig") ?? "";
|
|
1460
|
-
const expected = await sign(config.secret, address, amountMsat, nonce);
|
|
1461
|
-
if (!equalInConstantTime(signature, expected)) return refuse("this callback was not signed here");
|
|
1462
|
-
const wanted = url.searchParams.get("amount");
|
|
1463
|
-
if (wanted !== null && Number(wanted) !== amountMsat) {
|
|
1464
|
-
return refuse(`this trigger costs exactly ${amountMsat} msat`);
|
|
1465
|
-
}
|
|
1466
|
-
const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
|
|
1467
|
-
return Response.json({
|
|
1468
|
-
status: "OK",
|
|
1469
|
-
pr: minted.bolt11,
|
|
1470
|
-
routes: [],
|
|
1471
|
-
verify: minted.verifyUrl
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
async function mintThroughGateway(config, address, amountMsat, nonce) {
|
|
1475
|
-
const payment = await config.gateway.createPayment(
|
|
1476
|
-
{ lnAddresses: [address], amountMsat },
|
|
1477
|
-
{ idempotencyKey: nonce, trigger: config.watchSecret }
|
|
1478
|
-
);
|
|
1479
|
-
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
1480
|
-
}
|
|
1481
|
-
async function mintBlind(config, address, amountMsat) {
|
|
1482
|
-
const resolved = await resolve([address], amountMsat);
|
|
1483
|
-
const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
|
|
1484
|
-
const locked = config.sealed;
|
|
1485
|
-
try {
|
|
1486
|
-
await config.gateway.watchPayment({
|
|
1487
|
-
paymentHash: resolved.paymentHash,
|
|
1488
|
-
verifyUrl: resolved.verifyUrl,
|
|
1489
|
-
expiresAt: resolved.expiresAt,
|
|
1490
|
-
trigger: config.watchSecret,
|
|
1491
|
-
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
1492
|
-
});
|
|
1493
|
-
} catch (refused) {
|
|
1494
|
-
if (!alreadyWatched(refused)) throw refused;
|
|
1495
|
-
}
|
|
1496
|
-
return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
|
|
1497
|
-
}
|
|
1498
|
-
function alreadyWatched(refused) {
|
|
1499
|
-
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
1500
|
-
}
|
|
1501
|
-
function sign(secret, address, amountMsat, nonce) {
|
|
1502
|
-
return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
|
|
1503
|
-
}
|
|
1504
|
-
function randomNonce() {
|
|
1505
|
-
const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
1506
|
-
let hex = "";
|
|
1507
|
-
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
1508
|
-
return hex;
|
|
1509
|
-
}
|
|
1510
|
-
function refuse(reason) {
|
|
1511
|
-
return Response.json({ status: "ERROR", reason });
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1153
|
// src/currency.ts
|
|
1515
1154
|
var ISO_4217_MINOR_UNITS = {
|
|
1516
1155
|
BHD: 3,
|
|
@@ -1575,7 +1214,9 @@ async function bankTransfer(params) {
|
|
|
1575
1214
|
verifyUrl,
|
|
1576
1215
|
expiresAt: params.expiresAt,
|
|
1577
1216
|
trigger: params.trigger,
|
|
1578
|
-
sealed: params.sealed
|
|
1217
|
+
sealed: params.sealed,
|
|
1218
|
+
webhookUrl: params.webhookUrl,
|
|
1219
|
+
webhookSecret: params.webhookSecret
|
|
1579
1220
|
});
|
|
1580
1221
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1581
1222
|
}
|
|
@@ -1754,6 +1395,66 @@ function asDate(unix) {
|
|
|
1754
1395
|
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
1755
1396
|
}
|
|
1756
1397
|
|
|
1398
|
+
// ../core/lnurl.ts
|
|
1399
|
+
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1400
|
+
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1401
|
+
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1402
|
+
var LNURL_HRP = "lnurl";
|
|
1403
|
+
function toLnurl(endpoint) {
|
|
1404
|
+
const scheme = parsedScheme(endpoint);
|
|
1405
|
+
if (scheme !== "https:" && scheme !== "http:") {
|
|
1406
|
+
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1407
|
+
}
|
|
1408
|
+
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1409
|
+
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1410
|
+
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1411
|
+
}
|
|
1412
|
+
function parsedScheme(endpoint) {
|
|
1413
|
+
try {
|
|
1414
|
+
return new URL(endpoint).protocol;
|
|
1415
|
+
} catch {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function toWords(bytes) {
|
|
1420
|
+
const words = [];
|
|
1421
|
+
let accumulator = 0;
|
|
1422
|
+
let bits = 0;
|
|
1423
|
+
for (const byte of bytes) {
|
|
1424
|
+
accumulator = accumulator << 8 | byte;
|
|
1425
|
+
bits += 8;
|
|
1426
|
+
while (bits >= 5) {
|
|
1427
|
+
bits -= 5;
|
|
1428
|
+
words.push(accumulator >> bits & 31);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1432
|
+
return words;
|
|
1433
|
+
}
|
|
1434
|
+
function checksumWords(hrp, words) {
|
|
1435
|
+
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1436
|
+
const values = [
|
|
1437
|
+
...expanded.map((code) => code >> 5),
|
|
1438
|
+
0,
|
|
1439
|
+
...expanded.map((code) => code & 31),
|
|
1440
|
+
...words,
|
|
1441
|
+
...CHECKSUM_SLOTS.map(() => 0)
|
|
1442
|
+
];
|
|
1443
|
+
const polymod = bech32Polymod(values) ^ 1;
|
|
1444
|
+
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1445
|
+
}
|
|
1446
|
+
function bech32Polymod(values) {
|
|
1447
|
+
let checksum = 1;
|
|
1448
|
+
for (const value of values) {
|
|
1449
|
+
const top = checksum >> 25;
|
|
1450
|
+
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1451
|
+
for (let bit = 0; bit < 5; bit++) {
|
|
1452
|
+
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return checksum;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1757
1458
|
// src/qr.ts
|
|
1758
1459
|
import { encode } from "uqr";
|
|
1759
1460
|
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
@@ -1836,6 +1537,8 @@ function bankRail(config) {
|
|
|
1836
1537
|
trigger: config.trigger,
|
|
1837
1538
|
sealed: await config.sealed?.(order),
|
|
1838
1539
|
variableSymbol: config.variableSymbol?.(order),
|
|
1540
|
+
webhookUrl: config.webhookUrl,
|
|
1541
|
+
webhookSecret: config.webhookSecret,
|
|
1839
1542
|
allowPublicGateway: config.allowPublicGateway
|
|
1840
1543
|
});
|
|
1841
1544
|
return {
|
|
@@ -1867,35 +1570,6 @@ function lightningRail(config) {
|
|
|
1867
1570
|
};
|
|
1868
1571
|
};
|
|
1869
1572
|
}
|
|
1870
|
-
function blindLightningRail(config) {
|
|
1871
|
-
return async (order) => {
|
|
1872
|
-
const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
1873
|
-
const watched = await config.gateway.watchPayment({
|
|
1874
|
-
paymentHash: resolved.paymentHash,
|
|
1875
|
-
verifyUrl: resolved.verifyUrl,
|
|
1876
|
-
expiresAt: resolved.expiresAt,
|
|
1877
|
-
trigger: config.trigger,
|
|
1878
|
-
sealed: await config.sealed?.(order)
|
|
1879
|
-
});
|
|
1880
|
-
return {
|
|
1881
|
-
id: watched.id,
|
|
1882
|
-
rail: config.name ?? LIGHTNING,
|
|
1883
|
-
scan: resolved.bolt11,
|
|
1884
|
-
qr: encodeForQr(resolved.bolt11),
|
|
1885
|
-
expiresAt: resolved.expiresAt
|
|
1886
|
-
};
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
|
-
async function invoiceFrom(lnAddresses, amountMsat) {
|
|
1890
|
-
try {
|
|
1891
|
-
return await resolve(lnAddresses, amountMsat);
|
|
1892
|
-
} catch (refused) {
|
|
1893
|
-
if (refused instanceof NoWalletAvailable) {
|
|
1894
|
-
throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
|
|
1895
|
-
}
|
|
1896
|
-
throw refused;
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
1573
|
|
|
1900
1574
|
// src/price.ts
|
|
1901
1575
|
var MSAT_PER_BTC2 = 1e11;
|
|
@@ -2027,13 +1701,13 @@ var DEFAULT_TOLERANCE_SECS = 300;
|
|
|
2027
1701
|
async function verifyWebhookSignature(body, signature, secret, timestamp, options = {}) {
|
|
2028
1702
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
2029
1703
|
const received = signature.startsWith(SIGNATURE_PREFIX) ? signature.slice(SIGNATURE_PREFIX.length) : signature;
|
|
2030
|
-
return equalInConstantTime(await
|
|
1704
|
+
return equalInConstantTime(await sign(secret, timestamp, body), received.toLowerCase());
|
|
2031
1705
|
}
|
|
2032
1706
|
async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
2033
1707
|
if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
|
|
2034
|
-
const
|
|
1708
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
2035
1709
|
try {
|
|
2036
|
-
return paymentFromWire(JSON.parse(
|
|
1710
|
+
return paymentFromWire(JSON.parse(text2));
|
|
2037
1711
|
} catch {
|
|
2038
1712
|
return null;
|
|
2039
1713
|
}
|
|
@@ -2044,12 +1718,27 @@ async function parseWebhookRequest(request, secret, options = {}) {
|
|
|
2044
1718
|
if (signature === null || timestamp === null) return null;
|
|
2045
1719
|
return parseWebhook(await request.text(), signature, secret, timestamp, options);
|
|
2046
1720
|
}
|
|
1721
|
+
async function parseWatchedWebhook(body, signature, secret, timestamp, options = {}) {
|
|
1722
|
+
if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
|
|
1723
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1724
|
+
try {
|
|
1725
|
+
return triggerEventFromWire(JSON.parse(text2));
|
|
1726
|
+
} catch {
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
async function parseWatchedWebhookRequest(request, secret, options = {}) {
|
|
1731
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1732
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1733
|
+
if (signature === null || timestamp === null) return null;
|
|
1734
|
+
return parseWatchedWebhook(await request.text(), signature, secret, timestamp, options);
|
|
1735
|
+
}
|
|
2047
1736
|
function recent(timestamp, toleranceSecs) {
|
|
2048
1737
|
const sent = Number(timestamp);
|
|
2049
1738
|
if (!Number.isFinite(sent)) return false;
|
|
2050
1739
|
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
2051
1740
|
}
|
|
2052
|
-
async function
|
|
1741
|
+
async function sign(secret, timestamp, body) {
|
|
2053
1742
|
return hmacHex(secret, signed(timestamp, body));
|
|
2054
1743
|
}
|
|
2055
1744
|
function signed(timestamp, body) {
|
|
@@ -2076,7 +1765,6 @@ export {
|
|
|
2076
1765
|
bankTransfer,
|
|
2077
1766
|
bankVerifyEndpoint,
|
|
2078
1767
|
bitstamp,
|
|
2079
|
-
blindLightningRail,
|
|
2080
1768
|
coinbase,
|
|
2081
1769
|
coinmate,
|
|
2082
1770
|
decodeInvoice,
|
|
@@ -2087,13 +1775,14 @@ export {
|
|
|
2087
1775
|
isProvablyPaid,
|
|
2088
1776
|
kraken,
|
|
2089
1777
|
lightningRail,
|
|
2090
|
-
lnurlPayEndpoint,
|
|
2091
1778
|
lnurlToDataUrl,
|
|
2092
1779
|
lnurlToSvg,
|
|
2093
1780
|
medianOf,
|
|
2094
1781
|
minorScaleOf,
|
|
2095
1782
|
minorUnitsOf,
|
|
2096
1783
|
msatFor,
|
|
1784
|
+
parseWatchedWebhook,
|
|
1785
|
+
parseWatchedWebhookRequest,
|
|
2097
1786
|
parseWebhook,
|
|
2098
1787
|
parseWebhookRequest,
|
|
2099
1788
|
preimageMatchesHash,
|