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.cjs
CHANGED
|
@@ -30,6 +30,7 @@ __export(index_exports, {
|
|
|
30
30
|
REQUEST_IN_FLIGHT: () => REQUEST_IN_FLIGHT,
|
|
31
31
|
ThunderBridge: () => ThunderBridge,
|
|
32
32
|
UnverifiedRecipientError: () => UnverifiedRecipientError,
|
|
33
|
+
bankRail: () => bankRail,
|
|
33
34
|
bankTransfer: () => bankTransfer,
|
|
34
35
|
bankVerifyEndpoint: () => bankVerifyEndpoint,
|
|
35
36
|
bitstamp: () => bitstamp,
|
|
@@ -42,7 +43,7 @@ __export(index_exports, {
|
|
|
42
43
|
isProblemType: () => isProblemType,
|
|
43
44
|
isProvablyPaid: () => isProvablyPaid,
|
|
44
45
|
kraken: () => kraken,
|
|
45
|
-
|
|
46
|
+
lightningRail: () => lightningRail,
|
|
46
47
|
lnurlToDataUrl: () => lnurlToDataUrl,
|
|
47
48
|
lnurlToSvg: () => lnurlToSvg,
|
|
48
49
|
medianOf: () => medianOf,
|
|
@@ -54,6 +55,8 @@ __export(index_exports, {
|
|
|
54
55
|
preimageMatchesHash: () => preimageMatchesHash,
|
|
55
56
|
proveOrigin: () => proveOrigin,
|
|
56
57
|
proveSettlement: () => proveSettlement,
|
|
58
|
+
qrToDataUrl: () => qrToDataUrl,
|
|
59
|
+
qrToSvg: () => qrToSvg,
|
|
57
60
|
seal: () => seal,
|
|
58
61
|
spdToDataUrl: () => spdToDataUrl,
|
|
59
62
|
spdToSvg: () => spdToSvg,
|
|
@@ -99,10 +102,8 @@ var ProblemError = class extends Error {
|
|
|
99
102
|
this.detail = problem.detail ?? null;
|
|
100
103
|
}
|
|
101
104
|
};
|
|
102
|
-
var NAMESPACE = "urn:problem-type:thunder-bridge:";
|
|
103
|
-
var NAMESPACE_BEFORE_THE_RENAME = "urn:problem-type:thunder-bridge-direct:";
|
|
104
105
|
function isProblemType(problem, type) {
|
|
105
|
-
return problem.type === type
|
|
106
|
+
return problem.type === type;
|
|
106
107
|
}
|
|
107
108
|
var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
|
|
108
109
|
var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
|
|
@@ -413,10 +414,13 @@ function sameOrigin(one, other) {
|
|
|
413
414
|
return false;
|
|
414
415
|
}
|
|
415
416
|
}
|
|
417
|
+
function publicAddress(literal) {
|
|
418
|
+
return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
|
|
419
|
+
}
|
|
416
420
|
function hostIsPublic(host) {
|
|
417
|
-
if (host.startsWith("[")) return
|
|
421
|
+
if (host.startsWith("[")) return publicAddress(host.slice(1, -1));
|
|
418
422
|
const name = host.replace(/\.$/, "");
|
|
419
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return
|
|
423
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return publicAddress(name);
|
|
420
424
|
const labels = name.split(".");
|
|
421
425
|
if (labels.length < 2 || labels.some((label) => label === "")) return false;
|
|
422
426
|
return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
|
|
@@ -681,6 +685,16 @@ var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
|
|
|
681
685
|
var RECONNECT_DELAY_MS = 3e3;
|
|
682
686
|
var RECONNECT_CAP_MS = 3e4;
|
|
683
687
|
var UNANSWERED_ATTEMPTS = 5;
|
|
688
|
+
var TRIGGER_MIN_CHARS = 16;
|
|
689
|
+
var STRANGER_PROBE = "is-this-gateway-yours";
|
|
690
|
+
function unguessable(trigger) {
|
|
691
|
+
if (trigger.length < TRIGGER_MIN_CHARS) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`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`
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
return trigger;
|
|
697
|
+
}
|
|
684
698
|
function backoffMs(firstDelay, attempt) {
|
|
685
699
|
const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
|
|
686
700
|
return Math.round(grown * (0.5 + Math.random() / 2));
|
|
@@ -689,6 +703,7 @@ var ThunderBridge = class {
|
|
|
689
703
|
baseUrl;
|
|
690
704
|
verify;
|
|
691
705
|
token;
|
|
706
|
+
strangers = null;
|
|
692
707
|
constructor(baseUrl, options) {
|
|
693
708
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
694
709
|
this.verify = options?.verify ?? true;
|
|
@@ -702,6 +717,21 @@ var ThunderBridge = class {
|
|
|
702
717
|
get isPrivate() {
|
|
703
718
|
return this.token !== null;
|
|
704
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Whether the gateway turns away a caller carrying no token, asked by making
|
|
722
|
+
* one unauthenticated read it would have to refuse. `isPrivate` answers only
|
|
723
|
+
* whether you configured a token, which is your side of the arrangement and
|
|
724
|
+
* says nothing about the gateway's, so a made-up token against a public
|
|
725
|
+
* instance reads as private and is not. Asked once and remembered, because an
|
|
726
|
+
* instance does not change its mind. Anything other than a refusal counts as
|
|
727
|
+
* open, so an unreachable gateway fails closed
|
|
728
|
+
*/
|
|
729
|
+
async refusesStrangers() {
|
|
730
|
+
this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
|
|
731
|
+
headers: { accept: "application/json" }
|
|
732
|
+
}).then((answer) => answer.status === 401).catch(() => false);
|
|
733
|
+
return await this.strangers;
|
|
734
|
+
}
|
|
705
735
|
/**
|
|
706
736
|
* Ask the gateway for an invoice payable to the first address on your list
|
|
707
737
|
* that can issue a provable one, throws `NoWalletAvailableError` when none can
|
|
@@ -713,7 +743,10 @@ var ThunderBridge = class {
|
|
|
713
743
|
const response = await fetch(`${this.baseUrl}/incoming-payments`, {
|
|
714
744
|
method: "POST",
|
|
715
745
|
headers,
|
|
716
|
-
body: createRequestBody(
|
|
746
|
+
body: createRequestBody(
|
|
747
|
+
params,
|
|
748
|
+
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
|
|
749
|
+
)
|
|
717
750
|
});
|
|
718
751
|
if (!response.ok) throw await problemFrom(response);
|
|
719
752
|
const payment = await paymentFrom(response);
|
|
@@ -955,7 +988,10 @@ var ThunderBridge = class {
|
|
|
955
988
|
const response = await fetch(`${this.baseUrl}/watched-payments`, {
|
|
956
989
|
method: "POST",
|
|
957
990
|
headers: this.sending(),
|
|
958
|
-
body: watchRequestBody(
|
|
991
|
+
body: watchRequestBody(
|
|
992
|
+
params,
|
|
993
|
+
params.trigger ? sha256Hex(unguessable(params.trigger)) : null
|
|
994
|
+
)
|
|
959
995
|
});
|
|
960
996
|
if (!response.ok) throw await problemFrom(response);
|
|
961
997
|
const watched = triggerEventFromWire(await response.json().catch(() => null));
|
|
@@ -974,7 +1010,7 @@ var ThunderBridge = class {
|
|
|
974
1010
|
*/
|
|
975
1011
|
followTrigger(secret, options) {
|
|
976
1012
|
const base = this.baseUrl.replace(/^http/, "ws");
|
|
977
|
-
const direct = `${base}/ws/triggers/${encodeURIComponent(secret)}`;
|
|
1013
|
+
const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}`;
|
|
978
1014
|
const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
|
|
979
1015
|
let socket = null;
|
|
980
1016
|
let retry;
|
|
@@ -1181,290 +1217,6 @@ function asBuffer(bytes) {
|
|
|
1181
1217
|
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1182
1218
|
}
|
|
1183
1219
|
|
|
1184
|
-
// ../core/refusal.ts
|
|
1185
|
-
var WalletRefused = class extends Error {
|
|
1186
|
-
reason;
|
|
1187
|
-
constructor(reason, message) {
|
|
1188
|
-
super(message);
|
|
1189
|
-
this.reason = reason;
|
|
1190
|
-
}
|
|
1191
|
-
};
|
|
1192
|
-
var NoWalletAvailable = class extends Error {
|
|
1193
|
-
wallets;
|
|
1194
|
-
constructor(wallets) {
|
|
1195
|
-
super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
|
|
1196
|
-
this.wallets = wallets;
|
|
1197
|
-
}
|
|
1198
|
-
};
|
|
1199
|
-
|
|
1200
|
-
// ../core/lnurl.ts
|
|
1201
|
-
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
1202
|
-
var HTTP_TIMEOUT_MS2 = 15e3;
|
|
1203
|
-
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1204
|
-
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1205
|
-
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1206
|
-
var LNURL_HRP = "lnurl";
|
|
1207
|
-
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
1208
|
-
async function resolve(addresses, amountMsat) {
|
|
1209
|
-
const served = await firstThatServes(
|
|
1210
|
-
addresses,
|
|
1211
|
-
(address, deadline) => resolveAddress(address, amountMsat, deadline)
|
|
1212
|
-
);
|
|
1213
|
-
return served.won;
|
|
1214
|
-
}
|
|
1215
|
-
async function firstThatServes(addresses, attempt) {
|
|
1216
|
-
const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
|
|
1217
|
-
const refusals3 = [];
|
|
1218
|
-
for (const address of addresses) {
|
|
1219
|
-
if (deadline.aborted) break;
|
|
1220
|
-
try {
|
|
1221
|
-
return { won: await attempt(address, deadline), refusals: refusals3 };
|
|
1222
|
-
} catch (refusal) {
|
|
1223
|
-
if (!(refusal instanceof WalletRefused)) throw refusal;
|
|
1224
|
-
console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
|
|
1225
|
-
refusals3.push({ address, reason: refusal.reason });
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
throw new NoWalletAvailable(refusals3);
|
|
1229
|
-
}
|
|
1230
|
-
async function probe(address, amountMsat, deadline) {
|
|
1231
|
-
const [user, domain] = splitAddress(address);
|
|
1232
|
-
if (cannotReleaseAPreimage(domain)) {
|
|
1233
|
-
throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
|
|
1234
|
-
}
|
|
1235
|
-
const pay = await answered(
|
|
1236
|
-
`https://${domain}/.well-known/lnurlp/${user}`,
|
|
1237
|
-
deadline
|
|
1238
|
-
);
|
|
1239
|
-
if (pay.tag !== "payRequest" || !pay.callback) {
|
|
1240
|
-
throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
|
|
1241
|
-
}
|
|
1242
|
-
if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
|
|
1243
|
-
throw new WalletRefused(
|
|
1244
|
-
"amount-not-accepted",
|
|
1245
|
-
`${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
1248
|
-
return { address, pay };
|
|
1249
|
-
}
|
|
1250
|
-
async function resolveAddress(address, amountMsat, deadline) {
|
|
1251
|
-
const { pay } = await probe(address, amountMsat, deadline);
|
|
1252
|
-
const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
|
|
1253
|
-
if (!invoice.pr) throw new WalletRefused("unreachable", `${address} returned no invoice`);
|
|
1254
|
-
if (!invoice.verify || !publicHttps(invoice.verify)) {
|
|
1255
|
-
throw new WalletRefused(
|
|
1256
|
-
"cannot-prove-delivery",
|
|
1257
|
-
`${address} publishes no usable LUD-21 verify URL`
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1260
|
-
const issued = decodeIssued(address, invoice.pr);
|
|
1261
|
-
if (issued.amountMsat !== amountMsat) {
|
|
1262
|
-
throw new WalletRefused(
|
|
1263
|
-
"invoice-refused",
|
|
1264
|
-
`${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
|
|
1268
|
-
throw new WalletRefused(
|
|
1269
|
-
"invoice-refused",
|
|
1270
|
-
`${address} does not bind its invoice to the metadata it serves`
|
|
1271
|
-
);
|
|
1272
|
-
}
|
|
1273
|
-
return {
|
|
1274
|
-
address,
|
|
1275
|
-
bolt11: invoice.pr,
|
|
1276
|
-
verifyUrl: invoice.verify,
|
|
1277
|
-
paymentHash: issued.paymentHash,
|
|
1278
|
-
expiresAt: issued.expiresAt
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
async function answered(url, deadline) {
|
|
1282
|
-
try {
|
|
1283
|
-
return await fetchJson(url, deadline);
|
|
1284
|
-
} catch (cause) {
|
|
1285
|
-
throw new WalletRefused("unreachable", String(cause));
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
function decodeIssued(address, bolt11) {
|
|
1289
|
-
const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
|
|
1290
|
-
if (paymentHash === null || expiresAt === null) {
|
|
1291
|
-
throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
|
|
1292
|
-
}
|
|
1293
|
-
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
1294
|
-
}
|
|
1295
|
-
function cannotReleaseAPreimage(host) {
|
|
1296
|
-
const lowered = host.toLowerCase();
|
|
1297
|
-
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
1298
|
-
(known) => lowered === known || lowered.endsWith(`.${known}`)
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
function toLnurl(endpoint) {
|
|
1302
|
-
const scheme = parsedScheme(endpoint);
|
|
1303
|
-
if (scheme !== "https:" && scheme !== "http:") {
|
|
1304
|
-
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1305
|
-
}
|
|
1306
|
-
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1307
|
-
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1308
|
-
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1309
|
-
}
|
|
1310
|
-
function parsedScheme(endpoint) {
|
|
1311
|
-
try {
|
|
1312
|
-
return new URL(endpoint).protocol;
|
|
1313
|
-
} catch {
|
|
1314
|
-
return null;
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
function toWords(bytes) {
|
|
1318
|
-
const words = [];
|
|
1319
|
-
let accumulator = 0;
|
|
1320
|
-
let bits = 0;
|
|
1321
|
-
for (const byte of bytes) {
|
|
1322
|
-
accumulator = accumulator << 8 | byte;
|
|
1323
|
-
bits += 8;
|
|
1324
|
-
while (bits >= 5) {
|
|
1325
|
-
bits -= 5;
|
|
1326
|
-
words.push(accumulator >> bits & 31);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1330
|
-
return words;
|
|
1331
|
-
}
|
|
1332
|
-
function checksumWords(hrp, words) {
|
|
1333
|
-
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1334
|
-
const values = [
|
|
1335
|
-
...expanded.map((code) => code >> 5),
|
|
1336
|
-
0,
|
|
1337
|
-
...expanded.map((code) => code & 31),
|
|
1338
|
-
...words,
|
|
1339
|
-
...CHECKSUM_SLOTS.map(() => 0)
|
|
1340
|
-
];
|
|
1341
|
-
const polymod = bech32Polymod(values) ^ 1;
|
|
1342
|
-
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1343
|
-
}
|
|
1344
|
-
function bech32Polymod(values) {
|
|
1345
|
-
let checksum = 1;
|
|
1346
|
-
for (const value of values) {
|
|
1347
|
-
const top = checksum >> 25;
|
|
1348
|
-
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1349
|
-
for (let bit = 0; bit < 5; bit++) {
|
|
1350
|
-
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
return checksum;
|
|
1354
|
-
}
|
|
1355
|
-
function withAmount(callback, amountMsat) {
|
|
1356
|
-
const separator = callback.includes("?") ? "&" : "?";
|
|
1357
|
-
return `${callback}${separator}amount=${amountMsat}`;
|
|
1358
|
-
}
|
|
1359
|
-
function splitAddress(address) {
|
|
1360
|
-
const [user, domain, ...rest] = address.split("@");
|
|
1361
|
-
if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
|
|
1362
|
-
throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
|
|
1363
|
-
}
|
|
1364
|
-
return [user, domain];
|
|
1365
|
-
}
|
|
1366
|
-
async function fetchJson(url, deadline) {
|
|
1367
|
-
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
1368
|
-
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
|
|
1369
|
-
const response = await fetch(url, {
|
|
1370
|
-
signal: deadline ? AbortSignal.any([deadline, capped]) : capped,
|
|
1371
|
-
headers: { accept: "application/json" }
|
|
1372
|
-
});
|
|
1373
|
-
if (!response.ok) throw new Error(`${url} answered ${response.status}`);
|
|
1374
|
-
return await response.json();
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// src/trigger.ts
|
|
1378
|
-
var NONCE_BYTES = 16;
|
|
1379
|
-
function lnurlPayEndpoint(config) {
|
|
1380
|
-
return async (request) => {
|
|
1381
|
-
const url = new URL(request.url);
|
|
1382
|
-
const asked = url.searchParams.get("to");
|
|
1383
|
-
try {
|
|
1384
|
-
return asked === null ? await offer(config, url) : await mint(config, url, asked);
|
|
1385
|
-
} catch (failure) {
|
|
1386
|
-
return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
|
|
1387
|
-
}
|
|
1388
|
-
};
|
|
1389
|
-
}
|
|
1390
|
-
async function offer(config, url) {
|
|
1391
|
-
const amountMsat = await config.amountMsat();
|
|
1392
|
-
const quote = await config.gateway.createQuote({
|
|
1393
|
-
lnAddresses: config.lnAddresses,
|
|
1394
|
-
amountMsat
|
|
1395
|
-
});
|
|
1396
|
-
const nonce = randomNonce();
|
|
1397
|
-
const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
|
|
1398
|
-
callback.searchParams.set("to", quote.lnAddress);
|
|
1399
|
-
callback.searchParams.set("msat", String(amountMsat));
|
|
1400
|
-
callback.searchParams.set("n", nonce);
|
|
1401
|
-
callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
|
|
1402
|
-
return Response.json({
|
|
1403
|
-
tag: "payRequest",
|
|
1404
|
-
callback: callback.toString(),
|
|
1405
|
-
metadata: quote.metadata,
|
|
1406
|
-
minSendable: amountMsat,
|
|
1407
|
-
maxSendable: amountMsat
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
1410
|
-
async function mint(config, url, address) {
|
|
1411
|
-
const amountMsat = Number(url.searchParams.get("msat"));
|
|
1412
|
-
const nonce = url.searchParams.get("n") ?? "";
|
|
1413
|
-
const signature = url.searchParams.get("sig") ?? "";
|
|
1414
|
-
const expected = await sign(config.secret, address, amountMsat, nonce);
|
|
1415
|
-
if (!equalInConstantTime(signature, expected)) return refuse("this callback was not signed here");
|
|
1416
|
-
const wanted = url.searchParams.get("amount");
|
|
1417
|
-
if (wanted !== null && Number(wanted) !== amountMsat) {
|
|
1418
|
-
return refuse(`this trigger costs exactly ${amountMsat} msat`);
|
|
1419
|
-
}
|
|
1420
|
-
const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
|
|
1421
|
-
return Response.json({
|
|
1422
|
-
status: "OK",
|
|
1423
|
-
pr: minted.bolt11,
|
|
1424
|
-
routes: [],
|
|
1425
|
-
verify: minted.verifyUrl
|
|
1426
|
-
});
|
|
1427
|
-
}
|
|
1428
|
-
async function mintThroughGateway(config, address, amountMsat, nonce) {
|
|
1429
|
-
const payment = await config.gateway.createPayment(
|
|
1430
|
-
{ lnAddresses: [address], amountMsat },
|
|
1431
|
-
{ idempotencyKey: nonce, trigger: config.watchSecret }
|
|
1432
|
-
);
|
|
1433
|
-
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
1434
|
-
}
|
|
1435
|
-
async function mintBlind(config, address, amountMsat) {
|
|
1436
|
-
const resolved = await resolve([address], amountMsat);
|
|
1437
|
-
const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
|
|
1438
|
-
const locked = config.sealed;
|
|
1439
|
-
try {
|
|
1440
|
-
await config.gateway.watchPayment({
|
|
1441
|
-
paymentHash: resolved.paymentHash,
|
|
1442
|
-
verifyUrl: resolved.verifyUrl,
|
|
1443
|
-
expiresAt: resolved.expiresAt,
|
|
1444
|
-
trigger: config.watchSecret,
|
|
1445
|
-
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
1446
|
-
});
|
|
1447
|
-
} catch (refused) {
|
|
1448
|
-
if (!alreadyWatched(refused)) throw refused;
|
|
1449
|
-
}
|
|
1450
|
-
return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
|
|
1451
|
-
}
|
|
1452
|
-
function alreadyWatched(refused) {
|
|
1453
|
-
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
1454
|
-
}
|
|
1455
|
-
function sign(secret, address, amountMsat, nonce) {
|
|
1456
|
-
return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
|
|
1457
|
-
}
|
|
1458
|
-
function randomNonce() {
|
|
1459
|
-
const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
1460
|
-
let hex = "";
|
|
1461
|
-
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
1462
|
-
return hex;
|
|
1463
|
-
}
|
|
1464
|
-
function refuse(reason) {
|
|
1465
|
-
return Response.json({ status: "ERROR", reason });
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
1220
|
// src/currency.ts
|
|
1469
1221
|
var ISO_4217_MINOR_UNITS = {
|
|
1470
1222
|
BHD: 3,
|
|
@@ -1515,6 +1267,7 @@ var FORBIDDEN_IN_SPD = "*";
|
|
|
1515
1267
|
async function bankTransfer(params) {
|
|
1516
1268
|
const currency = params.currency ?? DEFAULT_CURRENCY;
|
|
1517
1269
|
refuseUnusable(params, currency);
|
|
1270
|
+
await refuseAnOpenGateway(params);
|
|
1518
1271
|
const spd = shortPaymentDescriptor(params, currency);
|
|
1519
1272
|
const subject = subjectOf(params.reference, params.amountMinor, currency);
|
|
1520
1273
|
const polling = new URL(params.verifyUrl);
|
|
@@ -1581,12 +1334,14 @@ function shortPaymentDescriptor(params, currency) {
|
|
|
1581
1334
|
function major(amountMinor, currency) {
|
|
1582
1335
|
return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
|
|
1583
1336
|
}
|
|
1337
|
+
async function refuseAnOpenGateway(params) {
|
|
1338
|
+
if (params.allowPublicGateway === true) return;
|
|
1339
|
+
if (await params.gateway.refusesStrangers()) return;
|
|
1340
|
+
throw new Error(
|
|
1341
|
+
"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"
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1584
1344
|
function refuseUnusable(params, currency) {
|
|
1585
|
-
if (!params.gateway.isPrivate && params.allowPublicGateway !== true) {
|
|
1586
|
-
throw new Error(
|
|
1587
|
-
"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"
|
|
1588
|
-
);
|
|
1589
|
-
}
|
|
1590
1345
|
if (!IBAN.test(params.iban)) throw new Error(`${params.iban} is not an IBAN`);
|
|
1591
1346
|
if (!Number.isInteger(params.amountMinor) || params.amountMinor <= 0) {
|
|
1592
1347
|
throw new Error("amountMinor must be a whole number of minor units above zero");
|
|
@@ -1625,6 +1380,7 @@ var WHERE_A_PAYER_WRITES = [
|
|
|
1625
1380
|
USER_IDENTIFICATION,
|
|
1626
1381
|
PAYER_REFERENCE
|
|
1627
1382
|
];
|
|
1383
|
+
var UTC_OFFSET = /^[+-]\d{4}$/;
|
|
1628
1384
|
function fioStatement(config) {
|
|
1629
1385
|
const named = typeof config.token === "string" ? [config.token] : config.token;
|
|
1630
1386
|
if (named.length === 0) throw new Error("fioStatement needs at least one token to read with");
|
|
@@ -1677,9 +1433,21 @@ function asCredit(transaction) {
|
|
|
1677
1433
|
amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
|
|
1678
1434
|
currency: textIn(transaction[CURRENCY]),
|
|
1679
1435
|
reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
|
|
1680
|
-
bookedAt:
|
|
1436
|
+
bookedAt: bookedAtIn(transaction[BOOKED_AT])
|
|
1681
1437
|
};
|
|
1682
1438
|
}
|
|
1439
|
+
function bookedAtIn(cell) {
|
|
1440
|
+
const value = cell?.value;
|
|
1441
|
+
if (typeof value === "number") return Math.floor(value / MILLIS);
|
|
1442
|
+
const at = Date.parse(asIso8601(textIn(cell)));
|
|
1443
|
+
return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
|
|
1444
|
+
}
|
|
1445
|
+
function asIso8601(booked) {
|
|
1446
|
+
const day = booked.slice(0, DATE_CHARS);
|
|
1447
|
+
const zone = booked.slice(DATE_CHARS);
|
|
1448
|
+
if (!UTC_OFFSET.test(zone)) return `${day}T00:00:00Z`;
|
|
1449
|
+
return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
|
|
1450
|
+
}
|
|
1683
1451
|
function numberIn(cell) {
|
|
1684
1452
|
const value = Number(cell?.value ?? Number.NaN);
|
|
1685
1453
|
return Number.isFinite(value) ? value : 0;
|
|
@@ -1692,6 +1460,180 @@ function asDate(unix) {
|
|
|
1692
1460
|
return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
|
|
1693
1461
|
}
|
|
1694
1462
|
|
|
1463
|
+
// ../core/lnurl.ts
|
|
1464
|
+
var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
1465
|
+
var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
|
|
1466
|
+
var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
|
|
1467
|
+
var LNURL_HRP = "lnurl";
|
|
1468
|
+
function toLnurl(endpoint) {
|
|
1469
|
+
const scheme = parsedScheme(endpoint);
|
|
1470
|
+
if (scheme !== "https:" && scheme !== "http:") {
|
|
1471
|
+
throw new Error(`${endpoint} is not an http or https URL`);
|
|
1472
|
+
}
|
|
1473
|
+
const words = toWords(new TextEncoder().encode(endpoint));
|
|
1474
|
+
const data = [...words, ...checksumWords(LNURL_HRP, words)];
|
|
1475
|
+
return `${LNURL_HRP}1${data.map((word) => BECH32_CHARSET2[word]).join("")}`.toUpperCase();
|
|
1476
|
+
}
|
|
1477
|
+
function parsedScheme(endpoint) {
|
|
1478
|
+
try {
|
|
1479
|
+
return new URL(endpoint).protocol;
|
|
1480
|
+
} catch {
|
|
1481
|
+
return null;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
function toWords(bytes) {
|
|
1485
|
+
const words = [];
|
|
1486
|
+
let accumulator = 0;
|
|
1487
|
+
let bits = 0;
|
|
1488
|
+
for (const byte of bytes) {
|
|
1489
|
+
accumulator = accumulator << 8 | byte;
|
|
1490
|
+
bits += 8;
|
|
1491
|
+
while (bits >= 5) {
|
|
1492
|
+
bits -= 5;
|
|
1493
|
+
words.push(accumulator >> bits & 31);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (bits > 0) words.push(accumulator << 5 - bits & 31);
|
|
1497
|
+
return words;
|
|
1498
|
+
}
|
|
1499
|
+
function checksumWords(hrp, words) {
|
|
1500
|
+
const expanded = [...hrp].map((char) => char.charCodeAt(0));
|
|
1501
|
+
const values = [
|
|
1502
|
+
...expanded.map((code) => code >> 5),
|
|
1503
|
+
0,
|
|
1504
|
+
...expanded.map((code) => code & 31),
|
|
1505
|
+
...words,
|
|
1506
|
+
...CHECKSUM_SLOTS.map(() => 0)
|
|
1507
|
+
];
|
|
1508
|
+
const polymod = bech32Polymod(values) ^ 1;
|
|
1509
|
+
return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
|
|
1510
|
+
}
|
|
1511
|
+
function bech32Polymod(values) {
|
|
1512
|
+
let checksum = 1;
|
|
1513
|
+
for (const value of values) {
|
|
1514
|
+
const top = checksum >> 25;
|
|
1515
|
+
checksum = (checksum & 33554431) << 5 ^ value;
|
|
1516
|
+
for (let bit = 0; bit < 5; bit++) {
|
|
1517
|
+
if (top >> bit & 1) checksum ^= BECH32_GENERATOR[bit];
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return checksum;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/qr.ts
|
|
1524
|
+
var import_uqr = require("uqr");
|
|
1525
|
+
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
1526
|
+
function encodeForQr(destination) {
|
|
1527
|
+
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1528
|
+
return `LIGHTNING:${scanned}`;
|
|
1529
|
+
}
|
|
1530
|
+
function invoiceToSvg(destination, options) {
|
|
1531
|
+
return svgOf(encodeForQr(destination), options);
|
|
1532
|
+
}
|
|
1533
|
+
function invoiceToDataUrl(destination, options) {
|
|
1534
|
+
return asDataUrl(invoiceToSvg(destination, options));
|
|
1535
|
+
}
|
|
1536
|
+
function lnurlToSvg(endpoint, options) {
|
|
1537
|
+
return svgOf(toLnurl(endpoint), options);
|
|
1538
|
+
}
|
|
1539
|
+
function lnurlToDataUrl(endpoint, options) {
|
|
1540
|
+
return asDataUrl(lnurlToSvg(endpoint, options));
|
|
1541
|
+
}
|
|
1542
|
+
function spdToSvg(spd, options) {
|
|
1543
|
+
return svgOf(spd, options);
|
|
1544
|
+
}
|
|
1545
|
+
function spdToDataUrl(spd, options) {
|
|
1546
|
+
return asDataUrl(spdToSvg(spd, options));
|
|
1547
|
+
}
|
|
1548
|
+
function qrToSvg(payload, options) {
|
|
1549
|
+
return svgOf(payload, options);
|
|
1550
|
+
}
|
|
1551
|
+
function qrToDataUrl(payload, options) {
|
|
1552
|
+
return asDataUrl(qrToSvg(payload, options));
|
|
1553
|
+
}
|
|
1554
|
+
function svgOf(scanned, options) {
|
|
1555
|
+
const size = options?.size ?? 256;
|
|
1556
|
+
const color = options?.color ?? "#000";
|
|
1557
|
+
if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
|
|
1558
|
+
return renderSvg(scanned, size, color);
|
|
1559
|
+
}
|
|
1560
|
+
function asDataUrl(svg) {
|
|
1561
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1562
|
+
}
|
|
1563
|
+
function isLnAddress(destination) {
|
|
1564
|
+
const at = destination.indexOf("@");
|
|
1565
|
+
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1566
|
+
}
|
|
1567
|
+
function renderSvg(content, size, color) {
|
|
1568
|
+
const { data } = (0, import_uqr.encode)(content);
|
|
1569
|
+
const modules = data.length;
|
|
1570
|
+
const cellSize = size / modules;
|
|
1571
|
+
let path = "";
|
|
1572
|
+
for (let y = 0; y < modules; y++) {
|
|
1573
|
+
for (let x = 0; x < modules; x++) {
|
|
1574
|
+
if (data[y][x]) {
|
|
1575
|
+
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return [
|
|
1580
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
1581
|
+
`<rect width="100%" height="100%" fill="white"/>`,
|
|
1582
|
+
`<path d="${path}" fill="${color}"/>`,
|
|
1583
|
+
`</svg>`
|
|
1584
|
+
].join("");
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/rail.ts
|
|
1588
|
+
var BANK = "bank";
|
|
1589
|
+
var LIGHTNING = "lightning";
|
|
1590
|
+
function bankRail(config) {
|
|
1591
|
+
return async (order) => {
|
|
1592
|
+
const expiresAt = config.expiresAt(order);
|
|
1593
|
+
const transfer = await bankTransfer({
|
|
1594
|
+
gateway: config.gateway,
|
|
1595
|
+
secret: config.secret,
|
|
1596
|
+
iban: config.iban,
|
|
1597
|
+
verifyUrl: config.verifyUrl,
|
|
1598
|
+
reference: order.reference,
|
|
1599
|
+
amountMinor: order.amountMinor,
|
|
1600
|
+
currency: order.currency,
|
|
1601
|
+
expiresAt,
|
|
1602
|
+
trigger: config.trigger,
|
|
1603
|
+
sealed: await config.sealed?.(order),
|
|
1604
|
+
variableSymbol: config.variableSymbol?.(order),
|
|
1605
|
+
allowPublicGateway: config.allowPublicGateway
|
|
1606
|
+
});
|
|
1607
|
+
return {
|
|
1608
|
+
id: transfer.id,
|
|
1609
|
+
rail: config.name ?? BANK,
|
|
1610
|
+
scan: transfer.spd,
|
|
1611
|
+
qr: transfer.spd,
|
|
1612
|
+
expiresAt
|
|
1613
|
+
};
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
function lightningRail(config) {
|
|
1617
|
+
return async (order) => {
|
|
1618
|
+
const payment = await config.gateway.createPayment(
|
|
1619
|
+
{
|
|
1620
|
+
lnAddresses: config.lnAddresses,
|
|
1621
|
+
amountMsat: await config.amountMsat(order),
|
|
1622
|
+
webhookUrl: config.webhookUrl,
|
|
1623
|
+
webhookSecret: config.webhookSecret
|
|
1624
|
+
},
|
|
1625
|
+
{ idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
|
|
1626
|
+
);
|
|
1627
|
+
return {
|
|
1628
|
+
id: payment.id,
|
|
1629
|
+
rail: config.name ?? LIGHTNING,
|
|
1630
|
+
scan: payment.bolt11,
|
|
1631
|
+
qr: encodeForQr(payment.bolt11),
|
|
1632
|
+
expiresAt: payment.expiresAt
|
|
1633
|
+
};
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1695
1637
|
// src/price.ts
|
|
1696
1638
|
var MSAT_PER_BTC2 = 1e11;
|
|
1697
1639
|
var BASIS_POINTS = 1e4;
|
|
@@ -1814,64 +1756,6 @@ function refusals2(answers) {
|
|
|
1814
1756
|
).join("; ");
|
|
1815
1757
|
}
|
|
1816
1758
|
|
|
1817
|
-
// src/qr.ts
|
|
1818
|
-
var import_uqr = require("uqr");
|
|
1819
|
-
var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
|
|
1820
|
-
function encodeForQr(destination) {
|
|
1821
|
-
const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
|
|
1822
|
-
return `LIGHTNING:${scanned}`;
|
|
1823
|
-
}
|
|
1824
|
-
function invoiceToSvg(destination, options) {
|
|
1825
|
-
return svgOf(encodeForQr(destination), options);
|
|
1826
|
-
}
|
|
1827
|
-
function invoiceToDataUrl(destination, options) {
|
|
1828
|
-
return asDataUrl(invoiceToSvg(destination, options));
|
|
1829
|
-
}
|
|
1830
|
-
function lnurlToSvg(endpoint, options) {
|
|
1831
|
-
return svgOf(toLnurl(endpoint), options);
|
|
1832
|
-
}
|
|
1833
|
-
function lnurlToDataUrl(endpoint, options) {
|
|
1834
|
-
return asDataUrl(lnurlToSvg(endpoint, options));
|
|
1835
|
-
}
|
|
1836
|
-
function spdToSvg(spd, options) {
|
|
1837
|
-
return svgOf(spd, options);
|
|
1838
|
-
}
|
|
1839
|
-
function spdToDataUrl(spd, options) {
|
|
1840
|
-
return asDataUrl(spdToSvg(spd, options));
|
|
1841
|
-
}
|
|
1842
|
-
function svgOf(scanned, options) {
|
|
1843
|
-
const size = options?.size ?? 256;
|
|
1844
|
-
const color = options?.color ?? "#000";
|
|
1845
|
-
if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
|
|
1846
|
-
return renderSvg(scanned, size, color);
|
|
1847
|
-
}
|
|
1848
|
-
function asDataUrl(svg) {
|
|
1849
|
-
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1850
|
-
}
|
|
1851
|
-
function isLnAddress(destination) {
|
|
1852
|
-
const at = destination.indexOf("@");
|
|
1853
|
-
return at > 0 && destination.slice(at + 1).includes(".");
|
|
1854
|
-
}
|
|
1855
|
-
function renderSvg(content, size, color) {
|
|
1856
|
-
const { data } = (0, import_uqr.encode)(content);
|
|
1857
|
-
const modules = data.length;
|
|
1858
|
-
const cellSize = size / modules;
|
|
1859
|
-
let path = "";
|
|
1860
|
-
for (let y = 0; y < modules; y++) {
|
|
1861
|
-
for (let x = 0; x < modules; x++) {
|
|
1862
|
-
if (data[y][x]) {
|
|
1863
|
-
path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
return [
|
|
1868
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
|
|
1869
|
-
`<rect width="100%" height="100%" fill="white"/>`,
|
|
1870
|
-
`<path d="${path}" fill="${color}"/>`,
|
|
1871
|
-
`</svg>`
|
|
1872
|
-
].join("");
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
1759
|
// src/webhook.ts
|
|
1876
1760
|
var SIGNATURE_HEADER = "x-signature";
|
|
1877
1761
|
var TIMESTAMP_HEADER = "x-timestamp";
|
|
@@ -1880,7 +1764,7 @@ var DEFAULT_TOLERANCE_SECS = 300;
|
|
|
1880
1764
|
async function verifyWebhookSignature(body, signature, secret, timestamp, options = {}) {
|
|
1881
1765
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1882
1766
|
const received = signature.startsWith(SIGNATURE_PREFIX) ? signature.slice(SIGNATURE_PREFIX.length) : signature;
|
|
1883
|
-
return equalInConstantTime(await
|
|
1767
|
+
return equalInConstantTime(await sign(secret, timestamp, body), received.toLowerCase());
|
|
1884
1768
|
}
|
|
1885
1769
|
async function parseWebhook(body, signature, secret, timestamp, options = {}) {
|
|
1886
1770
|
if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
|
|
@@ -1902,7 +1786,7 @@ function recent(timestamp, toleranceSecs) {
|
|
|
1902
1786
|
if (!Number.isFinite(sent)) return false;
|
|
1903
1787
|
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1904
1788
|
}
|
|
1905
|
-
async function
|
|
1789
|
+
async function sign(secret, timestamp, body) {
|
|
1906
1790
|
return hmacHex(secret, signed(timestamp, body));
|
|
1907
1791
|
}
|
|
1908
1792
|
function signed(timestamp, body) {
|
|
@@ -1926,6 +1810,7 @@ function signed(timestamp, body) {
|
|
|
1926
1810
|
REQUEST_IN_FLIGHT,
|
|
1927
1811
|
ThunderBridge,
|
|
1928
1812
|
UnverifiedRecipientError,
|
|
1813
|
+
bankRail,
|
|
1929
1814
|
bankTransfer,
|
|
1930
1815
|
bankVerifyEndpoint,
|
|
1931
1816
|
bitstamp,
|
|
@@ -1938,7 +1823,7 @@ function signed(timestamp, body) {
|
|
|
1938
1823
|
isProblemType,
|
|
1939
1824
|
isProvablyPaid,
|
|
1940
1825
|
kraken,
|
|
1941
|
-
|
|
1826
|
+
lightningRail,
|
|
1942
1827
|
lnurlToDataUrl,
|
|
1943
1828
|
lnurlToSvg,
|
|
1944
1829
|
medianOf,
|
|
@@ -1950,6 +1835,8 @@ function signed(timestamp, body) {
|
|
|
1950
1835
|
preimageMatchesHash,
|
|
1951
1836
|
proveOrigin,
|
|
1952
1837
|
proveSettlement,
|
|
1838
|
+
qrToDataUrl,
|
|
1839
|
+
qrToSvg,
|
|
1953
1840
|
seal,
|
|
1954
1841
|
spdToDataUrl,
|
|
1955
1842
|
spdToSvg,
|