thunder-bridge 0.8.10 → 1.0.0
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 +60 -62
- package/dist/index.cjs +291 -118
- package/dist/index.d.cts +63 -10
- package/dist/index.d.ts +63 -10
- package/dist/index.js +287 -118
- package/dist/{rail-J8QoYbSr.d.cts → rail-CL9QkiHo.d.cts} +30 -6
- package/dist/{rail-J8QoYbSr.d.ts → rail-CL9QkiHo.d.ts} +30 -6
- package/dist/server.cjs +1 -2
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -2
- package/openapi.yaml +43 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
GatewayCheatError: () => GatewayCheatError,
|
|
24
|
+
Gateways: () => Gateways,
|
|
24
25
|
IDEMPOTENCY_KEY_REUSED: () => IDEMPOTENCY_KEY_REUSED,
|
|
25
26
|
IdempotencyConflictError: () => IdempotencyConflictError,
|
|
26
27
|
NO_WALLET_AVAILABLE: () => NO_WALLET_AVAILABLE,
|
|
@@ -46,6 +47,7 @@ __export(index_exports, {
|
|
|
46
47
|
invoiceToSvg: () => invoiceToSvg,
|
|
47
48
|
isProblemType: () => isProblemType,
|
|
48
49
|
isProvablyPaid: () => isProvablyPaid,
|
|
50
|
+
isProvablySettled: () => isProvablySettled,
|
|
49
51
|
kraken: () => kraken,
|
|
50
52
|
lightningRail: () => lightningRail,
|
|
51
53
|
lnurlToDataUrl: () => lnurlToDataUrl,
|
|
@@ -54,6 +56,8 @@ __export(index_exports, {
|
|
|
54
56
|
minorScaleOf: () => minorScaleOf,
|
|
55
57
|
minorUnitsOf: () => minorUnitsOf,
|
|
56
58
|
msatFor: () => msatFor,
|
|
59
|
+
parseSettlement: () => parseSettlement,
|
|
60
|
+
parseSettlementRequest: () => parseSettlementRequest,
|
|
57
61
|
parseWatchedWebhook: () => parseWatchedWebhook,
|
|
58
62
|
parseWatchedWebhookRequest: () => parseWatchedWebhookRequest,
|
|
59
63
|
parseWebhook: () => parseWebhook,
|
|
@@ -402,6 +406,105 @@ function wordsToBytes(words) {
|
|
|
402
406
|
return Uint8Array.from(bytes);
|
|
403
407
|
}
|
|
404
408
|
|
|
409
|
+
// ../core/ed25519.ts
|
|
410
|
+
var ALGORITHM = { name: "Ed25519" };
|
|
411
|
+
var SEED_BYTES = 32;
|
|
412
|
+
var SIGNATURE_BYTES = 64;
|
|
413
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
414
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
415
|
+
async function signingKeyFromSeed(seed) {
|
|
416
|
+
if (seed.length !== SEED_BYTES) throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
|
|
417
|
+
const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
|
|
418
|
+
wrapped.set(PKCS8_HEADER);
|
|
419
|
+
wrapped.set(seed, PKCS8_HEADER.length);
|
|
420
|
+
const key = await crypto.subtle.importKey("pkcs8", asBuffer(wrapped), ALGORITHM, true, ["sign"]);
|
|
421
|
+
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
422
|
+
return {
|
|
423
|
+
publicKeyHex: bytesToHex(fromBase64Url(jwk.x ?? "")),
|
|
424
|
+
sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key, asBuffer(payload))))
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
428
|
+
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
429
|
+
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
430
|
+
try {
|
|
431
|
+
const key = await crypto.subtle.importKey(
|
|
432
|
+
"raw",
|
|
433
|
+
asBuffer(hexToBytes(publicKeyHex)),
|
|
434
|
+
ALGORITHM,
|
|
435
|
+
false,
|
|
436
|
+
["verify"]
|
|
437
|
+
);
|
|
438
|
+
return await crypto.subtle.verify(
|
|
439
|
+
ALGORITHM,
|
|
440
|
+
key,
|
|
441
|
+
asBuffer(hexToBytes(signatureHex)),
|
|
442
|
+
asBuffer(payload)
|
|
443
|
+
);
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function fromBase64Url(text2) {
|
|
449
|
+
const padded = text2.replace(/-/g, "+").replace(/_/g, "/");
|
|
450
|
+
const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
451
|
+
const bytes = new Uint8Array(raw.length);
|
|
452
|
+
for (let at = 0; at < raw.length; at++) bytes[at] = raw.charCodeAt(at);
|
|
453
|
+
return bytes;
|
|
454
|
+
}
|
|
455
|
+
function asBuffer(bytes) {
|
|
456
|
+
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ../core/hmac.ts
|
|
460
|
+
async function hmacHex(secret, payload) {
|
|
461
|
+
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
462
|
+
const key = await crypto.subtle.importKey(
|
|
463
|
+
"raw",
|
|
464
|
+
asBuffer2(raw),
|
|
465
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
466
|
+
false,
|
|
467
|
+
["sign"]
|
|
468
|
+
);
|
|
469
|
+
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
470
|
+
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer2(message))));
|
|
471
|
+
}
|
|
472
|
+
function equalInConstantTime(one, other) {
|
|
473
|
+
if (one.length !== other.length) return false;
|
|
474
|
+
let difference = 0;
|
|
475
|
+
for (let at = 0; at < one.length; at++) difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
476
|
+
return difference === 0;
|
|
477
|
+
}
|
|
478
|
+
function asBuffer2(bytes) {
|
|
479
|
+
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ../core/caller.ts
|
|
483
|
+
var KEY_HEADER = "x-client-key";
|
|
484
|
+
var SIGNATURE_HEADER = "x-signature";
|
|
485
|
+
var TIMESTAMP_HEADER = "x-timestamp";
|
|
486
|
+
var PREFIX = "ed25519=";
|
|
487
|
+
var SIGNING_LABEL = "request-signing";
|
|
488
|
+
async function callerKey(secret) {
|
|
489
|
+
return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
|
|
490
|
+
}
|
|
491
|
+
function paymentNamedBy(caller, paymentHash) {
|
|
492
|
+
return sha256Hex(`${caller}.payment-id.${paymentHash}`);
|
|
493
|
+
}
|
|
494
|
+
async function signedAs(key, method, path, body) {
|
|
495
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
496
|
+
return {
|
|
497
|
+
[KEY_HEADER]: key.publicKeyHex,
|
|
498
|
+
[TIMESTAMP_HEADER]: timestamp,
|
|
499
|
+
[SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
function spoken(timestamp, method, path, body) {
|
|
503
|
+
return new TextEncoder().encode(
|
|
504
|
+
`${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
405
508
|
// ../core/url.ts
|
|
406
509
|
var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
|
|
407
510
|
function publicHttps(raw) {
|
|
@@ -550,7 +653,7 @@ function createRequestBody(params, trigger) {
|
|
|
550
653
|
return JSON.stringify({
|
|
551
654
|
ln_addresses: params.lnAddresses,
|
|
552
655
|
incoming_amount: toAmount(params.amountMsat),
|
|
553
|
-
webhook: params.webhookUrl ? { url: params.webhookUrl
|
|
656
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0,
|
|
554
657
|
trigger: trigger ?? void 0
|
|
555
658
|
});
|
|
556
659
|
}
|
|
@@ -616,9 +719,21 @@ function watchRequestBody(params, trigger) {
|
|
|
616
719
|
expires_at: expiresAt.toISOString(),
|
|
617
720
|
trigger: trigger ?? void 0,
|
|
618
721
|
sealed: params.sealed,
|
|
619
|
-
webhook: params.webhookUrl ? { url: params.webhookUrl
|
|
722
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0
|
|
620
723
|
});
|
|
621
724
|
}
|
|
725
|
+
function settlementFromWire(body) {
|
|
726
|
+
const wire = asObject(body);
|
|
727
|
+
if (wire === null) return null;
|
|
728
|
+
const id = text(wire["id"]);
|
|
729
|
+
const paymentHash = text(wire["payment_hash"]);
|
|
730
|
+
const settledAt = secondsFrom(wire["settled_at"]);
|
|
731
|
+
const status = wire["status"];
|
|
732
|
+
const preimage = wire["preimage"] ?? null;
|
|
733
|
+
if (id === null || paymentHash === null || settledAt === null || !isStatus(status)) return null;
|
|
734
|
+
if (preimage !== null && typeof preimage !== "string") return null;
|
|
735
|
+
return { id, status, paymentHash, preimage, settledAt };
|
|
736
|
+
}
|
|
622
737
|
function triggerEventFromWire(body) {
|
|
623
738
|
const wire = asObject(body);
|
|
624
739
|
if (wire === null) return null;
|
|
@@ -716,11 +831,14 @@ var ThunderBridge = class {
|
|
|
716
831
|
baseUrl;
|
|
717
832
|
verify;
|
|
718
833
|
token;
|
|
834
|
+
secret;
|
|
719
835
|
strangers = null;
|
|
836
|
+
speaks = null;
|
|
720
837
|
constructor(baseUrl, options) {
|
|
721
838
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
722
839
|
this.verify = options?.verify ?? true;
|
|
723
840
|
this.token = options?.token ?? null;
|
|
841
|
+
this.secret = options?.secret ?? null;
|
|
724
842
|
}
|
|
725
843
|
/**
|
|
726
844
|
* Whether a token was given, which is what makes an instance yours: a gateway
|
|
@@ -751,15 +869,16 @@ var ThunderBridge = class {
|
|
|
751
869
|
* and `GatewayCheatError` when what comes back is not what you asked for
|
|
752
870
|
*/
|
|
753
871
|
async createPayment(params, options) {
|
|
754
|
-
const
|
|
872
|
+
const sent = createRequestBody(
|
|
873
|
+
params,
|
|
874
|
+
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
|
|
875
|
+
);
|
|
876
|
+
const headers = await this.sending("/incoming-payments", sent);
|
|
755
877
|
if (options?.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
|
|
756
878
|
const response = await fetch(`${this.baseUrl}/incoming-payments`, {
|
|
757
879
|
method: "POST",
|
|
758
880
|
headers,
|
|
759
|
-
body:
|
|
760
|
-
params,
|
|
761
|
-
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
|
|
762
|
-
)
|
|
881
|
+
body: sent
|
|
763
882
|
});
|
|
764
883
|
if (!response.ok) throw await problemFrom(response);
|
|
765
884
|
const payment = await paymentFrom(response);
|
|
@@ -774,10 +893,11 @@ var ThunderBridge = class {
|
|
|
774
893
|
* it for one, and asking mints it
|
|
775
894
|
*/
|
|
776
895
|
async createQuote(params) {
|
|
896
|
+
const sent = quoteRequestBody(params);
|
|
777
897
|
const response = await fetch(`${this.baseUrl}/quotes`, {
|
|
778
898
|
method: "POST",
|
|
779
|
-
headers: this.sending(),
|
|
780
|
-
body:
|
|
899
|
+
headers: await this.sending("/quotes", sent),
|
|
900
|
+
body: sent
|
|
781
901
|
});
|
|
782
902
|
if (!response.ok) throw await problemFrom(response);
|
|
783
903
|
const quote = quoteFromWire(await response.json().catch(() => null));
|
|
@@ -808,8 +928,9 @@ var ThunderBridge = class {
|
|
|
808
928
|
return body.public_key;
|
|
809
929
|
}
|
|
810
930
|
async getPayment(id) {
|
|
811
|
-
const
|
|
812
|
-
|
|
931
|
+
const path = `/incoming-payments/${encodeURIComponent(id)}`;
|
|
932
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
933
|
+
headers: await this.reading("GET", path)
|
|
813
934
|
});
|
|
814
935
|
if (response.status === 404) return null;
|
|
815
936
|
if (!response.ok) throw await problemFrom(response);
|
|
@@ -821,8 +942,9 @@ var ThunderBridge = class {
|
|
|
821
942
|
* `getPayment` refuses it and this reads the shape both rails share
|
|
822
943
|
*/
|
|
823
944
|
async getWatched(id) {
|
|
824
|
-
const
|
|
825
|
-
|
|
945
|
+
const path = `/incoming-payments/${encodeURIComponent(id)}`;
|
|
946
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
947
|
+
headers: await this.reading("GET", path)
|
|
826
948
|
});
|
|
827
949
|
if (response.status === 404) return null;
|
|
828
950
|
if (!response.ok) throw await problemFrom(response);
|
|
@@ -845,9 +967,9 @@ var ThunderBridge = class {
|
|
|
845
967
|
* pretend otherwise
|
|
846
968
|
*/
|
|
847
969
|
async listPayments(limit) {
|
|
848
|
-
const
|
|
849
|
-
const response = await fetch(`${this.baseUrl}
|
|
850
|
-
headers: this.reading()
|
|
970
|
+
const path = `/incoming-payments${limit === void 0 ? "" : `?limit=${limit}`}`;
|
|
971
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
972
|
+
headers: await this.reading("GET", path)
|
|
851
973
|
});
|
|
852
974
|
if (!response.ok) throw await problemFrom(response);
|
|
853
975
|
const body = await response.json().catch(() => null);
|
|
@@ -1015,13 +1137,14 @@ var ThunderBridge = class {
|
|
|
1015
1137
|
* the watcher needs goes in `sealed`, which the gateway cannot read
|
|
1016
1138
|
*/
|
|
1017
1139
|
async watchPayment(params) {
|
|
1140
|
+
const sent = watchRequestBody(
|
|
1141
|
+
params,
|
|
1142
|
+
params.trigger ? sha256Hex(unguessable(params.trigger)) : null
|
|
1143
|
+
);
|
|
1018
1144
|
const response = await fetch(`${this.baseUrl}/watched-payments`, {
|
|
1019
1145
|
method: "POST",
|
|
1020
|
-
headers: this.sending(),
|
|
1021
|
-
body:
|
|
1022
|
-
params,
|
|
1023
|
-
params.trigger ? sha256Hex(unguessable(params.trigger)) : null
|
|
1024
|
-
)
|
|
1146
|
+
headers: await this.sending("/watched-payments", sent),
|
|
1147
|
+
body: sent
|
|
1025
1148
|
});
|
|
1026
1149
|
if (!response.ok) throw await problemFrom(response);
|
|
1027
1150
|
const watched = triggerEventFromWire(await response.json().catch(() => null));
|
|
@@ -1031,8 +1154,22 @@ var ThunderBridge = class {
|
|
|
1031
1154
|
title: "The gateway answered with something that is not a watched payment"
|
|
1032
1155
|
});
|
|
1033
1156
|
}
|
|
1157
|
+
const named = await this.nameFor(params.paymentHash);
|
|
1158
|
+
if (named !== null && watched.id !== named) {
|
|
1159
|
+
throw new GatewayCheatError("id_not_mine", watched.id);
|
|
1160
|
+
}
|
|
1034
1161
|
return watched;
|
|
1035
1162
|
}
|
|
1163
|
+
/**
|
|
1164
|
+
* What this payment is called, which you can work out before any gateway has
|
|
1165
|
+
* heard of it. Every gateway you hand the same invoice to answers with the same
|
|
1166
|
+
* name, so watching at several of them adds up to one payment rather than
|
|
1167
|
+
* several, and no gateway's key is in the answer. Null when no secret was given,
|
|
1168
|
+
* because then the gateway names the payment and only it can
|
|
1169
|
+
*/
|
|
1170
|
+
async nameFor(paymentHash) {
|
|
1171
|
+
return this.secret === null ? null : paymentNamedBy((await this.speaking()).publicKeyHex, paymentHash);
|
|
1172
|
+
}
|
|
1036
1173
|
/**
|
|
1037
1174
|
* Follow every payment made to one trigger, replayed from the recent ones on
|
|
1038
1175
|
* connect and then live, reconnecting on its own until the returned function
|
|
@@ -1089,10 +1226,11 @@ var ThunderBridge = class {
|
|
|
1089
1226
|
return asked === true || this.token !== null;
|
|
1090
1227
|
}
|
|
1091
1228
|
async wsTicket(body) {
|
|
1229
|
+
const sent = JSON.stringify(body);
|
|
1092
1230
|
const response = await fetch(`${this.baseUrl}/ws-tickets`, {
|
|
1093
1231
|
method: "POST",
|
|
1094
|
-
headers: this.sending(),
|
|
1095
|
-
body:
|
|
1232
|
+
headers: await this.sending("/ws-tickets", sent),
|
|
1233
|
+
body: sent
|
|
1096
1234
|
});
|
|
1097
1235
|
if (!response.ok) throw await problemFrom(response);
|
|
1098
1236
|
const minted = await response.json().catch(() => null);
|
|
@@ -1104,11 +1242,18 @@ var ThunderBridge = class {
|
|
|
1104
1242
|
}
|
|
1105
1243
|
return encodeURIComponent(minted.ticket);
|
|
1106
1244
|
}
|
|
1107
|
-
sending() {
|
|
1108
|
-
return { "content-type": "application/json", ...this.reading() };
|
|
1245
|
+
async sending(path, body) {
|
|
1246
|
+
return { "content-type": "application/json", ...await this.reading("POST", path, body) };
|
|
1247
|
+
}
|
|
1248
|
+
async reading(method, path, body = "") {
|
|
1249
|
+
return {
|
|
1250
|
+
...this.token === null ? {} : { authorization: `Bearer ${this.token}` },
|
|
1251
|
+
...this.secret === null ? {} : await signedAs(await this.speaking(), method, path, body)
|
|
1252
|
+
};
|
|
1109
1253
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1254
|
+
async speaking() {
|
|
1255
|
+
this.speaks ??= callerKey(this.secret ?? "");
|
|
1256
|
+
return await this.speaks;
|
|
1112
1257
|
}
|
|
1113
1258
|
proven(settled) {
|
|
1114
1259
|
const unproven = settled.status === "paid" && (settled.preimage === null || !preimageMatchesHash(settled.preimage, settled.paymentHash));
|
|
@@ -1153,6 +1298,80 @@ function refusals(wallets) {
|
|
|
1153
1298
|
});
|
|
1154
1299
|
}
|
|
1155
1300
|
|
|
1301
|
+
// src/gateways.ts
|
|
1302
|
+
var Gateways = class {
|
|
1303
|
+
each;
|
|
1304
|
+
urls;
|
|
1305
|
+
onRefused;
|
|
1306
|
+
constructor(baseUrls, options) {
|
|
1307
|
+
if (baseUrls.length === 0) throw new Error("name at least one gateway to watch at");
|
|
1308
|
+
this.urls = [...baseUrls];
|
|
1309
|
+
this.each = baseUrls.map((baseUrl) => new ThunderBridge(baseUrl, options));
|
|
1310
|
+
this.onRefused = options?.onRefused ?? (() => {
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
/** What this payment is called, which every gateway here will agree on */
|
|
1314
|
+
async nameFor(paymentHash) {
|
|
1315
|
+
return await this.each[0].nameFor(paymentHash);
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Hand the same invoice to every gateway. Throws only when none of them took it,
|
|
1319
|
+
* carrying the first refusal, because one gateway that agreed is enough to be
|
|
1320
|
+
* watched
|
|
1321
|
+
*/
|
|
1322
|
+
async watchPayment(params) {
|
|
1323
|
+
const asked = await Promise.allSettled(this.each.map((gateway) => gateway.watchPayment(params)));
|
|
1324
|
+
const taken = [];
|
|
1325
|
+
const refusals3 = [];
|
|
1326
|
+
for (const [at, answer] of asked.entries()) {
|
|
1327
|
+
if (answer.status === "fulfilled") {
|
|
1328
|
+
taken.push(answer.value);
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
refusals3.push(answer.reason);
|
|
1332
|
+
this.onRefused(this.urls[at], answer.reason);
|
|
1333
|
+
}
|
|
1334
|
+
if (taken[0] === void 0) throw refusals3[0];
|
|
1335
|
+
return taken[0];
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Wait for whichever gateway speaks first. A settlement from any of them is the
|
|
1339
|
+
* settlement, and each has already proved the preimage against the hash before
|
|
1340
|
+
* saying so. When they all end without a payment, the first ending is the answer,
|
|
1341
|
+
* and when they all fail, the first failure is thrown
|
|
1342
|
+
*/
|
|
1343
|
+
async waitForWatched(id, options) {
|
|
1344
|
+
const stopLosers = new AbortController();
|
|
1345
|
+
const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
|
|
1346
|
+
const unpaid = [];
|
|
1347
|
+
let refused = null;
|
|
1348
|
+
try {
|
|
1349
|
+
const settled = await new Promise((resolve2) => {
|
|
1350
|
+
let waiting = this.each.length;
|
|
1351
|
+
const lost = () => {
|
|
1352
|
+
waiting -= 1;
|
|
1353
|
+
if (waiting === 0) resolve2(null);
|
|
1354
|
+
};
|
|
1355
|
+
for (const gateway of this.each) {
|
|
1356
|
+
gateway.waitForWatched(id, { ...options, signal }).then((watched) => {
|
|
1357
|
+
if (watched.status === "paid") return resolve2(watched);
|
|
1358
|
+
unpaid.push(watched);
|
|
1359
|
+
lost();
|
|
1360
|
+
}).catch((failure) => {
|
|
1361
|
+
refused ??= failure;
|
|
1362
|
+
lost();
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
if (settled !== null) return settled;
|
|
1367
|
+
if (unpaid.length > 0) return unpaid[0];
|
|
1368
|
+
throw refused;
|
|
1369
|
+
} finally {
|
|
1370
|
+
stopLosers.abort();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1156
1375
|
// ../core/sealed.ts
|
|
1157
1376
|
var VERSION = "v1";
|
|
1158
1377
|
var IV_BYTES = 12;
|
|
@@ -1175,7 +1394,7 @@ async function seal(secret, plaintext) {
|
|
|
1175
1394
|
async function unseal(secret, sealed) {
|
|
1176
1395
|
const key = await keyFor(secret);
|
|
1177
1396
|
if (!sealed.startsWith(`${VERSION}.`)) return null;
|
|
1178
|
-
const joined =
|
|
1397
|
+
const joined = fromBase64Url2(sealed.slice(VERSION.length + 1));
|
|
1179
1398
|
if (joined === null || joined.length <= IV_BYTES) return null;
|
|
1180
1399
|
try {
|
|
1181
1400
|
const body = await crypto.subtle.decrypt(
|
|
@@ -1212,7 +1431,7 @@ function toBase64Url(bytes) {
|
|
|
1212
1431
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1213
1432
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1214
1433
|
}
|
|
1215
|
-
function
|
|
1434
|
+
function fromBase64Url2(text2) {
|
|
1216
1435
|
if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
|
|
1217
1436
|
try {
|
|
1218
1437
|
const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
|
|
@@ -1224,29 +1443,6 @@ function fromBase64Url(text2) {
|
|
|
1224
1443
|
}
|
|
1225
1444
|
}
|
|
1226
1445
|
|
|
1227
|
-
// ../core/hmac.ts
|
|
1228
|
-
async function hmacHex(secret, payload) {
|
|
1229
|
-
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
1230
|
-
const key = await crypto.subtle.importKey(
|
|
1231
|
-
"raw",
|
|
1232
|
-
asBuffer(raw),
|
|
1233
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
1234
|
-
false,
|
|
1235
|
-
["sign"]
|
|
1236
|
-
);
|
|
1237
|
-
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
1238
|
-
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
|
|
1239
|
-
}
|
|
1240
|
-
function equalInConstantTime(one, other) {
|
|
1241
|
-
if (one.length !== other.length) return false;
|
|
1242
|
-
let difference = 0;
|
|
1243
|
-
for (let at = 0; at < one.length; at++) difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
1244
|
-
return difference === 0;
|
|
1245
|
-
}
|
|
1246
|
-
function asBuffer(bytes) {
|
|
1247
|
-
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
1446
|
// src/currency.ts
|
|
1251
1447
|
var ISO_4217_MINOR_UNITS = {
|
|
1252
1448
|
BHD: 3,
|
|
@@ -1289,57 +1485,21 @@ function minorScaleOf(currency) {
|
|
|
1289
1485
|
return 10 ** minorUnitsOf(currency);
|
|
1290
1486
|
}
|
|
1291
1487
|
|
|
1292
|
-
// ../core/ed25519.ts
|
|
1293
|
-
var ALGORITHM = { name: "Ed25519" };
|
|
1294
|
-
var SIGNATURE_BYTES = 64;
|
|
1295
|
-
var PUBLIC_KEY_BYTES = 32;
|
|
1296
|
-
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1297
|
-
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1298
|
-
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1299
|
-
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1300
|
-
try {
|
|
1301
|
-
const key = await crypto.subtle.importKey(
|
|
1302
|
-
"raw",
|
|
1303
|
-
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1304
|
-
ALGORITHM,
|
|
1305
|
-
false,
|
|
1306
|
-
["verify"]
|
|
1307
|
-
);
|
|
1308
|
-
return await crypto.subtle.verify(
|
|
1309
|
-
ALGORITHM,
|
|
1310
|
-
key,
|
|
1311
|
-
asBuffer2(hexToBytes(signatureHex)),
|
|
1312
|
-
asBuffer2(payload)
|
|
1313
|
-
);
|
|
1314
|
-
} catch {
|
|
1315
|
-
return false;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
function asBuffer2(bytes) {
|
|
1319
|
-
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
1488
|
// src/webhook.ts
|
|
1323
|
-
var
|
|
1324
|
-
var
|
|
1325
|
-
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1489
|
+
var SIGNATURE_HEADER2 = "x-signature";
|
|
1490
|
+
var TIMESTAMP_HEADER2 = "x-timestamp";
|
|
1326
1491
|
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1327
1492
|
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1328
1493
|
var CHALLENGE = "webhook-challenge";
|
|
1329
1494
|
var VERIFY_CHALLENGE = "verify-challenge";
|
|
1330
1495
|
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1331
1496
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1341
|
-
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1342
|
-
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1497
|
+
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1498
|
+
return await verifyHex(
|
|
1499
|
+
credential.publicKey.toLowerCase(),
|
|
1500
|
+
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1501
|
+
signed(timestamp, body)
|
|
1502
|
+
);
|
|
1343
1503
|
}
|
|
1344
1504
|
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1345
1505
|
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
@@ -1351,8 +1511,8 @@ async function parseWebhook(body, signature, credential, timestamp, options = {}
|
|
|
1351
1511
|
}
|
|
1352
1512
|
}
|
|
1353
1513
|
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1354
|
-
const signature = request.headers.get(
|
|
1355
|
-
const timestamp = request.headers.get(
|
|
1514
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1515
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1356
1516
|
if (signature === null || timestamp === null) return null;
|
|
1357
1517
|
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1358
1518
|
}
|
|
@@ -1366,25 +1526,37 @@ async function parseWatchedWebhook(body, signature, credential, timestamp, optio
|
|
|
1366
1526
|
}
|
|
1367
1527
|
}
|
|
1368
1528
|
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1369
|
-
const signature = request.headers.get(
|
|
1370
|
-
const timestamp = request.headers.get(
|
|
1529
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1530
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1371
1531
|
if (signature === null || timestamp === null) return null;
|
|
1372
1532
|
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1373
1533
|
}
|
|
1534
|
+
async function parseSettlement(body, signature, credential, timestamp, options = {}) {
|
|
1535
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1536
|
+
try {
|
|
1537
|
+
return settlementFromWire(JSON.parse(textOf(body)));
|
|
1538
|
+
} catch {
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
async function parseSettlementRequest(request, credential, options = {}) {
|
|
1543
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1544
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1545
|
+
if (signature === null || timestamp === null) return null;
|
|
1546
|
+
return await parseSettlement(await request.text(), signature, credential, timestamp, options);
|
|
1547
|
+
}
|
|
1548
|
+
function isProvablySettled(settled) {
|
|
1549
|
+
if (settled.status !== "paid" || settled.preimage === null) return false;
|
|
1550
|
+
return preimageMatchesHash(settled.preimage, settled.paymentHash);
|
|
1551
|
+
}
|
|
1374
1552
|
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1375
1553
|
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
if (nonce === null) return null;
|
|
1379
|
-
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1380
|
-
return JSON.stringify({
|
|
1381
|
-
nonce,
|
|
1382
|
-
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1383
|
-
});
|
|
1554
|
+
const nonce = challenged(typeof body === "string" ? body : new TextDecoder().decode(body));
|
|
1555
|
+
return nonce === null ? null : JSON.stringify({ nonce });
|
|
1384
1556
|
}
|
|
1385
1557
|
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1386
|
-
const signature = request.headers.get(
|
|
1387
|
-
const timestamp = request.headers.get(
|
|
1558
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1559
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1388
1560
|
if (signature === null || timestamp === null) return null;
|
|
1389
1561
|
const answer = await answerWebhookChallenge(
|
|
1390
1562
|
await request.clone().text(),
|
|
@@ -1395,6 +1567,9 @@ async function answerWebhookChallengeRequest(request, credential, options = {})
|
|
|
1395
1567
|
);
|
|
1396
1568
|
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1397
1569
|
}
|
|
1570
|
+
function textOf(body) {
|
|
1571
|
+
return typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1572
|
+
}
|
|
1398
1573
|
function challenged(text2) {
|
|
1399
1574
|
return nonceOf(text2, CHALLENGE);
|
|
1400
1575
|
}
|
|
@@ -1421,9 +1596,6 @@ function recent(timestamp, toleranceSecs) {
|
|
|
1421
1596
|
if (!Number.isFinite(sent)) return false;
|
|
1422
1597
|
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1423
1598
|
}
|
|
1424
|
-
async function sign(secret, timestamp, body) {
|
|
1425
|
-
return hmacHex(secret, signed(timestamp, body));
|
|
1426
|
-
}
|
|
1427
1599
|
function signed(timestamp, body) {
|
|
1428
1600
|
const encoder = new TextEncoder();
|
|
1429
1601
|
const stamp = encoder.encode(`${timestamp}.`);
|
|
@@ -1458,8 +1630,7 @@ async function bankTransfer(params) {
|
|
|
1458
1630
|
expiresAt: params.expiresAt,
|
|
1459
1631
|
trigger: params.trigger,
|
|
1460
1632
|
sealed: params.sealed,
|
|
1461
|
-
webhookUrl: params.webhookUrl
|
|
1462
|
-
webhookSecret: params.webhookSecret
|
|
1633
|
+
webhookUrl: params.webhookUrl
|
|
1463
1634
|
});
|
|
1464
1635
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1465
1636
|
}
|
|
@@ -1786,7 +1957,6 @@ function bankRail(config) {
|
|
|
1786
1957
|
sealed: await config.sealed?.(order),
|
|
1787
1958
|
variableSymbol: config.variableSymbol?.(order),
|
|
1788
1959
|
webhookUrl: config.webhookUrl,
|
|
1789
|
-
webhookSecret: config.webhookSecret,
|
|
1790
1960
|
allowPublicGateway: config.allowPublicGateway
|
|
1791
1961
|
});
|
|
1792
1962
|
return {
|
|
@@ -1804,8 +1974,7 @@ function lightningRail(config) {
|
|
|
1804
1974
|
{
|
|
1805
1975
|
lnAddresses: config.lnAddresses,
|
|
1806
1976
|
amountMsat: await config.amountMsat(order),
|
|
1807
|
-
webhookUrl: config.webhookUrl
|
|
1808
|
-
webhookSecret: config.webhookSecret
|
|
1977
|
+
webhookUrl: config.webhookUrl
|
|
1809
1978
|
},
|
|
1810
1979
|
{ idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
|
|
1811
1980
|
);
|
|
@@ -1943,6 +2112,7 @@ function refusals2(answers) {
|
|
|
1943
2112
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1944
2113
|
0 && (module.exports = {
|
|
1945
2114
|
GatewayCheatError,
|
|
2115
|
+
Gateways,
|
|
1946
2116
|
IDEMPOTENCY_KEY_REUSED,
|
|
1947
2117
|
IdempotencyConflictError,
|
|
1948
2118
|
NO_WALLET_AVAILABLE,
|
|
@@ -1968,6 +2138,7 @@ function refusals2(answers) {
|
|
|
1968
2138
|
invoiceToSvg,
|
|
1969
2139
|
isProblemType,
|
|
1970
2140
|
isProvablyPaid,
|
|
2141
|
+
isProvablySettled,
|
|
1971
2142
|
kraken,
|
|
1972
2143
|
lightningRail,
|
|
1973
2144
|
lnurlToDataUrl,
|
|
@@ -1976,6 +2147,8 @@ function refusals2(answers) {
|
|
|
1976
2147
|
minorScaleOf,
|
|
1977
2148
|
minorUnitsOf,
|
|
1978
2149
|
msatFor,
|
|
2150
|
+
parseSettlement,
|
|
2151
|
+
parseSettlementRequest,
|
|
1979
2152
|
parseWatchedWebhook,
|
|
1980
2153
|
parseWatchedWebhookRequest,
|
|
1981
2154
|
parseWebhook,
|