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.cjs
CHANGED
|
@@ -34,7 +34,6 @@ __export(index_exports, {
|
|
|
34
34
|
bankTransfer: () => bankTransfer,
|
|
35
35
|
bankVerifyEndpoint: () => bankVerifyEndpoint,
|
|
36
36
|
bitstamp: () => bitstamp,
|
|
37
|
-
blindLightningRail: () => blindLightningRail,
|
|
38
37
|
coinbase: () => coinbase,
|
|
39
38
|
coinmate: () => coinmate,
|
|
40
39
|
decodeInvoice: () => decodeInvoice,
|
|
@@ -45,13 +44,14 @@ __export(index_exports, {
|
|
|
45
44
|
isProvablyPaid: () => isProvablyPaid,
|
|
46
45
|
kraken: () => kraken,
|
|
47
46
|
lightningRail: () => lightningRail,
|
|
48
|
-
lnurlPayEndpoint: () => lnurlPayEndpoint,
|
|
49
47
|
lnurlToDataUrl: () => lnurlToDataUrl,
|
|
50
48
|
lnurlToSvg: () => lnurlToSvg,
|
|
51
49
|
medianOf: () => medianOf,
|
|
52
50
|
minorScaleOf: () => minorScaleOf,
|
|
53
51
|
minorUnitsOf: () => minorUnitsOf,
|
|
54
52
|
msatFor: () => msatFor,
|
|
53
|
+
parseWatchedWebhook: () => parseWatchedWebhook,
|
|
54
|
+
parseWatchedWebhookRequest: () => parseWatchedWebhookRequest,
|
|
55
55
|
parseWebhook: () => parseWebhook,
|
|
56
56
|
parseWebhookRequest: () => parseWebhookRequest,
|
|
57
57
|
preimageMatchesHash: () => preimageMatchesHash,
|
|
@@ -141,8 +141,8 @@ function hexToBytes(hex) {
|
|
|
141
141
|
}
|
|
142
142
|
return bytes;
|
|
143
143
|
}
|
|
144
|
-
function isHex(
|
|
145
|
-
return /^[0-9a-f]+$/i.test(
|
|
144
|
+
function isHex(text2) {
|
|
145
|
+
return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
// ../core/sha256.ts
|
|
@@ -236,8 +236,8 @@ function sha256(bytes) {
|
|
|
236
236
|
}
|
|
237
237
|
return asBytes(state);
|
|
238
238
|
}
|
|
239
|
-
function sha256Hex(
|
|
240
|
-
return bytesToHex(sha256(new TextEncoder().encode(
|
|
239
|
+
function sha256Hex(text2) {
|
|
240
|
+
return bytesToHex(sha256(new TextEncoder().encode(text2)));
|
|
241
241
|
}
|
|
242
242
|
function withPadding(bytes) {
|
|
243
243
|
const padded = new Uint8Array(
|
|
@@ -611,7 +611,8 @@ function watchRequestBody(params, trigger) {
|
|
|
611
611
|
verify_url: params.verifyUrl,
|
|
612
612
|
expires_at: expiresAt.toISOString(),
|
|
613
613
|
trigger: trigger ?? void 0,
|
|
614
|
-
sealed: params.sealed
|
|
614
|
+
sealed: params.sealed,
|
|
615
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl, secret: params.webhookSecret } : void 0
|
|
615
616
|
});
|
|
616
617
|
}
|
|
617
618
|
function triggerEventFromWire(body) {
|
|
@@ -1184,10 +1185,10 @@ function toBase64Url(bytes) {
|
|
|
1184
1185
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1185
1186
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1186
1187
|
}
|
|
1187
|
-
function fromBase64Url(
|
|
1188
|
-
if (
|
|
1188
|
+
function fromBase64Url(text2) {
|
|
1189
|
+
if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
|
|
1189
1190
|
try {
|
|
1190
|
-
const binary = atob(
|
|
1191
|
+
const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
|
|
1191
1192
|
const bytes = new Uint8Array(binary.length);
|
|
1192
1193
|
for (let at = 0; at < bytes.length; at++) bytes[at] = binary.charCodeAt(at);
|
|
1193
1194
|
return bytes;
|
|
@@ -1219,368 +1220,6 @@ function asBuffer(bytes) {
|
|
|
1219
1220
|
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1220
1221
|
}
|
|
1221
1222
|
|
|
1222
|
-
// ../core/outbound.ts
|
|
1223
|
-
var import_promises = require("dns/promises");
|
|
1224
|
-
var import_promises2 = require("timers/promises");
|
|
1225
|
-
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1226
|
-
var LOOKUP_TIMEOUT_MS = 5e3;
|
|
1227
|
-
var HOPS_FOLLOWED = 2;
|
|
1228
|
-
var REDIRECTS = [301, 302, 303, 307, 308];
|
|
1229
|
-
var KEEPS_THE_METHOD = [307, 308];
|
|
1230
|
-
var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
|
|
1231
|
-
var BODY_LIMIT_BYTES = 262144;
|
|
1232
|
-
async function ask(url, sent = {}) {
|
|
1233
|
-
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
|
|
1234
|
-
const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
|
|
1235
|
-
let target = url;
|
|
1236
|
-
let carried = sent;
|
|
1237
|
-
for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
|
|
1238
|
-
await refuseUnlessPublic(target);
|
|
1239
|
-
const response = await fetch(target, {
|
|
1240
|
-
method: carried.method ?? "GET",
|
|
1241
|
-
headers: carried.headers ?? {},
|
|
1242
|
-
body: carried.body,
|
|
1243
|
-
redirect: "manual",
|
|
1244
|
-
signal
|
|
1245
|
-
});
|
|
1246
|
-
if (!REDIRECTS.includes(response.status)) return await answerOf(response);
|
|
1247
|
-
const location = response.headers.get("location");
|
|
1248
|
-
if (location === null) {
|
|
1249
|
-
throw new Error(`${target} answered ${response.status} without saying where to`);
|
|
1250
|
-
}
|
|
1251
|
-
await response.body?.cancel();
|
|
1252
|
-
const next = new URL(location, target).toString();
|
|
1253
|
-
carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
|
|
1254
|
-
carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
|
|
1255
|
-
target = next;
|
|
1256
|
-
}
|
|
1257
|
-
throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
|
|
1258
|
-
}
|
|
1259
|
-
async function resolvesNothingPrivate(url) {
|
|
1260
|
-
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
1261
|
-
const found = await Promise.race([
|
|
1262
|
-
(0, import_promises.lookup)(host, { all: true, verbatim: true }).catch(() => []),
|
|
1263
|
-
(0, import_promises2.setTimeout)(LOOKUP_TIMEOUT_MS, null, { ref: false })
|
|
1264
|
-
]);
|
|
1265
|
-
return found !== null && found.every((one) => publicAddress(one.address));
|
|
1266
|
-
}
|
|
1267
|
-
async function refuseUnlessPublic(url) {
|
|
1268
|
-
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
1269
|
-
if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
|
|
1270
|
-
}
|
|
1271
|
-
async function answerOf(response) {
|
|
1272
|
-
const { status, ok } = response;
|
|
1273
|
-
const reader = response.body?.getReader();
|
|
1274
|
-
if (!reader) return { status, ok, body: "", truncated: false };
|
|
1275
|
-
const read = [];
|
|
1276
|
-
let bytes = 0;
|
|
1277
|
-
while (bytes <= BODY_LIMIT_BYTES) {
|
|
1278
|
-
const { done, value } = await reader.read();
|
|
1279
|
-
if (done) return { status, ok, body: text2(read), truncated: false };
|
|
1280
|
-
read.push(value);
|
|
1281
|
-
bytes += value.length;
|
|
1282
|
-
}
|
|
1283
|
-
await reader.cancel();
|
|
1284
|
-
return { status, ok, body: text2(read), truncated: true };
|
|
1285
|
-
}
|
|
1286
|
-
function withoutBody(sent) {
|
|
1287
|
-
const headers = { ...sent.headers };
|
|
1288
|
-
delete headers["content-type"];
|
|
1289
|
-
return { ...sent, method: "GET", headers, body: void 0 };
|
|
1290
|
-
}
|
|
1291
|
-
function withoutCredentials(sent) {
|
|
1292
|
-
const headers = { ...sent.headers };
|
|
1293
|
-
for (const named of CREDENTIALS) delete headers[named];
|
|
1294
|
-
return { ...sent, headers };
|
|
1295
|
-
}
|
|
1296
|
-
function text2(chunks) {
|
|
1297
|
-
const decoder = new TextDecoder();
|
|
1298
|
-
let decoded = "";
|
|
1299
|
-
for (const chunk of chunks) decoded += decoder.decode(chunk, { stream: true });
|
|
1300
|
-
return decoded + decoder.decode();
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// ../core/refusal.ts
|
|
1304
|
-
var WalletRefused = class extends Error {
|
|
1305
|
-
reason;
|
|
1306
|
-
constructor(reason, message) {
|
|
1307
|
-
super(message);
|
|
1308
|
-
this.reason = reason;
|
|
1309
|
-
}
|
|
1310
|
-
};
|
|
1311
|
-
var NoWalletAvailable = class extends Error {
|
|
1312
|
-
wallets;
|
|
1313
|
-
constructor(wallets) {
|
|
1314
|
-
super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
|
|
1315
|
-
this.wallets = wallets;
|
|
1316
|
-
}
|
|
1317
|
-
};
|
|
1318
|
-
|
|
1319
|
-
// ../core/lnurl.ts
|
|
1320
|
-
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1321
|
-
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1322
|
-
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1323
|
-
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1324
|
-
var LNURL_HRP = "lnurl";
|
|
1325
|
-
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
1326
|
-
async function resolve(addresses, amountMsat) {
|
|
1327
|
-
const served = await firstThatServes(
|
|
1328
|
-
addresses,
|
|
1329
|
-
(address, deadline) => resolveAddress(address, amountMsat, deadline)
|
|
1330
|
-
);
|
|
1331
|
-
return served.won;
|
|
1332
|
-
}
|
|
1333
|
-
async function firstThatServes(addresses, attempt) {
|
|
1334
|
-
const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
1335
|
-
const refusals3 = [];
|
|
1336
|
-
for (const address of addresses) {
|
|
1337
|
-
if (deadline.aborted) break;
|
|
1338
|
-
try {
|
|
1339
|
-
return { won: await attempt(address, deadline), refusals: refusals3 };
|
|
1340
|
-
} catch (refusal) {
|
|
1341
|
-
if (!(refusal instanceof WalletRefused)) throw refusal;
|
|
1342
|
-
console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
|
|
1343
|
-
refusals3.push({ address, reason: refusal.reason });
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
throw new NoWalletAvailable(refusals3);
|
|
1347
|
-
}
|
|
1348
|
-
async function probe(address, amountMsat, deadline) {
|
|
1349
|
-
const [user, domain] = splitAddress(address);
|
|
1350
|
-
if (cannotReleaseAPreimage(domain)) {
|
|
1351
|
-
throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
|
|
1352
|
-
}
|
|
1353
|
-
const pay = await answered(
|
|
1354
|
-
`https://${domain}/.well-known/lnurlp/${user}`,
|
|
1355
|
-
deadline
|
|
1356
|
-
);
|
|
1357
|
-
if (pay.tag !== "payRequest" || !pay.callback) {
|
|
1358
|
-
throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
|
|
1359
|
-
}
|
|
1360
|
-
if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
|
|
1361
|
-
throw new WalletRefused(
|
|
1362
|
-
"amount-not-accepted",
|
|
1363
|
-
`${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
|
|
1364
|
-
);
|
|
1365
|
-
}
|
|
1366
|
-
return { address, pay };
|
|
1367
|
-
}
|
|
1368
|
-
async function resolveAddress(address, amountMsat, deadline) {
|
|
1369
|
-
const { pay } = await probe(address, amountMsat, deadline);
|
|
1370
|
-
const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
|
|
1371
|
-
if (!invoice.pr) throw new WalletRefused("unreachable", `${address} returned no invoice`);
|
|
1372
|
-
if (!invoice.verify || !publicHttps(invoice.verify)) {
|
|
1373
|
-
throw new WalletRefused(
|
|
1374
|
-
"cannot-prove-delivery",
|
|
1375
|
-
`${address} publishes no usable LUD-21 verify URL`
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
|
-
const issued = decodeIssued(address, invoice.pr);
|
|
1379
|
-
if (issued.amountMsat !== amountMsat) {
|
|
1380
|
-
throw new WalletRefused(
|
|
1381
|
-
"invoice-refused",
|
|
1382
|
-
`${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
|
|
1383
|
-
);
|
|
1384
|
-
}
|
|
1385
|
-
if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
|
|
1386
|
-
throw new WalletRefused(
|
|
1387
|
-
"invoice-refused",
|
|
1388
|
-
`${address} does not bind its invoice to the metadata it serves`
|
|
1389
|
-
);
|
|
1390
|
-
}
|
|
1391
|
-
return {
|
|
1392
|
-
address,
|
|
1393
|
-
bolt11: invoice.pr,
|
|
1394
|
-
verifyUrl: invoice.verify,
|
|
1395
|
-
paymentHash: issued.paymentHash,
|
|
1396
|
-
expiresAt: issued.expiresAt
|
|
1397
|
-
};
|
|
1398
|
-
}
|
|
1399
|
-
async function answered(url, deadline) {
|
|
1400
|
-
try {
|
|
1401
|
-
return await fetchJson(url, deadline);
|
|
1402
|
-
} catch (cause) {
|
|
1403
|
-
throw new WalletRefused("unreachable", String(cause));
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
function decodeIssued(address, bolt11) {
|
|
1407
|
-
const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
|
|
1408
|
-
if (paymentHash === null || expiresAt === null) {
|
|
1409
|
-
throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
|
|
1410
|
-
}
|
|
1411
|
-
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
1412
|
-
}
|
|
1413
|
-
function cannotReleaseAPreimage(host) {
|
|
1414
|
-
const lowered = host.toLowerCase();
|
|
1415
|
-
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
1416
|
-
(known) => lowered === known || lowered.endsWith(`.${known}`)
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
|
-
function toLnurl(endpoint) {
|
|
1420
|
-
const scheme = parsedScheme(endpoint);
|
|
1421
|
-
if (scheme !== "https:" && scheme !== "http:") {
|
|
1422
|
-
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1423
|
-
}
|
|
1424
|
-
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1425
|
-
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1426
|
-
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1427
|
-
}
|
|
1428
|
-
function parsedScheme(endpoint) {
|
|
1429
|
-
try {
|
|
1430
|
-
return new URL(endpoint).protocol;
|
|
1431
|
-
} catch {
|
|
1432
|
-
return null;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
function toWords(bytes) {
|
|
1436
|
-
const words = [];
|
|
1437
|
-
let accumulator = 0;
|
|
1438
|
-
let bits = 0;
|
|
1439
|
-
for (const byte of bytes) {
|
|
1440
|
-
accumulator = accumulator << 8 | byte;
|
|
1441
|
-
bits += 8;
|
|
1442
|
-
while (bits >= 5) {
|
|
1443
|
-
bits -= 5;
|
|
1444
|
-
words.push(accumulator >> bits & 31);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1448
|
-
return words;
|
|
1449
|
-
}
|
|
1450
|
-
function checksumWords(hrp, words) {
|
|
1451
|
-
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1452
|
-
const values = [
|
|
1453
|
-
...expanded.map((code) => code >> 5),
|
|
1454
|
-
0,
|
|
1455
|
-
...expanded.map((code) => code & 31),
|
|
1456
|
-
...words,
|
|
1457
|
-
...CHECKSUM_SLOTS.map(() => 0)
|
|
1458
|
-
];
|
|
1459
|
-
const polymod = bech32Polymod(values) ^ 1;
|
|
1460
|
-
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1461
|
-
}
|
|
1462
|
-
function bech32Polymod(values) {
|
|
1463
|
-
let checksum = 1;
|
|
1464
|
-
for (const value of values) {
|
|
1465
|
-
const top = checksum >> 25;
|
|
1466
|
-
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1467
|
-
for (let bit = 0; bit < 5; bit++) {
|
|
1468
|
-
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
return checksum;
|
|
1472
|
-
}
|
|
1473
|
-
function withAmount(callback, amountMsat) {
|
|
1474
|
-
const separator = callback.includes("?") ? "&" : "?";
|
|
1475
|
-
return `${callback}${separator}amount=${amountMsat}`;
|
|
1476
|
-
}
|
|
1477
|
-
function splitAddress(address) {
|
|
1478
|
-
const [user, domain, ...rest] = address.split("@");
|
|
1479
|
-
if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
|
|
1480
|
-
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
1481
|
-
}
|
|
1482
|
-
return [user, domain];
|
|
1483
|
-
}
|
|
1484
|
-
async function fetchJson(url, deadline) {
|
|
1485
|
-
const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
|
|
1486
|
-
if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
|
|
1487
|
-
if (answer.truncated) {
|
|
1488
|
-
throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
|
|
1489
|
-
}
|
|
1490
|
-
return JSON.parse(answer.body);
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// src/trigger.ts
|
|
1494
|
-
var NONCE_BYTES = 16;
|
|
1495
|
-
function lnurlPayEndpoint(config) {
|
|
1496
|
-
return async (request) => {
|
|
1497
|
-
const url = new URL(request.url);
|
|
1498
|
-
const asked = url.searchParams.get("to");
|
|
1499
|
-
try {
|
|
1500
|
-
return asked === null ? await offer(config, url) : await mint(config, url, asked);
|
|
1501
|
-
} catch (failure) {
|
|
1502
|
-
return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
|
|
1503
|
-
}
|
|
1504
|
-
};
|
|
1505
|
-
}
|
|
1506
|
-
async function offer(config, url) {
|
|
1507
|
-
const amountMsat = await config.amountMsat();
|
|
1508
|
-
const quote = await config.gateway.createQuote({
|
|
1509
|
-
lnAddresses: config.lnAddresses,
|
|
1510
|
-
amountMsat
|
|
1511
|
-
});
|
|
1512
|
-
const nonce = randomNonce();
|
|
1513
|
-
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
1514
|
-
callback.searchParams.set("to", quote.lnAddress);
|
|
1515
|
-
callback.searchParams.set("msat", String(amountMsat));
|
|
1516
|
-
callback.searchParams.set("n", nonce);
|
|
1517
|
-
callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
|
|
1518
|
-
return Response.json({
|
|
1519
|
-
tag: "payRequest",
|
|
1520
|
-
callback: callback.toString(),
|
|
1521
|
-
metadata: quote.metadata,
|
|
1522
|
-
minSendable: amountMsat,
|
|
1523
|
-
maxSendable: amountMsat
|
|
1524
|
-
});
|
|
1525
|
-
}
|
|
1526
|
-
async function mint(config, url, address) {
|
|
1527
|
-
const amountMsat = Number(url.searchParams.get("msat"));
|
|
1528
|
-
const nonce = url.searchParams.get("n") ?? "";
|
|
1529
|
-
const signature = url.searchParams.get("sig") ?? "";
|
|
1530
|
-
const expected = await sign(config.secret, address, amountMsat, nonce);
|
|
1531
|
-
if (!equalInConstantTime(signature, expected)) return refuse("this callback was not signed here");
|
|
1532
|
-
const wanted = url.searchParams.get("amount");
|
|
1533
|
-
if (wanted !== null && Number(wanted) !== amountMsat) {
|
|
1534
|
-
return refuse(`this trigger costs exactly ${amountMsat} msat`);
|
|
1535
|
-
}
|
|
1536
|
-
const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
|
|
1537
|
-
return Response.json({
|
|
1538
|
-
status: "OK",
|
|
1539
|
-
pr: minted.bolt11,
|
|
1540
|
-
routes: [],
|
|
1541
|
-
verify: minted.verifyUrl
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
async function mintThroughGateway(config, address, amountMsat, nonce) {
|
|
1545
|
-
const payment = await config.gateway.createPayment(
|
|
1546
|
-
{ lnAddresses: [address], amountMsat },
|
|
1547
|
-
{ idempotencyKey: nonce, trigger: config.watchSecret }
|
|
1548
|
-
);
|
|
1549
|
-
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
1550
|
-
}
|
|
1551
|
-
async function mintBlind(config, address, amountMsat) {
|
|
1552
|
-
const resolved = await resolve([address], amountMsat);
|
|
1553
|
-
const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
|
|
1554
|
-
const locked = config.sealed;
|
|
1555
|
-
try {
|
|
1556
|
-
await config.gateway.watchPayment({
|
|
1557
|
-
paymentHash: resolved.paymentHash,
|
|
1558
|
-
verifyUrl: resolved.verifyUrl,
|
|
1559
|
-
expiresAt: resolved.expiresAt,
|
|
1560
|
-
trigger: config.watchSecret,
|
|
1561
|
-
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
1562
|
-
});
|
|
1563
|
-
} catch (refused) {
|
|
1564
|
-
if (!alreadyWatched(refused)) throw refused;
|
|
1565
|
-
}
|
|
1566
|
-
return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
|
|
1567
|
-
}
|
|
1568
|
-
function alreadyWatched(refused) {
|
|
1569
|
-
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
1570
|
-
}
|
|
1571
|
-
function sign(secret, address, amountMsat, nonce) {
|
|
1572
|
-
return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
|
|
1573
|
-
}
|
|
1574
|
-
function randomNonce() {
|
|
1575
|
-
const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
1576
|
-
let hex = "";
|
|
1577
|
-
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
1578
|
-
return hex;
|
|
1579
|
-
}
|
|
1580
|
-
function refuse(reason) {
|
|
1581
|
-
return Response.json({ status: "ERROR", reason });
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
1223
|
// src/currency.ts
|
|
1585
1224
|
var ISO_4217_MINOR_UNITS = {
|
|
1586
1225
|
BHD: 3,
|
|
@@ -1645,7 +1284,9 @@ async function bankTransfer(params) {
|
|
|
1645
1284
|
verifyUrl,
|
|
1646
1285
|
expiresAt: params.expiresAt,
|
|
1647
1286
|
trigger: params.trigger,
|
|
1648
|
-
sealed: params.sealed
|
|
1287
|
+
sealed: params.sealed,
|
|
1288
|
+
webhookUrl: params.webhookUrl,
|
|
1289
|
+
webhookSecret: params.webhookSecret
|
|
1649
1290
|
});
|
|
1650
1291
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1651
1292
|
}
|
|
@@ -1824,6 +1465,66 @@ function asDate(unix) {
|
|
|
1824
1465
|
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
1825
1466
|
}
|
|
1826
1467
|
|
|
1468
|
+
// ../core/lnurl.ts
|
|
1469
|
+
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1470
|
+
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1471
|
+
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1472
|
+
var LNURL_HRP = "lnurl";
|
|
1473
|
+
function toLnurl(endpoint) {
|
|
1474
|
+
const scheme = parsedScheme(endpoint);
|
|
1475
|
+
if (scheme !== "https:" && scheme !== "http:") {
|
|
1476
|
+
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1477
|
+
}
|
|
1478
|
+
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1479
|
+
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1480
|
+
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1481
|
+
}
|
|
1482
|
+
function parsedScheme(endpoint) {
|
|
1483
|
+
try {
|
|
1484
|
+
return new URL(endpoint).protocol;
|
|
1485
|
+
} catch {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function toWords(bytes) {
|
|
1490
|
+
const words = [];
|
|
1491
|
+
let accumulator = 0;
|
|
1492
|
+
let bits = 0;
|
|
1493
|
+
for (const byte of bytes) {
|
|
1494
|
+
accumulator = accumulator << 8 | byte;
|
|
1495
|
+
bits += 8;
|
|
1496
|
+
while (bits >= 5) {
|
|
1497
|
+
bits -= 5;
|
|
1498
|
+
words.push(accumulator >> bits & 31);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1502
|
+
return words;
|
|
1503
|
+
}
|
|
1504
|
+
function checksumWords(hrp, words) {
|
|
1505
|
+
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1506
|
+
const values = [
|
|
1507
|
+
...expanded.map((code) => code >> 5),
|
|
1508
|
+
0,
|
|
1509
|
+
...expanded.map((code) => code & 31),
|
|
1510
|
+
...words,
|
|
1511
|
+
...CHECKSUM_SLOTS.map(() => 0)
|
|
1512
|
+
];
|
|
1513
|
+
const polymod = bech32Polymod(values) ^ 1;
|
|
1514
|
+
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1515
|
+
}
|
|
1516
|
+
function bech32Polymod(values) {
|
|
1517
|
+
let checksum = 1;
|
|
1518
|
+
for (const value of values) {
|
|
1519
|
+
const top = checksum >> 25;
|
|
1520
|
+
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1521
|
+
for (let bit = 0; bit < 5; bit++) {
|
|
1522
|
+
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return checksum;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1827
1528
|
// src/qr.ts
|
|
1828
1529
|
var import_uqr = require("uqr");
|
|
1829
1530
|
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
@@ -1906,6 +1607,8 @@ function bankRail(config) {
|
|
|
1906
1607
|
trigger: config.trigger,
|
|
1907
1608
|
sealed: await config.sealed?.(order),
|
|
1908
1609
|
variableSymbol: config.variableSymbol?.(order),
|
|
1610
|
+
webhookUrl: config.webhookUrl,
|
|
1611
|
+
webhookSecret: config.webhookSecret,
|
|
1909
1612
|
allowPublicGateway: config.allowPublicGateway
|
|
1910
1613
|
});
|
|
1911
1614
|
return {
|
|
@@ -1937,35 +1640,6 @@ function lightningRail(config) {
|
|
|
1937
1640
|
};
|
|
1938
1641
|
};
|
|
1939
1642
|
}
|
|
1940
|
-
function blindLightningRail(config) {
|
|
1941
|
-
return async (order) => {
|
|
1942
|
-
const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
1943
|
-
const watched = await config.gateway.watchPayment({
|
|
1944
|
-
paymentHash: resolved.paymentHash,
|
|
1945
|
-
verifyUrl: resolved.verifyUrl,
|
|
1946
|
-
expiresAt: resolved.expiresAt,
|
|
1947
|
-
trigger: config.trigger,
|
|
1948
|
-
sealed: await config.sealed?.(order)
|
|
1949
|
-
});
|
|
1950
|
-
return {
|
|
1951
|
-
id: watched.id,
|
|
1952
|
-
rail: config.name ?? LIGHTNING,
|
|
1953
|
-
scan: resolved.bolt11,
|
|
1954
|
-
qr: encodeForQr(resolved.bolt11),
|
|
1955
|
-
expiresAt: resolved.expiresAt
|
|
1956
|
-
};
|
|
1957
|
-
};
|
|
1958
|
-
}
|
|
1959
|
-
async function invoiceFrom(lnAddresses, amountMsat) {
|
|
1960
|
-
try {
|
|
1961
|
-
return await resolve(lnAddresses, amountMsat);
|
|
1962
|
-
} catch (refused) {
|
|
1963
|
-
if (refused instanceof NoWalletAvailable) {
|
|
1964
|
-
throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
|
|
1965
|
-
}
|
|
1966
|
-
throw refused;
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
1643
|
|
|
1970
1644
|
// src/price.ts
|
|
1971
1645
|
var MSAT_PER_BTC2 = 1e11;
|
|
@@ -2097,13 +1771,13 @@ var DEFAULT_TOLERANCE_SECS = 300;
|
|
|
2097
1771
|
async function verifyWebhookSignature(body, signature, secret, timestamp, options = {}) {
|
|
2098
1772
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
2099
1773
|
const received = signature.startsWith(SIGNATURE_PREFIX) ? signature.slice(SIGNATURE_PREFIX.length) : signature;
|
|
2100
|
-
return equalInConstantTime(await
|
|
1774
|
+
return equalInConstantTime(await sign(secret, timestamp, body), received.toLowerCase());
|
|
2101
1775
|
}
|
|
2102
1776
|
async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
2103
1777
|
if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
|
|
2104
|
-
const
|
|
1778
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
2105
1779
|
try {
|
|
2106
|
-
return paymentFromWire(JSON.parse(
|
|
1780
|
+
return paymentFromWire(JSON.parse(text2));
|
|
2107
1781
|
} catch {
|
|
2108
1782
|
return null;
|
|
2109
1783
|
}
|
|
@@ -2114,12 +1788,27 @@ async function parseWebhookRequest(request, secret, options = {}) {
|
|
|
2114
1788
|
if (signature === null || timestamp === null) return null;
|
|
2115
1789
|
return parseWebhook(await request.text(), signature, secret, timestamp, options);
|
|
2116
1790
|
}
|
|
1791
|
+
async function parseWatchedWebhook(body, signature, secret, timestamp, options = {}) {
|
|
1792
|
+
if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
|
|
1793
|
+
const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1794
|
+
try {
|
|
1795
|
+
return triggerEventFromWire(JSON.parse(text2));
|
|
1796
|
+
} catch {
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
async function parseWatchedWebhookRequest(request, secret, options = {}) {
|
|
1801
|
+
const signature = request.headers.get(SIGNATURE_HEADER);
|
|
1802
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER);
|
|
1803
|
+
if (signature === null || timestamp === null) return null;
|
|
1804
|
+
return parseWatchedWebhook(await request.text(), signature, secret, timestamp, options);
|
|
1805
|
+
}
|
|
2117
1806
|
function recent(timestamp, toleranceSecs) {
|
|
2118
1807
|
const sent = Number(timestamp);
|
|
2119
1808
|
if (!Number.isFinite(sent)) return false;
|
|
2120
1809
|
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
2121
1810
|
}
|
|
2122
|
-
async function
|
|
1811
|
+
async function sign(secret, timestamp, body) {
|
|
2123
1812
|
return hmacHex(secret, signed(timestamp, body));
|
|
2124
1813
|
}
|
|
2125
1814
|
function signed(timestamp, body) {
|
|
@@ -2147,7 +1836,6 @@ function signed(timestamp, body) {
|
|
|
2147
1836
|
bankTransfer,
|
|
2148
1837
|
bankVerifyEndpoint,
|
|
2149
1838
|
bitstamp,
|
|
2150
|
-
blindLightningRail,
|
|
2151
1839
|
coinbase,
|
|
2152
1840
|
coinmate,
|
|
2153
1841
|
decodeInvoice,
|
|
@@ -2158,13 +1846,14 @@ function signed(timestamp, body) {
|
|
|
2158
1846
|
isProvablyPaid,
|
|
2159
1847
|
kraken,
|
|
2160
1848
|
lightningRail,
|
|
2161
|
-
lnurlPayEndpoint,
|
|
2162
1849
|
lnurlToDataUrl,
|
|
2163
1850
|
lnurlToSvg,
|
|
2164
1851
|
medianOf,
|
|
2165
1852
|
minorScaleOf,
|
|
2166
1853
|
minorUnitsOf,
|
|
2167
1854
|
msatFor,
|
|
1855
|
+
parseWatchedWebhook,
|
|
1856
|
+
parseWatchedWebhookRequest,
|
|
2168
1857
|
parseWebhook,
|
|
2169
1858
|
parseWebhookRequest,
|
|
2170
1859
|
preimageMatchesHash,
|