thunder-bridge 0.8.9 → 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 +35 -30
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +35 -30
- package/openapi.yaml +43 -1
- package/package.json +1 -1
|
@@ -23,7 +23,6 @@ interface CreatePaymentParams {
|
|
|
23
23
|
lnAddresses: string[];
|
|
24
24
|
amountMsat: number;
|
|
25
25
|
webhookUrl?: string;
|
|
26
|
-
webhookSecret?: string;
|
|
27
26
|
}
|
|
28
27
|
/**
|
|
29
28
|
* `lnAddresses` is the same priority list `createPayment` takes, and quoting it
|
|
@@ -78,7 +77,6 @@ interface WatchPaymentParams {
|
|
|
78
77
|
trigger?: string;
|
|
79
78
|
sealed?: string;
|
|
80
79
|
webhookUrl?: string;
|
|
81
|
-
webhookSecret?: string;
|
|
82
80
|
}
|
|
83
81
|
/** Why one wallet in the list could not be used */
|
|
84
82
|
type WalletReason = "address-unusable" | "unreachable" | "amount-not-accepted" | "cannot-prove-delivery" | "invoice-refused";
|
|
@@ -86,6 +84,17 @@ interface WalletFailure {
|
|
|
86
84
|
address: string;
|
|
87
85
|
reason: WalletReason;
|
|
88
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* What a delivery carries. Everything needed to act on a settlement and to check
|
|
89
|
+
* it, and nothing else, so a retry is the same size every time
|
|
90
|
+
*/
|
|
91
|
+
interface Settlement {
|
|
92
|
+
id: string;
|
|
93
|
+
status: PaymentStatus;
|
|
94
|
+
paymentHash: string;
|
|
95
|
+
preimage: string | null;
|
|
96
|
+
settledAt: number;
|
|
97
|
+
}
|
|
89
98
|
|
|
90
99
|
interface ThunderBridgeOptions {
|
|
91
100
|
/**
|
|
@@ -101,6 +110,13 @@ interface ThunderBridgeOptions {
|
|
|
101
110
|
* through a ticket
|
|
102
111
|
*/
|
|
103
112
|
token?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The same long lived server side secret your rail derives its preimages from.
|
|
115
|
+
* Given here, every call carries a signature the gateway reads as your identity,
|
|
116
|
+
* so a payment you create is handed back to you and to nobody else. Withheld,
|
|
117
|
+
* you are anonymous and any holder of an id can read what it names
|
|
118
|
+
*/
|
|
119
|
+
secret?: string;
|
|
104
120
|
}
|
|
105
121
|
interface WaitOptions {
|
|
106
122
|
/** Give up when this aborts, `AbortSignal.timeout(ms)` covers the usual case */
|
|
@@ -159,7 +175,9 @@ declare class ThunderBridge {
|
|
|
159
175
|
private readonly baseUrl;
|
|
160
176
|
private readonly verify;
|
|
161
177
|
private readonly token;
|
|
178
|
+
private readonly secret;
|
|
162
179
|
private strangers;
|
|
180
|
+
private speaks;
|
|
163
181
|
constructor(baseUrl: string, options?: ThunderBridgeOptions);
|
|
164
182
|
/**
|
|
165
183
|
* Whether a token was given, which is what makes an instance yours: a gateway
|
|
@@ -262,6 +280,14 @@ declare class ThunderBridge {
|
|
|
262
280
|
* the watcher needs goes in `sealed`, which the gateway cannot read
|
|
263
281
|
*/
|
|
264
282
|
watchPayment(params: WatchPaymentParams): Promise<TriggerEvent>;
|
|
283
|
+
/**
|
|
284
|
+
* What this payment is called, which you can work out before any gateway has
|
|
285
|
+
* heard of it. Every gateway you hand the same invoice to answers with the same
|
|
286
|
+
* name, so watching at several of them adds up to one payment rather than
|
|
287
|
+
* several, and no gateway's key is in the answer. Null when no secret was given,
|
|
288
|
+
* because then the gateway names the payment and only it can
|
|
289
|
+
*/
|
|
290
|
+
nameFor(paymentHash: string): Promise<string | null>;
|
|
265
291
|
/**
|
|
266
292
|
* Follow every payment made to one trigger, replayed from the recent ones on
|
|
267
293
|
* connect and then live, reconnecting on its own until the returned function
|
|
@@ -272,6 +298,7 @@ declare class ThunderBridge {
|
|
|
272
298
|
private wsTicket;
|
|
273
299
|
private sending;
|
|
274
300
|
private reading;
|
|
301
|
+
private speaking;
|
|
275
302
|
private proven;
|
|
276
303
|
private checked;
|
|
277
304
|
}
|
|
@@ -324,7 +351,6 @@ interface BankRailConfig {
|
|
|
324
351
|
/** Up to ten digits, for accounting systems that still want one */
|
|
325
352
|
variableSymbol?: (order: Order) => string | undefined;
|
|
326
353
|
webhookUrl?: string;
|
|
327
|
-
webhookSecret?: string;
|
|
328
354
|
/** Register on a gateway you do not own anyway, on the terms `bankTransfer` sets out */
|
|
329
355
|
allowPublicGateway?: boolean;
|
|
330
356
|
/** What `Leg.rail` reads, for a shop running more than one account */
|
|
@@ -345,7 +371,6 @@ interface LightningRailConfig {
|
|
|
345
371
|
*/
|
|
346
372
|
idempotencyKey?: (order: Order) => string | undefined;
|
|
347
373
|
webhookUrl?: string;
|
|
348
|
-
webhookSecret?: string;
|
|
349
374
|
/** What `Leg.rail` reads, for a shop running more than one wallet */
|
|
350
375
|
name?: string;
|
|
351
376
|
}
|
|
@@ -361,7 +386,6 @@ interface BlindLightningRailConfig {
|
|
|
361
386
|
/** Only a watched leg has anywhere to carry this */
|
|
362
387
|
sealed?: (order: Order) => string | Promise<string>;
|
|
363
388
|
webhookUrl?: string;
|
|
364
|
-
webhookSecret?: string;
|
|
365
389
|
/**
|
|
366
390
|
* Where your own `lightningVerifyEndpoint` is mounted, and the secret it
|
|
367
391
|
* unseals with. Set both and the gateway is handed your URL rather than the
|
|
@@ -397,4 +421,4 @@ declare function lightningRail(config: LightningRailConfig): Rail;
|
|
|
397
421
|
*/
|
|
398
422
|
declare function blindLightningRail(config: BlindLightningRailConfig): Rail;
|
|
399
423
|
|
|
400
|
-
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, ThunderBridge as T, type
|
|
424
|
+
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, type Settlement as S, ThunderBridge as T, type WatchPaymentParams as W, type ThunderBridgeOptions as a, type TriggerEvent as b, type WaitOptions as c, type WalletFailure as d, type CreateOptions as e, type CreateQuoteParams as f, type LightningRailConfig as g, type PaymentKind as h, type PaymentStatus as i, type WalletReason as j, bankRail as k, lightningRail as l, type BlindLightningRailConfig as m, blindLightningRail as n };
|
|
@@ -23,7 +23,6 @@ interface CreatePaymentParams {
|
|
|
23
23
|
lnAddresses: string[];
|
|
24
24
|
amountMsat: number;
|
|
25
25
|
webhookUrl?: string;
|
|
26
|
-
webhookSecret?: string;
|
|
27
26
|
}
|
|
28
27
|
/**
|
|
29
28
|
* `lnAddresses` is the same priority list `createPayment` takes, and quoting it
|
|
@@ -78,7 +77,6 @@ interface WatchPaymentParams {
|
|
|
78
77
|
trigger?: string;
|
|
79
78
|
sealed?: string;
|
|
80
79
|
webhookUrl?: string;
|
|
81
|
-
webhookSecret?: string;
|
|
82
80
|
}
|
|
83
81
|
/** Why one wallet in the list could not be used */
|
|
84
82
|
type WalletReason = "address-unusable" | "unreachable" | "amount-not-accepted" | "cannot-prove-delivery" | "invoice-refused";
|
|
@@ -86,6 +84,17 @@ interface WalletFailure {
|
|
|
86
84
|
address: string;
|
|
87
85
|
reason: WalletReason;
|
|
88
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* What a delivery carries. Everything needed to act on a settlement and to check
|
|
89
|
+
* it, and nothing else, so a retry is the same size every time
|
|
90
|
+
*/
|
|
91
|
+
interface Settlement {
|
|
92
|
+
id: string;
|
|
93
|
+
status: PaymentStatus;
|
|
94
|
+
paymentHash: string;
|
|
95
|
+
preimage: string | null;
|
|
96
|
+
settledAt: number;
|
|
97
|
+
}
|
|
89
98
|
|
|
90
99
|
interface ThunderBridgeOptions {
|
|
91
100
|
/**
|
|
@@ -101,6 +110,13 @@ interface ThunderBridgeOptions {
|
|
|
101
110
|
* through a ticket
|
|
102
111
|
*/
|
|
103
112
|
token?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The same long lived server side secret your rail derives its preimages from.
|
|
115
|
+
* Given here, every call carries a signature the gateway reads as your identity,
|
|
116
|
+
* so a payment you create is handed back to you and to nobody else. Withheld,
|
|
117
|
+
* you are anonymous and any holder of an id can read what it names
|
|
118
|
+
*/
|
|
119
|
+
secret?: string;
|
|
104
120
|
}
|
|
105
121
|
interface WaitOptions {
|
|
106
122
|
/** Give up when this aborts, `AbortSignal.timeout(ms)` covers the usual case */
|
|
@@ -159,7 +175,9 @@ declare class ThunderBridge {
|
|
|
159
175
|
private readonly baseUrl;
|
|
160
176
|
private readonly verify;
|
|
161
177
|
private readonly token;
|
|
178
|
+
private readonly secret;
|
|
162
179
|
private strangers;
|
|
180
|
+
private speaks;
|
|
163
181
|
constructor(baseUrl: string, options?: ThunderBridgeOptions);
|
|
164
182
|
/**
|
|
165
183
|
* Whether a token was given, which is what makes an instance yours: a gateway
|
|
@@ -262,6 +280,14 @@ declare class ThunderBridge {
|
|
|
262
280
|
* the watcher needs goes in `sealed`, which the gateway cannot read
|
|
263
281
|
*/
|
|
264
282
|
watchPayment(params: WatchPaymentParams): Promise<TriggerEvent>;
|
|
283
|
+
/**
|
|
284
|
+
* What this payment is called, which you can work out before any gateway has
|
|
285
|
+
* heard of it. Every gateway you hand the same invoice to answers with the same
|
|
286
|
+
* name, so watching at several of them adds up to one payment rather than
|
|
287
|
+
* several, and no gateway's key is in the answer. Null when no secret was given,
|
|
288
|
+
* because then the gateway names the payment and only it can
|
|
289
|
+
*/
|
|
290
|
+
nameFor(paymentHash: string): Promise<string | null>;
|
|
265
291
|
/**
|
|
266
292
|
* Follow every payment made to one trigger, replayed from the recent ones on
|
|
267
293
|
* connect and then live, reconnecting on its own until the returned function
|
|
@@ -272,6 +298,7 @@ declare class ThunderBridge {
|
|
|
272
298
|
private wsTicket;
|
|
273
299
|
private sending;
|
|
274
300
|
private reading;
|
|
301
|
+
private speaking;
|
|
275
302
|
private proven;
|
|
276
303
|
private checked;
|
|
277
304
|
}
|
|
@@ -324,7 +351,6 @@ interface BankRailConfig {
|
|
|
324
351
|
/** Up to ten digits, for accounting systems that still want one */
|
|
325
352
|
variableSymbol?: (order: Order) => string | undefined;
|
|
326
353
|
webhookUrl?: string;
|
|
327
|
-
webhookSecret?: string;
|
|
328
354
|
/** Register on a gateway you do not own anyway, on the terms `bankTransfer` sets out */
|
|
329
355
|
allowPublicGateway?: boolean;
|
|
330
356
|
/** What `Leg.rail` reads, for a shop running more than one account */
|
|
@@ -345,7 +371,6 @@ interface LightningRailConfig {
|
|
|
345
371
|
*/
|
|
346
372
|
idempotencyKey?: (order: Order) => string | undefined;
|
|
347
373
|
webhookUrl?: string;
|
|
348
|
-
webhookSecret?: string;
|
|
349
374
|
/** What `Leg.rail` reads, for a shop running more than one wallet */
|
|
350
375
|
name?: string;
|
|
351
376
|
}
|
|
@@ -361,7 +386,6 @@ interface BlindLightningRailConfig {
|
|
|
361
386
|
/** Only a watched leg has anywhere to carry this */
|
|
362
387
|
sealed?: (order: Order) => string | Promise<string>;
|
|
363
388
|
webhookUrl?: string;
|
|
364
|
-
webhookSecret?: string;
|
|
365
389
|
/**
|
|
366
390
|
* Where your own `lightningVerifyEndpoint` is mounted, and the secret it
|
|
367
391
|
* unseals with. Set both and the gateway is handed your URL rather than the
|
|
@@ -397,4 +421,4 @@ declare function lightningRail(config: LightningRailConfig): Rail;
|
|
|
397
421
|
*/
|
|
398
422
|
declare function blindLightningRail(config: BlindLightningRailConfig): Rail;
|
|
399
423
|
|
|
400
|
-
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, ThunderBridge as T, type
|
|
424
|
+
export { type BankRailConfig as B, type CreatePaymentParams as C, type FollowOptions as F, type Leg as L, type Order as O, type Payment as P, type Quote as Q, type Rail as R, type Settlement as S, ThunderBridge as T, type WatchPaymentParams as W, type ThunderBridgeOptions as a, type TriggerEvent as b, type WaitOptions as c, type WalletFailure as d, type CreateOptions as e, type CreateQuoteParams as f, type LightningRailConfig as g, type PaymentKind as h, type PaymentStatus as i, type WalletReason as j, bankRail as k, lightningRail as l, type BlindLightningRailConfig as m, blindLightningRail as n };
|
package/dist/server.cjs
CHANGED
|
@@ -402,20 +402,22 @@ var REDIRECTS = [301, 302, 303, 307, 308];
|
|
|
402
402
|
var KEEPS_THE_METHOD = [307, 308];
|
|
403
403
|
var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
|
|
404
404
|
var BODY_LIMIT_BYTES = 262144;
|
|
405
|
+
var throughFetch = (url, sent, signal) => fetch(url, {
|
|
406
|
+
method: sent.method ?? "GET",
|
|
407
|
+
headers: sent.headers ?? {},
|
|
408
|
+
body: sent.body,
|
|
409
|
+
redirect: "manual",
|
|
410
|
+
signal
|
|
411
|
+
});
|
|
412
|
+
var send = throughFetch;
|
|
405
413
|
async function ask(url, sent = {}) {
|
|
406
414
|
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
|
|
407
415
|
const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
|
|
408
416
|
let target = url;
|
|
409
417
|
let carried = sent;
|
|
410
418
|
for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
|
|
411
|
-
await
|
|
412
|
-
const response = await
|
|
413
|
-
method: carried.method ?? "GET",
|
|
414
|
-
headers: carried.headers ?? {},
|
|
415
|
-
body: carried.body,
|
|
416
|
-
redirect: "manual",
|
|
417
|
-
signal
|
|
418
|
-
});
|
|
419
|
+
const at = await addressesToReach(target);
|
|
420
|
+
const response = await send(target, carried, signal, at);
|
|
419
421
|
if (!REDIRECTS.includes(response.status)) return await answerOf(response);
|
|
420
422
|
const location = response.headers.get("location");
|
|
421
423
|
if (location === null) {
|
|
@@ -429,7 +431,15 @@ async function ask(url, sent = {}) {
|
|
|
429
431
|
}
|
|
430
432
|
throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
|
|
431
433
|
}
|
|
432
|
-
async function
|
|
434
|
+
async function addressesToReach(url) {
|
|
435
|
+
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
436
|
+
const found = await resolved(url);
|
|
437
|
+
if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
|
|
438
|
+
throw new Error(`${url} resolves to an address we do not reach`);
|
|
439
|
+
}
|
|
440
|
+
return found;
|
|
441
|
+
}
|
|
442
|
+
async function resolved(url) {
|
|
433
443
|
const { lookup } = await import("dns/promises");
|
|
434
444
|
const { setTimeout: sleep } = await import("timers/promises");
|
|
435
445
|
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
@@ -437,11 +447,7 @@ async function resolvesNothingPrivate(url) {
|
|
|
437
447
|
lookup(host, { all: true, verbatim: true }).catch(() => []),
|
|
438
448
|
sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
|
|
439
449
|
]);
|
|
440
|
-
return found
|
|
441
|
-
}
|
|
442
|
-
async function refuseUnlessPublic(url) {
|
|
443
|
-
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
444
|
-
if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
|
|
450
|
+
return found ?? [];
|
|
445
451
|
}
|
|
446
452
|
async function answerOf(response) {
|
|
447
453
|
const { status, ok, headers } = response;
|
|
@@ -808,21 +814,21 @@ async function mintThroughGateway(config, address, amountMsat, nonce) {
|
|
|
808
814
|
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
809
815
|
}
|
|
810
816
|
async function mintBlind(config, address, amountMsat) {
|
|
811
|
-
const
|
|
812
|
-
const minted = { ...
|
|
817
|
+
const resolved2 = await resolve([address], amountMsat);
|
|
818
|
+
const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
|
|
813
819
|
const locked = config.sealed;
|
|
814
820
|
try {
|
|
815
821
|
await config.gateway.watchPayment({
|
|
816
|
-
paymentHash:
|
|
817
|
-
verifyUrl:
|
|
818
|
-
expiresAt:
|
|
822
|
+
paymentHash: resolved2.paymentHash,
|
|
823
|
+
verifyUrl: resolved2.verifyUrl,
|
|
824
|
+
expiresAt: resolved2.expiresAt,
|
|
819
825
|
trigger: config.watchSecret,
|
|
820
826
|
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
821
827
|
});
|
|
822
828
|
} catch (refused) {
|
|
823
829
|
if (!alreadyWatched(refused)) throw refused;
|
|
824
830
|
}
|
|
825
|
-
return { bolt11:
|
|
831
|
+
return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
|
|
826
832
|
}
|
|
827
833
|
function alreadyWatched(refused) {
|
|
828
834
|
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
@@ -908,27 +914,26 @@ async function relayedVerifyUrl(endpoint, wallet, secret) {
|
|
|
908
914
|
var LIGHTNING = "lightning";
|
|
909
915
|
function blindLightningRail(config) {
|
|
910
916
|
return async (order) => {
|
|
911
|
-
const
|
|
917
|
+
const resolved2 = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
912
918
|
const relay = config.relayVerifyThrough;
|
|
913
919
|
const watched = await config.gateway.watchPayment({
|
|
914
|
-
paymentHash:
|
|
920
|
+
paymentHash: resolved2.paymentHash,
|
|
915
921
|
verifyUrl: relay ? await relayedVerifyUrl(
|
|
916
922
|
relay.endpoint,
|
|
917
|
-
{ url:
|
|
923
|
+
{ url: resolved2.verifyUrl, hash: resolved2.paymentHash },
|
|
918
924
|
relay.secret
|
|
919
|
-
) :
|
|
920
|
-
expiresAt:
|
|
925
|
+
) : resolved2.verifyUrl,
|
|
926
|
+
expiresAt: resolved2.expiresAt,
|
|
921
927
|
trigger: config.trigger,
|
|
922
928
|
sealed: await config.sealed?.(order),
|
|
923
|
-
webhookUrl: config.webhookUrl
|
|
924
|
-
webhookSecret: config.webhookSecret
|
|
929
|
+
webhookUrl: config.webhookUrl
|
|
925
930
|
});
|
|
926
931
|
return {
|
|
927
932
|
id: watched.id,
|
|
928
933
|
rail: config.name ?? LIGHTNING,
|
|
929
|
-
scan:
|
|
930
|
-
qr: encodeForQr(
|
|
931
|
-
expiresAt:
|
|
934
|
+
scan: resolved2.bolt11,
|
|
935
|
+
qr: encodeForQr(resolved2.bolt11),
|
|
936
|
+
expiresAt: resolved2.expiresAt
|
|
932
937
|
};
|
|
933
938
|
};
|
|
934
939
|
}
|
package/dist/server.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge } from './rail-
|
|
2
|
-
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-
|
|
1
|
+
import { T as ThunderBridge } from './rail-CL9QkiHo.cjs';
|
|
2
|
+
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-CL9QkiHo.cjs';
|
|
3
3
|
|
|
4
4
|
interface TriggerConfig {
|
|
5
5
|
/** The gateway that quotes the addresses and mints the invoice */
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge } from './rail-
|
|
2
|
-
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-
|
|
1
|
+
import { T as ThunderBridge } from './rail-CL9QkiHo.js';
|
|
2
|
+
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-CL9QkiHo.js';
|
|
3
3
|
|
|
4
4
|
interface TriggerConfig {
|
|
5
5
|
/** The gateway that quotes the addresses and mints the invoice */
|
package/dist/server.js
CHANGED
|
@@ -363,20 +363,22 @@ var REDIRECTS = [301, 302, 303, 307, 308];
|
|
|
363
363
|
var KEEPS_THE_METHOD = [307, 308];
|
|
364
364
|
var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
|
|
365
365
|
var BODY_LIMIT_BYTES = 262144;
|
|
366
|
+
var throughFetch = (url, sent, signal) => fetch(url, {
|
|
367
|
+
method: sent.method ?? "GET",
|
|
368
|
+
headers: sent.headers ?? {},
|
|
369
|
+
body: sent.body,
|
|
370
|
+
redirect: "manual",
|
|
371
|
+
signal
|
|
372
|
+
});
|
|
373
|
+
var send = throughFetch;
|
|
366
374
|
async function ask(url, sent = {}) {
|
|
367
375
|
const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
|
|
368
376
|
const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
|
|
369
377
|
let target = url;
|
|
370
378
|
let carried = sent;
|
|
371
379
|
for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
|
|
372
|
-
await
|
|
373
|
-
const response = await
|
|
374
|
-
method: carried.method ?? "GET",
|
|
375
|
-
headers: carried.headers ?? {},
|
|
376
|
-
body: carried.body,
|
|
377
|
-
redirect: "manual",
|
|
378
|
-
signal
|
|
379
|
-
});
|
|
380
|
+
const at = await addressesToReach(target);
|
|
381
|
+
const response = await send(target, carried, signal, at);
|
|
380
382
|
if (!REDIRECTS.includes(response.status)) return await answerOf(response);
|
|
381
383
|
const location = response.headers.get("location");
|
|
382
384
|
if (location === null) {
|
|
@@ -390,7 +392,15 @@ async function ask(url, sent = {}) {
|
|
|
390
392
|
}
|
|
391
393
|
throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
|
|
392
394
|
}
|
|
393
|
-
async function
|
|
395
|
+
async function addressesToReach(url) {
|
|
396
|
+
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
397
|
+
const found = await resolved(url);
|
|
398
|
+
if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
|
|
399
|
+
throw new Error(`${url} resolves to an address we do not reach`);
|
|
400
|
+
}
|
|
401
|
+
return found;
|
|
402
|
+
}
|
|
403
|
+
async function resolved(url) {
|
|
394
404
|
const { lookup } = await import("dns/promises");
|
|
395
405
|
const { setTimeout: sleep } = await import("timers/promises");
|
|
396
406
|
const host = new URL(url).hostname.replace(/^\[|]$/g, "");
|
|
@@ -398,11 +408,7 @@ async function resolvesNothingPrivate(url) {
|
|
|
398
408
|
lookup(host, { all: true, verbatim: true }).catch(() => []),
|
|
399
409
|
sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
|
|
400
410
|
]);
|
|
401
|
-
return found
|
|
402
|
-
}
|
|
403
|
-
async function refuseUnlessPublic(url) {
|
|
404
|
-
if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
|
|
405
|
-
if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
|
|
411
|
+
return found ?? [];
|
|
406
412
|
}
|
|
407
413
|
async function answerOf(response) {
|
|
408
414
|
const { status, ok, headers } = response;
|
|
@@ -769,21 +775,21 @@ async function mintThroughGateway(config, address, amountMsat, nonce) {
|
|
|
769
775
|
return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
|
|
770
776
|
}
|
|
771
777
|
async function mintBlind(config, address, amountMsat) {
|
|
772
|
-
const
|
|
773
|
-
const minted = { ...
|
|
778
|
+
const resolved2 = await resolve([address], amountMsat);
|
|
779
|
+
const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
|
|
774
780
|
const locked = config.sealed;
|
|
775
781
|
try {
|
|
776
782
|
await config.gateway.watchPayment({
|
|
777
|
-
paymentHash:
|
|
778
|
-
verifyUrl:
|
|
779
|
-
expiresAt:
|
|
783
|
+
paymentHash: resolved2.paymentHash,
|
|
784
|
+
verifyUrl: resolved2.verifyUrl,
|
|
785
|
+
expiresAt: resolved2.expiresAt,
|
|
780
786
|
trigger: config.watchSecret,
|
|
781
787
|
sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
|
|
782
788
|
});
|
|
783
789
|
} catch (refused) {
|
|
784
790
|
if (!alreadyWatched(refused)) throw refused;
|
|
785
791
|
}
|
|
786
|
-
return { bolt11:
|
|
792
|
+
return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
|
|
787
793
|
}
|
|
788
794
|
function alreadyWatched(refused) {
|
|
789
795
|
return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
|
|
@@ -869,27 +875,26 @@ async function relayedVerifyUrl(endpoint, wallet, secret) {
|
|
|
869
875
|
var LIGHTNING = "lightning";
|
|
870
876
|
function blindLightningRail(config) {
|
|
871
877
|
return async (order) => {
|
|
872
|
-
const
|
|
878
|
+
const resolved2 = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
873
879
|
const relay = config.relayVerifyThrough;
|
|
874
880
|
const watched = await config.gateway.watchPayment({
|
|
875
|
-
paymentHash:
|
|
881
|
+
paymentHash: resolved2.paymentHash,
|
|
876
882
|
verifyUrl: relay ? await relayedVerifyUrl(
|
|
877
883
|
relay.endpoint,
|
|
878
|
-
{ url:
|
|
884
|
+
{ url: resolved2.verifyUrl, hash: resolved2.paymentHash },
|
|
879
885
|
relay.secret
|
|
880
|
-
) :
|
|
881
|
-
expiresAt:
|
|
886
|
+
) : resolved2.verifyUrl,
|
|
887
|
+
expiresAt: resolved2.expiresAt,
|
|
882
888
|
trigger: config.trigger,
|
|
883
889
|
sealed: await config.sealed?.(order),
|
|
884
|
-
webhookUrl: config.webhookUrl
|
|
885
|
-
webhookSecret: config.webhookSecret
|
|
890
|
+
webhookUrl: config.webhookUrl
|
|
886
891
|
});
|
|
887
892
|
return {
|
|
888
893
|
id: watched.id,
|
|
889
894
|
rail: config.name ?? LIGHTNING,
|
|
890
|
-
scan:
|
|
891
|
-
qr: encodeForQr(
|
|
892
|
-
expiresAt:
|
|
895
|
+
scan: resolved2.bolt11,
|
|
896
|
+
qr: encodeForQr(resolved2.bolt11),
|
|
897
|
+
expiresAt: resolved2.expiresAt
|
|
893
898
|
};
|
|
894
899
|
};
|
|
895
900
|
}
|
package/openapi.yaml
CHANGED
|
@@ -2,7 +2,7 @@ openapi: 3.1.0
|
|
|
2
2
|
|
|
3
3
|
info:
|
|
4
4
|
title: thunder-bridge, the endpoint your own service serves
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 1.0.0
|
|
6
6
|
license:
|
|
7
7
|
name: MIT
|
|
8
8
|
identifier: MIT
|
|
@@ -207,6 +207,48 @@ paths:
|
|
|
207
207
|
schema:
|
|
208
208
|
$ref: "#/components/schemas/unsettled"
|
|
209
209
|
|
|
210
|
+
post:
|
|
211
|
+
tags: [bank-transfer]
|
|
212
|
+
operationId: answerVerifyChallenge
|
|
213
|
+
summary: Agree to be polled, before the gateway will watch anything pointed here
|
|
214
|
+
description: |
|
|
215
|
+
The gateway will not poll a URL a caller merely named. It POSTs a nonce here first and
|
|
216
|
+
watches nothing unless this endpoint hands the same nonce back, which is how a caller
|
|
217
|
+
shows the endpoint consented to the traffic rather than being volunteered for it. A
|
|
218
|
+
registration whose verify URL will not answer is refused with `verify-unconsented`.
|
|
219
|
+
|
|
220
|
+
Every verify endpoint the SDK builds answers this as its first line, before it parses
|
|
221
|
+
its own query, so `bankVerifyEndpoint`, `lightningVerifyEndpoint` and anything wrapping
|
|
222
|
+
`answerVerifyChallengeRequest` already do it.
|
|
223
|
+
|
|
224
|
+
There is nothing to sign. Echoing a nonce grants the asker nothing, so this holds no
|
|
225
|
+
secret and checks none.
|
|
226
|
+
requestBody:
|
|
227
|
+
required: true
|
|
228
|
+
content:
|
|
229
|
+
application/json:
|
|
230
|
+
schema:
|
|
231
|
+
type: object
|
|
232
|
+
properties:
|
|
233
|
+
type:
|
|
234
|
+
type: string
|
|
235
|
+
enum: [verify-challenge]
|
|
236
|
+
nonce:
|
|
237
|
+
type: string
|
|
238
|
+
pattern: "^[0-9a-f]{64}$"
|
|
239
|
+
required: [type, nonce]
|
|
240
|
+
responses:
|
|
241
|
+
"200":
|
|
242
|
+
description: The same nonce, and nothing else.
|
|
243
|
+
content:
|
|
244
|
+
application/json:
|
|
245
|
+
schema:
|
|
246
|
+
type: object
|
|
247
|
+
properties:
|
|
248
|
+
nonce:
|
|
249
|
+
type: string
|
|
250
|
+
required: [nonce]
|
|
251
|
+
|
|
210
252
|
components:
|
|
211
253
|
schemas:
|
|
212
254
|
settlement:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunder-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Trustless JavaScript client for the Thunder Bridge Lightning payment gateway. Proves the invoice came from your own wallet before the payer sees it.",
|
|
5
5
|
"author": "i-am-fatik",
|
|
6
6
|
"homepage": "https://agora.gripe/en/tools/thunder-bridge",
|