thunder-bridge 2.2.1 → 3.0.1
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 +95 -412
- package/dist/bank.cjs +3 -0
- package/dist/bank.d.cts +3 -3
- package/dist/bank.d.ts +3 -3
- package/dist/bank.js +8 -0
- package/dist/index.cjs +273 -117
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +278 -116
- package/dist/{bank-BgA45yOU.d.ts → nwc-BivXGSPY.d.ts} +70 -114
- package/dist/{bank-DvcV85sa.d.cts → nwc-h_-cGvMb.d.cts} +70 -114
- package/dist/nwc.cjs +10 -424
- package/dist/nwc.d.cts +2 -18
- package/dist/nwc.d.ts +2 -18
- package/dist/nwc.js +10 -422
- package/openapi.yaml +1 -1
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -125,9 +125,9 @@ function fillSchedule(schedule, reader, block) {
|
|
|
125
125
|
schedule[at] = reader.getUint32(block + at * 4, false);
|
|
126
126
|
}
|
|
127
127
|
for (let at = 16; at < SCHEDULE_WORDS; at++) {
|
|
128
|
-
const
|
|
128
|
+
const recent = schedule[at - 15];
|
|
129
129
|
const distant = schedule[at - 2];
|
|
130
|
-
const mixedRecent = rotate(
|
|
130
|
+
const mixedRecent = rotate(recent, 7) ^ rotate(recent, 18) ^ recent >>> 3;
|
|
131
131
|
const mixedDistant = rotate(distant, 17) ^ rotate(distant, 19) ^ distant >>> 10;
|
|
132
132
|
schedule[at] = schedule[at - 16] + mixedRecent + schedule[at - 7] + mixedDistant | 0;
|
|
133
133
|
}
|
|
@@ -382,7 +382,7 @@ async function sealFor(purpose, secret, plaintext, { stable = false } = {}) {
|
|
|
382
382
|
}
|
|
383
383
|
async function unsealFor(purpose, secret, sealed) {
|
|
384
384
|
if (!sealed.startsWith(`${PURPOSE_VERSION}.`)) {
|
|
385
|
-
return
|
|
385
|
+
return null;
|
|
386
386
|
}
|
|
387
387
|
return await opened(
|
|
388
388
|
await keyFor(secret, `${INFO}/${purpose}`),
|
|
@@ -925,13 +925,187 @@ function asBuffer2(bytes) {
|
|
|
925
925
|
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
926
926
|
}
|
|
927
927
|
|
|
928
|
-
// ../core/
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
928
|
+
// ../core/signature.ts
|
|
929
|
+
import {
|
|
930
|
+
parseDictionary,
|
|
931
|
+
serializeDictionary,
|
|
932
|
+
serializeInnerList,
|
|
933
|
+
serializeString
|
|
934
|
+
} from "structured-headers";
|
|
935
|
+
var TOLERANCE_SECS = 300;
|
|
936
|
+
var LABEL = "thunder-bridge";
|
|
937
|
+
var ALGORITHM2 = "ed25519";
|
|
934
938
|
var NONCE_BYTES = 16;
|
|
939
|
+
var SIGNATURE_BYTES2 = 64;
|
|
940
|
+
var PUBLIC_KEY = /^[0-9a-f]{64}$/i;
|
|
941
|
+
var CLIENT_SIGNS = {
|
|
942
|
+
tag: "thunder-bridge-client",
|
|
943
|
+
covers: ["@method", "@authority", "@path", "@query", "content-digest"]
|
|
944
|
+
};
|
|
945
|
+
var GATEWAY_SIGNS = {
|
|
946
|
+
tag: "thunder-bridge-gateway",
|
|
947
|
+
covers: ["@method", "@scheme", "@authority", "@path", "content-digest"]
|
|
948
|
+
};
|
|
949
|
+
function contentDigest(body) {
|
|
950
|
+
return serializeDictionary({
|
|
951
|
+
"sha-256": [Uint8Array.from(sha256(new TextEncoder().encode(body))), /* @__PURE__ */ new Map()]
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function signatureBase(message, covered) {
|
|
955
|
+
const lines = [];
|
|
956
|
+
const seen = /* @__PURE__ */ new Set();
|
|
957
|
+
for (const [name, parameters] of covered[0]) {
|
|
958
|
+
if (typeof name !== "string" || parameters.size > 0 || seen.has(name)) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
seen.add(name);
|
|
962
|
+
const value = componentValue(message, name);
|
|
963
|
+
if (value === null) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
lines.push(`${serializeString(name)}: ${value}`);
|
|
967
|
+
}
|
|
968
|
+
lines.push(`"@signature-params": ${serializeInnerList(covered)}`);
|
|
969
|
+
return lines.join("\n");
|
|
970
|
+
}
|
|
971
|
+
async function signedBy(key, profile, message, body, { created = unixNow(), nonce = randomNonce() } = {}) {
|
|
972
|
+
const digest = contentDigest(body);
|
|
973
|
+
const covered = [
|
|
974
|
+
profile.covers.map((name) => [name, /* @__PURE__ */ new Map()]),
|
|
975
|
+
/* @__PURE__ */ new Map([
|
|
976
|
+
["created", created],
|
|
977
|
+
["nonce", nonce],
|
|
978
|
+
["keyid", key.publicKeyHex],
|
|
979
|
+
["alg", ALGORITHM2],
|
|
980
|
+
["tag", profile.tag]
|
|
981
|
+
])
|
|
982
|
+
];
|
|
983
|
+
const base = signatureBase(
|
|
984
|
+
{ ...message, headers: new Headers({ "content-digest": digest }) },
|
|
985
|
+
covered
|
|
986
|
+
);
|
|
987
|
+
if (base === null) {
|
|
988
|
+
throw new Error(`${message.url} cannot be signed as ${profile.tag}`);
|
|
989
|
+
}
|
|
990
|
+
const signature = Uint8Array.from(hexToBytes(await key.sign(new TextEncoder().encode(base))));
|
|
991
|
+
return {
|
|
992
|
+
"content-digest": digest,
|
|
993
|
+
"signature-input": serializeDictionary(/* @__PURE__ */ new Map([[LABEL, covered]])),
|
|
994
|
+
signature: serializeDictionary(/* @__PURE__ */ new Map([[LABEL, [signature, /* @__PURE__ */ new Map()]]]))
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
async function signerOf(message, body, profile, toleranceSecs = TOLERANCE_SECS) {
|
|
998
|
+
const found = chosen(message.headers, profile.tag);
|
|
999
|
+
if (found === null) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
const [components, parameters] = found.covered;
|
|
1003
|
+
const named = components.map(([name]) => name);
|
|
1004
|
+
if (!profile.covers.every((name) => named.includes(name))) {
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
const keyid = parameters.get("keyid");
|
|
1008
|
+
const created = parameters.get("created");
|
|
1009
|
+
const nonce = parameters.get("nonce");
|
|
1010
|
+
const alg = parameters.get("alg");
|
|
1011
|
+
if (typeof keyid !== "string" || !PUBLIC_KEY.test(keyid)) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
if (typeof created !== "number" || !Number.isInteger(created)) {
|
|
1015
|
+
return null;
|
|
1016
|
+
}
|
|
1017
|
+
if (Math.abs(unixNow() - created) > toleranceSecs) {
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
if (alg !== void 0 && alg !== ALGORITHM2 || nonce !== void 0 && typeof nonce !== "string") {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
if (!digestMatches(message.headers.get("content-digest"), body)) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
const base = signatureBase(message, found.covered);
|
|
1027
|
+
if (base === null) {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
const signer = keyid.toLowerCase();
|
|
1031
|
+
const proven = await verifyHex(
|
|
1032
|
+
signer,
|
|
1033
|
+
bytesToHex(found.signature),
|
|
1034
|
+
new TextEncoder().encode(base)
|
|
1035
|
+
);
|
|
1036
|
+
return proven ? { keyid: signer, created, nonce: nonce ?? null } : null;
|
|
1037
|
+
}
|
|
1038
|
+
function componentValue(message, name) {
|
|
1039
|
+
let url;
|
|
1040
|
+
try {
|
|
1041
|
+
url = new URL(message.url);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
switch (name) {
|
|
1046
|
+
case "@method":
|
|
1047
|
+
return message.method;
|
|
1048
|
+
case "@authority":
|
|
1049
|
+
return (message.authority ?? url.host).toLowerCase();
|
|
1050
|
+
case "@scheme":
|
|
1051
|
+
return url.protocol.slice(0, -1).toLowerCase();
|
|
1052
|
+
case "@path":
|
|
1053
|
+
return url.pathname === "" ? "/" : url.pathname;
|
|
1054
|
+
case "@query":
|
|
1055
|
+
return url.search === "" ? "?" : url.search;
|
|
1056
|
+
default:
|
|
1057
|
+
return name.startsWith("@") ? null : message.headers.get(name)?.trim() ?? null;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function chosen(headers, tag) {
|
|
1061
|
+
const inputs = dictionaryOf(headers.get("signature-input"));
|
|
1062
|
+
const signatures = dictionaryOf(headers.get("signature"));
|
|
1063
|
+
if (inputs === null || signatures === null) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
for (const [label, member] of inputs) {
|
|
1067
|
+
if (!isInnerList(member) || member[1].get("tag") !== tag) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
const value = signatures.get(label);
|
|
1071
|
+
if (value === void 0 || isInnerList(value) || !(value[0] instanceof ArrayBuffer)) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
const signature = new Uint8Array(value[0]);
|
|
1075
|
+
return signature.length === SIGNATURE_BYTES2 ? { covered: member, signature } : null;
|
|
1076
|
+
}
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
function digestMatches(field, body) {
|
|
1080
|
+
const digest = dictionaryOf(field)?.get("sha-256");
|
|
1081
|
+
if (digest === void 0 || isInnerList(digest) || !(digest[0] instanceof ArrayBuffer)) {
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
const sent = new Uint8Array(digest[0]);
|
|
1085
|
+
const counted = sha256(new TextEncoder().encode(body));
|
|
1086
|
+
return sent.length === counted.length && sent.every((byte, at) => byte === counted[at]);
|
|
1087
|
+
}
|
|
1088
|
+
function dictionaryOf(field) {
|
|
1089
|
+
if (field === null) {
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
return parseDictionary(field);
|
|
1094
|
+
} catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function isInnerList(member) {
|
|
1099
|
+
return Array.isArray(member[0]);
|
|
1100
|
+
}
|
|
1101
|
+
function randomNonce() {
|
|
1102
|
+
return bytesToHex(crypto.getRandomValues(new Uint8Array(NONCE_BYTES)));
|
|
1103
|
+
}
|
|
1104
|
+
function unixNow() {
|
|
1105
|
+
return Math.floor(Date.now() / 1e3);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// ../core/caller.ts
|
|
935
1109
|
var SIGNING_LABEL = "request-signing";
|
|
936
1110
|
async function callerKey(secret) {
|
|
937
1111
|
return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
|
|
@@ -940,20 +1114,11 @@ function paymentNamedBy(caller, paymentHash) {
|
|
|
940
1114
|
return sha256Hex(`${caller}.payment-id.${paymentHash}`);
|
|
941
1115
|
}
|
|
942
1116
|
async function signedAs(key, method, path, body, host) {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
[NONCE_HEADER]: nonce,
|
|
949
|
-
[SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spokenTo(host, timestamp, nonce, method, path, body))}`
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
function spokenTo(host, timestamp, nonce, method, path, body) {
|
|
953
|
-
return new TextEncoder().encode(
|
|
954
|
-
["v2", host.toLowerCase(), timestamp, nonce, method.toUpperCase(), path, sha256Hex(body)].join(
|
|
955
|
-
"\n"
|
|
956
|
-
)
|
|
1117
|
+
return await signedBy(
|
|
1118
|
+
key,
|
|
1119
|
+
CLIENT_SIGNS,
|
|
1120
|
+
{ method, url: `https://${host}${path}`, authority: host },
|
|
1121
|
+
body
|
|
957
1122
|
);
|
|
958
1123
|
}
|
|
959
1124
|
|
|
@@ -1135,14 +1300,23 @@ async function addressesToReach(url) {
|
|
|
1135
1300
|
return found;
|
|
1136
1301
|
}
|
|
1137
1302
|
async function resolved(url) {
|
|
1138
|
-
const { isIP } = await import(
|
|
1303
|
+
const { isIP } = await import(
|
|
1304
|
+
/* webpackIgnore: true */
|
|
1305
|
+
"node:net"
|
|
1306
|
+
);
|
|
1139
1307
|
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
1140
1308
|
const literal = isIP(host);
|
|
1141
1309
|
if (literal !== 0) {
|
|
1142
1310
|
return [{ address: host, family: literal }];
|
|
1143
1311
|
}
|
|
1144
|
-
const { Resolver } = await import(
|
|
1145
|
-
|
|
1312
|
+
const { Resolver } = await import(
|
|
1313
|
+
/* webpackIgnore: true */
|
|
1314
|
+
"node:dns/promises"
|
|
1315
|
+
);
|
|
1316
|
+
const { setTimeout: sleep } = await import(
|
|
1317
|
+
/* webpackIgnore: true */
|
|
1318
|
+
"node:timers/promises"
|
|
1319
|
+
);
|
|
1146
1320
|
const resolver = new Resolver({ timeout: LOOKUP_TIMEOUT_MS, tries: 1 });
|
|
1147
1321
|
const found = await Promise.race([
|
|
1148
1322
|
Promise.all([
|
|
@@ -1229,6 +1403,7 @@ var NoWalletAvailable = class extends Error {
|
|
|
1229
1403
|
|
|
1230
1404
|
// ../core/lnurl.ts
|
|
1231
1405
|
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1406
|
+
var NAMES_AN_ADDRESS = ["text/identifier", "text/email"];
|
|
1232
1407
|
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
1233
1408
|
var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
1234
1409
|
var MIN_PACE_SECS = 1;
|
|
@@ -1290,6 +1465,12 @@ async function probe(send, address, amountMsat, deadline) {
|
|
|
1290
1465
|
if (pay.tag !== "payRequest" || !pay.callback) {
|
|
1291
1466
|
throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
|
|
1292
1467
|
}
|
|
1468
|
+
if (!namesTheAddress(pay.metadata, address)) {
|
|
1469
|
+
throw new WalletRefused(
|
|
1470
|
+
"cannot-prove-delivery",
|
|
1471
|
+
`${address} serves metadata that does not name it, so no invoice binds to it`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1293
1474
|
if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
|
|
1294
1475
|
throw new WalletRefused(
|
|
1295
1476
|
"amount-not-accepted",
|
|
@@ -1382,6 +1563,19 @@ function ceilingAskedFor(headers) {
|
|
|
1382
1563
|
}
|
|
1383
1564
|
return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
|
|
1384
1565
|
}
|
|
1566
|
+
function namesTheAddress(metadata, address) {
|
|
1567
|
+
if (typeof metadata !== "string") {
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
const entries = JSON.parse(metadata);
|
|
1572
|
+
return Array.isArray(entries) && entries.some(
|
|
1573
|
+
(entry) => Array.isArray(entry) && NAMES_AN_ADDRESS.includes(entry[0]) && typeof entry[1] === "string" && entry[1].toLowerCase() === address.toLowerCase()
|
|
1574
|
+
);
|
|
1575
|
+
} catch {
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1385
1579
|
function cannotReleaseAPreimage(host) {
|
|
1386
1580
|
const lowered = host.toLowerCase();
|
|
1387
1581
|
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
@@ -1430,8 +1624,14 @@ var pinnedToTheAddressWeVerified = async (url, sent, signal, at) => {
|
|
|
1430
1624
|
if (first === void 0) {
|
|
1431
1625
|
throw new Error(`${url} resolved to nothing worth connecting to`);
|
|
1432
1626
|
}
|
|
1433
|
-
const { request } = await import(
|
|
1434
|
-
|
|
1627
|
+
const { request } = await import(
|
|
1628
|
+
/* webpackIgnore: true */
|
|
1629
|
+
"node:https"
|
|
1630
|
+
);
|
|
1631
|
+
const { Readable } = await import(
|
|
1632
|
+
/* webpackIgnore: true */
|
|
1633
|
+
"node:stream"
|
|
1634
|
+
);
|
|
1435
1635
|
return new Promise((settle, fail) => {
|
|
1436
1636
|
if (signal.aborted) {
|
|
1437
1637
|
fail(signal.reason);
|
|
@@ -1496,18 +1696,9 @@ function headersOf(raw) {
|
|
|
1496
1696
|
return headers;
|
|
1497
1697
|
}
|
|
1498
1698
|
|
|
1499
|
-
// ../core/delivery.ts
|
|
1500
|
-
function deliverySigned(url, timestamp, body) {
|
|
1501
|
-
const endpoint = new URL(url);
|
|
1502
|
-
return new TextEncoder().encode(
|
|
1503
|
-
["v2", `${endpoint.origin}${endpoint.pathname}`, timestamp, body].join("\n")
|
|
1504
|
-
);
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
1699
|
// src/verify.ts
|
|
1508
1700
|
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1509
1701
|
var ISSUED_SKEW_SECS = 300;
|
|
1510
|
-
var NAMES_AN_ADDRESS = ["text/identifier", "text/email"];
|
|
1511
1702
|
var DEFAULT_WRAP_PROPORTION = 0.01;
|
|
1512
1703
|
var DEFAULT_WRAP_BASE_MSAT = 1e3;
|
|
1513
1704
|
async function proveOrigin(payment, asked, askedAt) {
|
|
@@ -1554,16 +1745,6 @@ async function proveOrigin(payment, asked, askedAt) {
|
|
|
1554
1745
|
throw cheat("invoice_settled");
|
|
1555
1746
|
}
|
|
1556
1747
|
}
|
|
1557
|
-
function namesTheAddress(metadata, address) {
|
|
1558
|
-
try {
|
|
1559
|
-
const entries = JSON.parse(metadata);
|
|
1560
|
-
return Array.isArray(entries) && entries.some(
|
|
1561
|
-
(entry) => Array.isArray(entry) && NAMES_AN_ADDRESS.includes(entry[0]) && typeof entry[1] === "string" && equalIgnoringCase(entry[1], address)
|
|
1562
|
-
);
|
|
1563
|
-
} catch {
|
|
1564
|
-
return false;
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
1748
|
async function proveSettlement(payment, asked) {
|
|
1568
1749
|
await proveOrigin(payment, asked);
|
|
1569
1750
|
const verified = await reachable(payment.verifyUrl, payment);
|
|
@@ -1587,7 +1768,6 @@ function agreesWithItself(report) {
|
|
|
1587
1768
|
}
|
|
1588
1769
|
return preimageMatchesHash(report.preimage, report.paymentHash);
|
|
1589
1770
|
}
|
|
1590
|
-
var carriesProof = agreesWithItself;
|
|
1591
1771
|
function wrapFeeCeiling(amountMsat, allowance) {
|
|
1592
1772
|
const proportional = amountMsat * (allowance?.proportion ?? DEFAULT_WRAP_PROPORTION);
|
|
1593
1773
|
return msat(Math.max(allowance?.baseMsat ?? DEFAULT_WRAP_BASE_MSAT, Math.floor(proportional)));
|
|
@@ -1869,10 +2049,6 @@ function isStatus(value) {
|
|
|
1869
2049
|
}
|
|
1870
2050
|
|
|
1871
2051
|
// src/webhook.ts
|
|
1872
|
-
var SIGNATURE_HEADER2 = "x-signature-v2";
|
|
1873
|
-
var TIMESTAMP_HEADER2 = "x-timestamp";
|
|
1874
|
-
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1875
|
-
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1876
2052
|
var CHALLENGE = "webhook-challenge";
|
|
1877
2053
|
var VERIFY_CHALLENGE = "verify-challenge";
|
|
1878
2054
|
async function readPayment(request, credential, options = {}) {
|
|
@@ -1896,24 +2072,14 @@ async function answerVerifyChallenge(request) {
|
|
|
1896
2072
|
return nonce === null ? null : answered2(nonce);
|
|
1897
2073
|
}
|
|
1898
2074
|
async function believable(request, credential, options) {
|
|
1899
|
-
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1900
|
-
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1901
|
-
if (signature === null || timestamp === null) {
|
|
1902
|
-
return null;
|
|
1903
|
-
}
|
|
1904
|
-
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) {
|
|
1905
|
-
return null;
|
|
1906
|
-
}
|
|
1907
|
-
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) {
|
|
1908
|
-
return null;
|
|
1909
|
-
}
|
|
1910
2075
|
const body = await request.clone().text();
|
|
1911
|
-
const
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
2076
|
+
const signer = await signerOf(
|
|
2077
|
+
{ method: request.method, url: options.url ?? request.url, headers: request.headers },
|
|
2078
|
+
body,
|
|
2079
|
+
GATEWAY_SIGNS,
|
|
2080
|
+
options.toleranceSecs
|
|
1915
2081
|
);
|
|
1916
|
-
return
|
|
2082
|
+
return signer !== null && signer.keyid === credential.publicKey.toLowerCase() ? body : null;
|
|
1917
2083
|
}
|
|
1918
2084
|
function proved(read) {
|
|
1919
2085
|
return read !== null && read.status === "paid" && !agreesWithItself(read) ? null : read;
|
|
@@ -1941,13 +2107,6 @@ function nonceOf2(text3, type) {
|
|
|
1941
2107
|
return null;
|
|
1942
2108
|
}
|
|
1943
2109
|
}
|
|
1944
|
-
function recent(timestamp, toleranceSecs) {
|
|
1945
|
-
const sent = Number(timestamp);
|
|
1946
|
-
if (!Number.isFinite(sent)) {
|
|
1947
|
-
return false;
|
|
1948
|
-
}
|
|
1949
|
-
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1950
|
-
}
|
|
1951
2110
|
|
|
1952
2111
|
// src/bank.ts
|
|
1953
2112
|
var DEFAULT_CURRENCY = "CZK";
|
|
@@ -2026,7 +2185,7 @@ async function answeredAt(params, subject) {
|
|
|
2026
2185
|
return polling.toString();
|
|
2027
2186
|
}
|
|
2028
2187
|
async function creditedPreimage(params) {
|
|
2029
|
-
const since =
|
|
2188
|
+
const since = unixNow2() - (params.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
|
|
2030
2189
|
const landed = (await params.statement(since)).some((credit) => pays(credit, params.asked));
|
|
2031
2190
|
if (!landed) {
|
|
2032
2191
|
return null;
|
|
@@ -2124,7 +2283,7 @@ function refuseUnusable(params, iban, currency) {
|
|
|
2124
2283
|
function accountOf(iban) {
|
|
2125
2284
|
return iban.replace(/\s+/g, "").toUpperCase();
|
|
2126
2285
|
}
|
|
2127
|
-
function
|
|
2286
|
+
function unixNow2() {
|
|
2128
2287
|
return Math.floor(Date.now() / 1e3);
|
|
2129
2288
|
}
|
|
2130
2289
|
|
|
@@ -2560,6 +2719,19 @@ function nwcRail(gateway, config) {
|
|
|
2560
2719
|
// src/relay.ts
|
|
2561
2720
|
var DEFAULT_POLL_EVERY_SECS3 = 5;
|
|
2562
2721
|
var WALLET = "w";
|
|
2722
|
+
function verifiedThrough(path) {
|
|
2723
|
+
const through = path.verifyThrough ?? null;
|
|
2724
|
+
const mints = path.gatewayMints === true;
|
|
2725
|
+
if (through !== null && mints) {
|
|
2726
|
+
throw new Error("verifyThrough and gatewayMints are two different paths, name only one");
|
|
2727
|
+
}
|
|
2728
|
+
if (through === null && !mints) {
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
"name verifyThrough, where your serve.lightningVerify is mounted, or gatewayMints: true to let the gateway mint and poll the wallet itself"
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
return through;
|
|
2734
|
+
}
|
|
2563
2735
|
function lightningVerifyEndpoint(config) {
|
|
2564
2736
|
const paced = {
|
|
2565
2737
|
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS3}`
|
|
@@ -2638,6 +2810,10 @@ function bankRail(gateway, config) {
|
|
|
2638
2810
|
};
|
|
2639
2811
|
}
|
|
2640
2812
|
function lightningRail(gateway, config) {
|
|
2813
|
+
const through = verifiedThrough(config);
|
|
2814
|
+
return through === null ? mintedByGateway(gateway, config) : resolvedHere(gateway, config, through);
|
|
2815
|
+
}
|
|
2816
|
+
function mintedByGateway(gateway, config) {
|
|
2641
2817
|
return async (order) => {
|
|
2642
2818
|
const payment = await gateway.mint(
|
|
2643
2819
|
{
|
|
@@ -2661,21 +2837,20 @@ function lightningRail(gateway, config) {
|
|
|
2661
2837
|
};
|
|
2662
2838
|
};
|
|
2663
2839
|
}
|
|
2664
|
-
function
|
|
2840
|
+
function resolvedHere(gateway, config, through) {
|
|
2665
2841
|
return async (order) => {
|
|
2666
2842
|
const resolved2 = await invoiceFrom(
|
|
2667
2843
|
config.paidTo,
|
|
2668
2844
|
await msatForOrder(order, config.amount, config.rate),
|
|
2669
2845
|
config.send
|
|
2670
2846
|
);
|
|
2671
|
-
const relay = config.relayThrough;
|
|
2672
2847
|
const watched = await gateway.watch({
|
|
2673
2848
|
paymentHash: resolved2.paymentHash,
|
|
2674
|
-
verifyUrl:
|
|
2675
|
-
|
|
2849
|
+
verifyUrl: await relayedVerifyUrl(
|
|
2850
|
+
through.endpoint,
|
|
2676
2851
|
{ url: resolved2.verifyUrl, hash: resolved2.paymentHash },
|
|
2677
|
-
|
|
2678
|
-
)
|
|
2852
|
+
through.secret
|
|
2853
|
+
),
|
|
2679
2854
|
expiresAt: resolved2.expiresAt,
|
|
2680
2855
|
trigger: config.trigger,
|
|
2681
2856
|
replay: config.replay,
|
|
@@ -2718,16 +2893,12 @@ var Rails = class {
|
|
|
2718
2893
|
this.gateway = gateway;
|
|
2719
2894
|
}
|
|
2720
2895
|
gateway;
|
|
2721
|
-
/** Lightning, with the gateway minting against a priority list of addresses */
|
|
2722
|
-
lightning(config) {
|
|
2723
|
-
return lightningRail(this.gateway, config);
|
|
2724
|
-
}
|
|
2725
2896
|
/**
|
|
2726
|
-
* Lightning
|
|
2727
|
-
*
|
|
2897
|
+
* Lightning against a priority list of addresses, verified through your
|
|
2898
|
+
* `serve.lightningVerify`, or minted by the gateway when you say `gatewayMints`
|
|
2728
2899
|
*/
|
|
2729
|
-
|
|
2730
|
-
return
|
|
2900
|
+
lightning(config) {
|
|
2901
|
+
return lightningRail(this.gateway, config);
|
|
2731
2902
|
}
|
|
2732
2903
|
/** A bank transfer, proved the way a Lightning payment is */
|
|
2733
2904
|
bank(config) {
|
|
@@ -2793,11 +2964,12 @@ function waitingOf(options) {
|
|
|
2793
2964
|
// src/trigger.ts
|
|
2794
2965
|
var NONCE_BYTES3 = 16;
|
|
2795
2966
|
function lnurlPayEndpoint(gateway, config) {
|
|
2967
|
+
const through = verifiedThrough(config);
|
|
2796
2968
|
return async (request) => {
|
|
2797
2969
|
const url = new URL(request.url);
|
|
2798
2970
|
const asked = url.searchParams.get("to");
|
|
2799
2971
|
try {
|
|
2800
|
-
return asked === null ? await offer(gateway, config, url) : await mint(gateway, config, url, asked);
|
|
2972
|
+
return asked === null ? await offer(gateway, config, through, url) : await mint(gateway, config, through, url, asked);
|
|
2801
2973
|
} catch (failure) {
|
|
2802
2974
|
return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
|
|
2803
2975
|
}
|
|
@@ -2815,10 +2987,10 @@ function watchTicketEndpoint(gateway, config) {
|
|
|
2815
2987
|
function publicWatchTicketEndpoint(gateway, config) {
|
|
2816
2988
|
return () => issue(gateway, config);
|
|
2817
2989
|
}
|
|
2818
|
-
async function offer(gateway, config, url) {
|
|
2990
|
+
async function offer(gateway, config, through, url) {
|
|
2819
2991
|
const { least, most } = await spread(config.amount);
|
|
2820
|
-
const winner = await quoted(gateway, config, least);
|
|
2821
|
-
const nonce =
|
|
2992
|
+
const winner = await quoted(gateway, config, through, least);
|
|
2993
|
+
const nonce = randomNonce2();
|
|
2822
2994
|
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
2823
2995
|
callback.searchParams.set("to", winner.address);
|
|
2824
2996
|
callback.searchParams.set("least", String(least));
|
|
@@ -2833,8 +3005,8 @@ async function offer(gateway, config, url) {
|
|
|
2833
3005
|
maxSendable: most
|
|
2834
3006
|
});
|
|
2835
3007
|
}
|
|
2836
|
-
async function quoted(gateway, config, amountMsat) {
|
|
2837
|
-
if (
|
|
3008
|
+
async function quoted(gateway, config, through, amountMsat) {
|
|
3009
|
+
if (through !== null) {
|
|
2838
3010
|
const { won } = await quote(
|
|
2839
3011
|
config.send ?? pinnedToTheAddressWeVerified,
|
|
2840
3012
|
listed(config.paidTo),
|
|
@@ -2860,7 +3032,7 @@ async function spread(amount) {
|
|
|
2860
3032
|
}
|
|
2861
3033
|
return { least, most };
|
|
2862
3034
|
}
|
|
2863
|
-
async function mint(gateway, config, url, address) {
|
|
3035
|
+
async function mint(gateway, config, through, url, address) {
|
|
2864
3036
|
const least = Number(url.searchParams.get("least"));
|
|
2865
3037
|
const most = Number(url.searchParams.get("most"));
|
|
2866
3038
|
const nonce = url.searchParams.get("n") ?? "";
|
|
@@ -2879,7 +3051,7 @@ async function mint(gateway, config, url, address) {
|
|
|
2879
3051
|
least === most ? `this trigger costs exactly ${least} msat` : `this trigger takes between ${least} and ${most} msat`
|
|
2880
3052
|
);
|
|
2881
3053
|
}
|
|
2882
|
-
const minted =
|
|
3054
|
+
const minted = through === null ? await mintThroughGateway(gateway, config, address, amountMsat, nonce) : await mintHere(gateway, config, through, address, amountMsat);
|
|
2883
3055
|
return Response.json({
|
|
2884
3056
|
status: "OK",
|
|
2885
3057
|
pr: minted.bolt11,
|
|
@@ -2894,7 +3066,7 @@ async function mintThroughGateway(gateway, config, address, amountMsat, nonce) {
|
|
|
2894
3066
|
);
|
|
2895
3067
|
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
2896
3068
|
}
|
|
2897
|
-
async function
|
|
3069
|
+
async function mintHere(gateway, config, through, address, amountMsat) {
|
|
2898
3070
|
const resolved2 = await resolve(
|
|
2899
3071
|
config.send ?? pinnedToTheAddressWeVerified,
|
|
2900
3072
|
[address],
|
|
@@ -2902,15 +3074,14 @@ async function mintBlind(gateway, config, address, amountMsat) {
|
|
|
2902
3074
|
);
|
|
2903
3075
|
const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
|
|
2904
3076
|
const locked = config.sealed;
|
|
2905
|
-
const relay = config.relayThrough;
|
|
2906
3077
|
try {
|
|
2907
3078
|
await gateway.watch({
|
|
2908
3079
|
paymentHash: resolved2.paymentHash,
|
|
2909
|
-
verifyUrl:
|
|
2910
|
-
|
|
3080
|
+
verifyUrl: await relayedVerifyUrl(
|
|
3081
|
+
through.endpoint,
|
|
2911
3082
|
{ url: resolved2.verifyUrl, hash: resolved2.paymentHash },
|
|
2912
|
-
|
|
2913
|
-
)
|
|
3083
|
+
through.secret
|
|
3084
|
+
),
|
|
2914
3085
|
expiresAt: resolved2.expiresAt,
|
|
2915
3086
|
trigger: config.watchSecret,
|
|
2916
3087
|
replay: config.replay,
|
|
@@ -2932,7 +3103,7 @@ function alreadyWatched(refused) {
|
|
|
2932
3103
|
function sign(secret, address, least, most, nonce) {
|
|
2933
3104
|
return hmacHex(secret, `lnurl-callback|${address}|${least}|${most}|${nonce}`);
|
|
2934
3105
|
}
|
|
2935
|
-
function
|
|
3106
|
+
function randomNonce2() {
|
|
2936
3107
|
const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES3));
|
|
2937
3108
|
let hex = "";
|
|
2938
3109
|
for (const byte of bytes) {
|
|
@@ -2986,14 +3157,6 @@ var Serve = class {
|
|
|
2986
3157
|
lightningVerify(config) {
|
|
2987
3158
|
return lightningVerifyEndpoint(config);
|
|
2988
3159
|
}
|
|
2989
|
-
/**
|
|
2990
|
-
* What `lightningVerify` was called before 2.2.0
|
|
2991
|
-
*
|
|
2992
|
-
* @deprecated Use `lightningVerify`, beside `bankVerify` and `nwcVerify`
|
|
2993
|
-
*/
|
|
2994
|
-
verify(config) {
|
|
2995
|
-
return this.lightningVerify(config);
|
|
2996
|
-
}
|
|
2997
3160
|
/** The verify endpoint a bank rail is polled at, answering off your own statement */
|
|
2998
3161
|
bankVerify(config) {
|
|
2999
3162
|
return bankVerifyEndpoint(config);
|
|
@@ -3015,7 +3178,7 @@ var Serve = class {
|
|
|
3015
3178
|
*/
|
|
3016
3179
|
webhook(handlers) {
|
|
3017
3180
|
const options = { toleranceSecs: handlers.toleranceSecs, url: handlers.url };
|
|
3018
|
-
const once = onceWhileReplayable(handlers.toleranceSecs ??
|
|
3181
|
+
const once = onceWhileReplayable(handlers.toleranceSecs ?? TOLERANCE_SECS);
|
|
3019
3182
|
return async (request) => {
|
|
3020
3183
|
const credential = await this.credential(handlers);
|
|
3021
3184
|
const challenge = await answerWebhookChallenge(request, credential, options);
|
|
@@ -3865,7 +4028,6 @@ export {
|
|
|
3865
4028
|
WrapRefusedError,
|
|
3866
4029
|
agreesWithItself,
|
|
3867
4030
|
answerVerifyChallenge,
|
|
3868
|
-
carriesProof,
|
|
3869
4031
|
decodeInvoice,
|
|
3870
4032
|
fiat,
|
|
3871
4033
|
invoiceFrom,
|