thunder-bridge 0.8.6 → 0.8.8
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 +49 -0
- package/dist/index.cjs +9 -5
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +9 -5
- package/dist/{rail-CdKeyVGn.d.cts → rail-J8QoYbSr.d.cts} +10 -0
- package/dist/{rail-CdKeyVGn.d.ts → rail-J8QoYbSr.d.ts} +10 -0
- package/dist/server.cjs +117 -8
- package/dist/server.d.cts +42 -3
- package/dist/server.d.ts +42 -3
- package/dist/server.js +114 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -112,6 +112,8 @@ them and this table does not repeat them.
|
|
|
112
112
|
| `bankTransfer(params)` | register a Czech QR platba as a watched payment. Refuses a gateway that serves strangers |
|
|
113
113
|
| `bankVerifyEndpoint(config)` | the other half, the LUD-21 shape backed by your own statement |
|
|
114
114
|
| `fioStatement(config)` | a `Statement` reading a Fio account, several tokens used strictly in turn |
|
|
115
|
+
| `lightningVerifyEndpoint(config)` | the same shape for Lightning, asking the wallet on the gateway's behalf. From `thunder-bridge/server` |
|
|
116
|
+
| `relayedVerifyUrl(mount, wallet, secret)` | the URL to hand the gateway instead of the wallet's, with the wallet's sealed inside |
|
|
115
117
|
|
|
116
118
|
What your service answers once those handlers are mounted is written out in
|
|
117
119
|
[`openapi.yaml`](openapi.yaml), shipped with this package.
|
|
@@ -248,6 +250,53 @@ already in the account:
|
|
|
248
250
|
Neither has been seen with Fio, which forwards the message untouched. Check it
|
|
249
251
|
against the banks your payers actually use before you promise them a rail.
|
|
250
252
|
|
|
253
|
+
## Making the gateway poll nobody but you
|
|
254
|
+
|
|
255
|
+
By default a Lightning watch hands the gateway the wallet's own verify URL, so the
|
|
256
|
+
gateway polls `blink.sv` or `coinos.io` directly and its logs, its ledger and its
|
|
257
|
+
peers all carry that domain. If you would rather it never touched a third party and
|
|
258
|
+
never learned which provider your recipient uses, put your own endpoint in between.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { blindLightningRail, lightningVerifyEndpoint } from "thunder-bridge/server";
|
|
262
|
+
|
|
263
|
+
app.get("/verify/lightning", (context) =>
|
|
264
|
+
lightningVerifyEndpoint({ secret: RELAY_SECRET, pollEverySecs: 5 })(context.req.raw),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const rail = blindLightningRail({
|
|
268
|
+
gateway,
|
|
269
|
+
lnAddresses: ["you@blink.sv"],
|
|
270
|
+
amountMsat: (order) => order.amountMinor * 40,
|
|
271
|
+
relayVerifyThrough: { endpoint: "https://shop.example/verify/lightning", secret: RELAY_SECRET },
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The wallet's URL is sealed into the query with your secret, so what the gateway
|
|
276
|
+
stores and replicates is a blob it cannot read. It polls you, you ask the wallet,
|
|
277
|
+
and the preimage still comes from the recipient's own server and still has to hash
|
|
278
|
+
to the payment hash, so standing in the middle buys privacy and pacing without
|
|
279
|
+
making you something anyone has to trust. A wallet you cannot reach answers `502`
|
|
280
|
+
rather than "not settled", because those are different claims.
|
|
281
|
+
|
|
282
|
+
Both rails then run through endpoints of yours, on a pace you set, and the gateway
|
|
283
|
+
is only ever talking to servers that asked to be talked to. It costs you a service
|
|
284
|
+
that has to stay up: a browser-only integration cannot do this, and should keep
|
|
285
|
+
letting the gateway poll the wallet.
|
|
286
|
+
|
|
287
|
+
### How often the gateway asks
|
|
288
|
+
|
|
289
|
+
Your endpoint decides, not the gateway. `bankVerifyEndpoint` answers with
|
|
290
|
+
`Cache-Control: max-age=30`, and the gateway uses that as the interval for every
|
|
291
|
+
payment on your host. Set `pollEverySecs` to whatever your bank's own refresh makes
|
|
292
|
+
sensible: reading a statement that moves once an hour every five seconds only burns
|
|
293
|
+
your rate limit.
|
|
294
|
+
|
|
295
|
+
The gateway also asks the URL once, before it accepts the watch, and refuses with
|
|
296
|
+
`424` if it does not answer this shape. So deploy the endpoint first and register
|
|
297
|
+
second. That is what stops anyone pointing a gateway at a server that never asked to
|
|
298
|
+
be polled for three days.
|
|
299
|
+
|
|
251
300
|
## What is still trusted
|
|
252
301
|
|
|
253
302
|
- **The gateway chooses which of your addresses gets paid.** Nothing here can
|
package/dist/index.cjs
CHANGED
|
@@ -1290,6 +1290,7 @@ function minorScaleOf(currency) {
|
|
|
1290
1290
|
// src/bank.ts
|
|
1291
1291
|
var DEFAULT_CURRENCY = "CZK";
|
|
1292
1292
|
var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
|
|
1293
|
+
var DEFAULT_POLL_EVERY_SECS = 30;
|
|
1293
1294
|
var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
|
|
1294
1295
|
var FORBIDDEN_IN_SPD = "*";
|
|
1295
1296
|
async function bankTransfer(params) {
|
|
@@ -1316,6 +1317,9 @@ async function bankTransfer(params) {
|
|
|
1316
1317
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1317
1318
|
}
|
|
1318
1319
|
function bankVerifyEndpoint(config) {
|
|
1320
|
+
const paced = {
|
|
1321
|
+
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1322
|
+
};
|
|
1319
1323
|
return async (request) => {
|
|
1320
1324
|
const asked = readQuery(new URL(request.url));
|
|
1321
1325
|
if (asked === null) return Response.json({ settled: false }, { status: 400 });
|
|
@@ -1326,11 +1330,11 @@ function bankVerifyEndpoint(config) {
|
|
|
1326
1330
|
}
|
|
1327
1331
|
const since = unixNow() - (config.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
|
|
1328
1332
|
const landed = (await config.statement(since)).some((credit) => pays(credit, asked));
|
|
1329
|
-
if (!landed) return Response.json({ settled: false });
|
|
1330
|
-
return Response.json(
|
|
1331
|
-
settled: true,
|
|
1332
|
-
|
|
1333
|
-
|
|
1333
|
+
if (!landed) return Response.json({ settled: false }, { headers: paced });
|
|
1334
|
+
return Response.json(
|
|
1335
|
+
{ settled: true, preimage: await hmacHex(config.secret, `preimage|${subject}`) },
|
|
1336
|
+
{ headers: paced }
|
|
1337
|
+
);
|
|
1334
1338
|
};
|
|
1335
1339
|
}
|
|
1336
1340
|
function readQuery(url) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-
|
|
2
|
-
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-
|
|
1
|
+
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-J8QoYbSr.cjs';
|
|
2
|
+
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-J8QoYbSr.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Encrypt what the watcher needs and the gateway must not have. The gateway
|
|
@@ -98,6 +98,13 @@ interface BankVerifyConfig {
|
|
|
98
98
|
statement: Statement;
|
|
99
99
|
/** How far back a credit still counts, seven days by default */
|
|
100
100
|
lookBackSecs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
103
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
104
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
105
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
106
|
+
*/
|
|
107
|
+
pollEverySecs?: number;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
110
|
* Ask for a bank transfer and put it under the gateway's watch, so it settles
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-
|
|
2
|
-
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-
|
|
1
|
+
import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-J8QoYbSr.js';
|
|
2
|
+
export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-J8QoYbSr.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Encrypt what the watcher needs and the gateway must not have. The gateway
|
|
@@ -98,6 +98,13 @@ interface BankVerifyConfig {
|
|
|
98
98
|
statement: Statement;
|
|
99
99
|
/** How far back a credit still counts, seven days by default */
|
|
100
100
|
lookBackSecs?: number;
|
|
101
|
+
/**
|
|
102
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
103
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
104
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
105
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
106
|
+
*/
|
|
107
|
+
pollEverySecs?: number;
|
|
101
108
|
}
|
|
102
109
|
/**
|
|
103
110
|
* Ask for a bank transfer and put it under the gateway's watch, so it settles
|
package/dist/index.js
CHANGED
|
@@ -1218,6 +1218,7 @@ function minorScaleOf(currency) {
|
|
|
1218
1218
|
// src/bank.ts
|
|
1219
1219
|
var DEFAULT_CURRENCY = "CZK";
|
|
1220
1220
|
var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
|
|
1221
|
+
var DEFAULT_POLL_EVERY_SECS = 30;
|
|
1221
1222
|
var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
|
|
1222
1223
|
var FORBIDDEN_IN_SPD = "*";
|
|
1223
1224
|
async function bankTransfer(params) {
|
|
@@ -1244,6 +1245,9 @@ async function bankTransfer(params) {
|
|
|
1244
1245
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1245
1246
|
}
|
|
1246
1247
|
function bankVerifyEndpoint(config) {
|
|
1248
|
+
const paced = {
|
|
1249
|
+
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
1250
|
+
};
|
|
1247
1251
|
return async (request) => {
|
|
1248
1252
|
const asked = readQuery(new URL(request.url));
|
|
1249
1253
|
if (asked === null) return Response.json({ settled: false }, { status: 400 });
|
|
@@ -1254,11 +1258,11 @@ function bankVerifyEndpoint(config) {
|
|
|
1254
1258
|
}
|
|
1255
1259
|
const since = unixNow() - (config.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
|
|
1256
1260
|
const landed = (await config.statement(since)).some((credit) => pays(credit, asked));
|
|
1257
|
-
if (!landed) return Response.json({ settled: false });
|
|
1258
|
-
return Response.json(
|
|
1259
|
-
settled: true,
|
|
1260
|
-
|
|
1261
|
-
|
|
1261
|
+
if (!landed) return Response.json({ settled: false }, { headers: paced });
|
|
1262
|
+
return Response.json(
|
|
1263
|
+
{ settled: true, preimage: await hmacHex(config.secret, `preimage|${subject}`) },
|
|
1264
|
+
{ headers: paced }
|
|
1265
|
+
);
|
|
1262
1266
|
};
|
|
1263
1267
|
}
|
|
1264
1268
|
function readQuery(url) {
|
|
@@ -362,6 +362,16 @@ interface BlindLightningRailConfig {
|
|
|
362
362
|
sealed?: (order: Order) => string | Promise<string>;
|
|
363
363
|
webhookUrl?: string;
|
|
364
364
|
webhookSecret?: string;
|
|
365
|
+
/**
|
|
366
|
+
* Where your own `lightningVerifyEndpoint` is mounted, and the secret it
|
|
367
|
+
* unseals with. Set both and the gateway is handed your URL rather than the
|
|
368
|
+
* wallet's, so it polls you, never a third party, and learns nothing about
|
|
369
|
+
* which provider the recipient uses. Leave them out and it polls the wallet
|
|
370
|
+
*/
|
|
371
|
+
relayVerifyThrough?: {
|
|
372
|
+
endpoint: string;
|
|
373
|
+
secret: string;
|
|
374
|
+
};
|
|
365
375
|
/** What `Leg.rail` reads, for a shop running more than one wallet */
|
|
366
376
|
name?: string;
|
|
367
377
|
}
|
|
@@ -362,6 +362,16 @@ interface BlindLightningRailConfig {
|
|
|
362
362
|
sealed?: (order: Order) => string | Promise<string>;
|
|
363
363
|
webhookUrl?: string;
|
|
364
364
|
webhookSecret?: string;
|
|
365
|
+
/**
|
|
366
|
+
* Where your own `lightningVerifyEndpoint` is mounted, and the secret it
|
|
367
|
+
* unseals with. Set both and the gateway is handed your URL rather than the
|
|
368
|
+
* wallet's, so it polls you, never a third party, and learns nothing about
|
|
369
|
+
* which provider the recipient uses. Leave them out and it polls the wallet
|
|
370
|
+
*/
|
|
371
|
+
relayVerifyThrough?: {
|
|
372
|
+
endpoint: string;
|
|
373
|
+
secret: string;
|
|
374
|
+
};
|
|
365
375
|
/** What `Leg.rail` reads, for a shop running more than one wallet */
|
|
366
376
|
name?: string;
|
|
367
377
|
}
|
package/dist/server.cjs
CHANGED
|
@@ -31,7 +31,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var server_exports = {};
|
|
32
32
|
__export(server_exports, {
|
|
33
33
|
blindLightningRail: () => blindLightningRail,
|
|
34
|
-
|
|
34
|
+
lightningVerifyEndpoint: () => lightningVerifyEndpoint,
|
|
35
|
+
lnurlPayEndpoint: () => lnurlPayEndpoint,
|
|
36
|
+
relayedVerifyUrl: () => relayedVerifyUrl
|
|
35
37
|
});
|
|
36
38
|
module.exports = __toCommonJS(server_exports);
|
|
37
39
|
|
|
@@ -41,6 +43,16 @@ function bytesToHex(bytes) {
|
|
|
41
43
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
42
44
|
return hex;
|
|
43
45
|
}
|
|
46
|
+
function hexToBytes(hex) {
|
|
47
|
+
const bytes = new Uint8Array(hex.length >> 1);
|
|
48
|
+
for (let at = 0; at < bytes.length; at++) {
|
|
49
|
+
bytes[at] = parseInt(hex.slice(at * 2, at * 2 + 2), 16);
|
|
50
|
+
}
|
|
51
|
+
return bytes;
|
|
52
|
+
}
|
|
53
|
+
function isHex(text2) {
|
|
54
|
+
return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
|
|
55
|
+
}
|
|
44
56
|
|
|
45
57
|
// ../core/hmac.ts
|
|
46
58
|
async function hmacHex(secret, payload) {
|
|
@@ -248,6 +260,10 @@ function decodeInvoice(bolt11) {
|
|
|
248
260
|
expiresAt: expiryOf(parts.words, tagged)
|
|
249
261
|
};
|
|
250
262
|
}
|
|
263
|
+
function preimageMatchesHash(preimage, paymentHash) {
|
|
264
|
+
if (!isHex(preimage)) return false;
|
|
265
|
+
return bytesToHex(sha256(hexToBytes(preimage))) === paymentHash.toLowerCase();
|
|
266
|
+
}
|
|
251
267
|
function splitBech32(bolt11) {
|
|
252
268
|
const lower = bolt11.toLowerCase();
|
|
253
269
|
if (isBolt12Encoding(lower)) return null;
|
|
@@ -428,19 +444,19 @@ async function refuseUnlessPublic(url) {
|
|
|
428
444
|
if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
|
|
429
445
|
}
|
|
430
446
|
async function answerOf(response) {
|
|
431
|
-
const { status, ok } = response;
|
|
447
|
+
const { status, ok, headers } = response;
|
|
432
448
|
const reader = response.body?.getReader();
|
|
433
|
-
if (!reader) return { status, ok, body: "", truncated: false };
|
|
449
|
+
if (!reader) return { status, ok, body: "", truncated: false, headers };
|
|
434
450
|
const read = [];
|
|
435
451
|
let bytes = 0;
|
|
436
452
|
while (bytes <= BODY_LIMIT_BYTES) {
|
|
437
453
|
const { done, value } = await reader.read();
|
|
438
|
-
if (done) return { status, ok, body: text(read), truncated: false };
|
|
454
|
+
if (done) return { status, ok, body: text(read), truncated: false, headers };
|
|
439
455
|
read.push(value);
|
|
440
456
|
bytes += value.length;
|
|
441
457
|
}
|
|
442
458
|
await reader.cancel();
|
|
443
|
-
return { status, ok, body: text(read), truncated: true };
|
|
459
|
+
return { status, ok, body: text(read), truncated: true, headers };
|
|
444
460
|
}
|
|
445
461
|
function withoutBody(sent) {
|
|
446
462
|
const headers = { ...sent.headers };
|
|
@@ -478,6 +494,10 @@ var NoWalletAvailable = class extends Error {
|
|
|
478
494
|
// ../core/lnurl.ts
|
|
479
495
|
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
480
496
|
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
497
|
+
var MIN_PACE_SECS = 1;
|
|
498
|
+
var MAX_PACE_SECS = 3600;
|
|
499
|
+
var MIN_PER_SECOND = 0.01;
|
|
500
|
+
var MAX_PER_SECOND = 100;
|
|
481
501
|
async function resolve(addresses, amountMsat) {
|
|
482
502
|
const served = await firstThatServes(
|
|
483
503
|
addresses,
|
|
@@ -565,6 +585,31 @@ function decodeIssued(address, bolt11) {
|
|
|
565
585
|
}
|
|
566
586
|
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
567
587
|
}
|
|
588
|
+
async function checkSettled(verifyUrl, paymentHash) {
|
|
589
|
+
const answer = await answeredJson(verifyUrl);
|
|
590
|
+
const asked = {
|
|
591
|
+
pace: paceAskedFor(answer.headers),
|
|
592
|
+
ceiling: ceilingAskedFor(answer.headers)
|
|
593
|
+
};
|
|
594
|
+
if (!answer.said.settled || !answer.said.preimage) return { preimage: null, ...asked };
|
|
595
|
+
if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
|
|
596
|
+
throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
|
|
597
|
+
}
|
|
598
|
+
return { preimage: answer.said.preimage, ...asked };
|
|
599
|
+
}
|
|
600
|
+
function paceAskedFor(headers) {
|
|
601
|
+
const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
|
|
602
|
+
if (!asked) return null;
|
|
603
|
+
return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
|
|
604
|
+
}
|
|
605
|
+
function ceilingAskedFor(headers) {
|
|
606
|
+
const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
|
|
607
|
+
if (!asked) return null;
|
|
608
|
+
const perWindow = Number(asked[1]);
|
|
609
|
+
const window = asked[2] === void 0 ? 1 : Number(asked[2]);
|
|
610
|
+
if (perWindow === 0 || window === 0) return null;
|
|
611
|
+
return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
|
|
612
|
+
}
|
|
568
613
|
function cannotReleaseAPreimage(host) {
|
|
569
614
|
const lowered = host.toLowerCase();
|
|
570
615
|
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
@@ -583,12 +628,15 @@ function splitAddress(address) {
|
|
|
583
628
|
return [user, domain];
|
|
584
629
|
}
|
|
585
630
|
async function fetchJson(url, deadline) {
|
|
631
|
+
return (await answeredJson(url, deadline)).said;
|
|
632
|
+
}
|
|
633
|
+
async function answeredJson(url, deadline) {
|
|
586
634
|
const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
|
|
587
635
|
if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
|
|
588
636
|
if (answer.truncated) {
|
|
589
637
|
throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
|
|
590
638
|
}
|
|
591
|
-
return JSON.parse(answer.body);
|
|
639
|
+
return { said: JSON.parse(answer.body), headers: answer.headers };
|
|
592
640
|
}
|
|
593
641
|
|
|
594
642
|
// ../core/sealed.ts
|
|
@@ -610,6 +658,22 @@ async function seal(secret, plaintext) {
|
|
|
610
658
|
joined.set(cipher, iv.length);
|
|
611
659
|
return `${VERSION}.${toBase64Url(joined)}`;
|
|
612
660
|
}
|
|
661
|
+
async function unseal(secret, sealed) {
|
|
662
|
+
const key = await keyFor(secret);
|
|
663
|
+
if (!sealed.startsWith(`${VERSION}.`)) return null;
|
|
664
|
+
const joined = fromBase64Url(sealed.slice(VERSION.length + 1));
|
|
665
|
+
if (joined === null || joined.length <= IV_BYTES) return null;
|
|
666
|
+
try {
|
|
667
|
+
const body = await crypto.subtle.decrypt(
|
|
668
|
+
{ name: "AES-GCM", iv: joined.slice(0, IV_BYTES) },
|
|
669
|
+
key,
|
|
670
|
+
joined.slice(IV_BYTES)
|
|
671
|
+
);
|
|
672
|
+
return new TextDecoder().decode(body);
|
|
673
|
+
} catch {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
613
677
|
async function keyFor(secret) {
|
|
614
678
|
if (secret.length < MIN_SECRET_CHARS) {
|
|
615
679
|
throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
|
|
@@ -634,6 +698,17 @@ function toBase64Url(bytes) {
|
|
|
634
698
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
635
699
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
636
700
|
}
|
|
701
|
+
function fromBase64Url(text2) {
|
|
702
|
+
if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
|
|
703
|
+
try {
|
|
704
|
+
const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
|
|
705
|
+
const bytes = new Uint8Array(binary.length);
|
|
706
|
+
for (let at = 0; at < bytes.length; at++) bytes[at] = binary.charCodeAt(at);
|
|
707
|
+
return bytes;
|
|
708
|
+
} catch {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
637
712
|
|
|
638
713
|
// src/errors.ts
|
|
639
714
|
var ProblemError = class extends Error {
|
|
@@ -767,14 +842,46 @@ function isLnAddress(destination) {
|
|
|
767
842
|
return at > 0 && destination.slice(at + 1).includes(".");
|
|
768
843
|
}
|
|
769
844
|
|
|
845
|
+
// src/relay.ts
|
|
846
|
+
var DEFAULT_POLL_EVERY_SECS = 5;
|
|
847
|
+
var WALLET = "w";
|
|
848
|
+
function lightningVerifyEndpoint(config) {
|
|
849
|
+
const paced = {
|
|
850
|
+
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
851
|
+
};
|
|
852
|
+
return async (request) => {
|
|
853
|
+
const sealed = new URL(request.url).searchParams.get(WALLET);
|
|
854
|
+
if (sealed === null) return Response.json({ settled: false }, { status: 400 });
|
|
855
|
+
const opened = await unseal(config.secret, sealed);
|
|
856
|
+
if (opened === null) return Response.json({ settled: false }, { status: 403 });
|
|
857
|
+
const wallet = JSON.parse(opened);
|
|
858
|
+
const asked = await checkSettled(wallet.url, wallet.hash).catch(() => null);
|
|
859
|
+
if (asked === null) return Response.json({ settled: false }, { status: 502 });
|
|
860
|
+
return Response.json(
|
|
861
|
+
{ settled: asked.preimage !== null, preimage: asked.preimage },
|
|
862
|
+
{ headers: paced }
|
|
863
|
+
);
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
async function relayedVerifyUrl(endpoint, wallet, secret) {
|
|
867
|
+
const relayed = new URL(endpoint);
|
|
868
|
+
relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
|
|
869
|
+
return relayed.toString();
|
|
870
|
+
}
|
|
871
|
+
|
|
770
872
|
// src/rail.ts
|
|
771
873
|
var LIGHTNING = "lightning";
|
|
772
874
|
function blindLightningRail(config) {
|
|
773
875
|
return async (order) => {
|
|
774
876
|
const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
877
|
+
const relay = config.relayVerifyThrough;
|
|
775
878
|
const watched = await config.gateway.watchPayment({
|
|
776
879
|
paymentHash: resolved.paymentHash,
|
|
777
|
-
verifyUrl:
|
|
880
|
+
verifyUrl: relay ? await relayedVerifyUrl(
|
|
881
|
+
relay.endpoint,
|
|
882
|
+
{ url: resolved.verifyUrl, hash: resolved.paymentHash },
|
|
883
|
+
relay.secret
|
|
884
|
+
) : resolved.verifyUrl,
|
|
778
885
|
expiresAt: resolved.expiresAt,
|
|
779
886
|
trigger: config.trigger,
|
|
780
887
|
sealed: await config.sealed?.(order),
|
|
@@ -803,5 +910,7 @@ async function invoiceFrom(lnAddresses, amountMsat) {
|
|
|
803
910
|
// Annotate the CommonJS export names for ESM import in node:
|
|
804
911
|
0 && (module.exports = {
|
|
805
912
|
blindLightningRail,
|
|
806
|
-
|
|
913
|
+
lightningVerifyEndpoint,
|
|
914
|
+
lnurlPayEndpoint,
|
|
915
|
+
relayedVerifyUrl
|
|
807
916
|
});
|
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-J8QoYbSr.cjs';
|
|
2
|
+
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-J8QoYbSr.cjs';
|
|
3
3
|
|
|
4
4
|
interface TriggerConfig {
|
|
5
5
|
/** The gateway that quotes the addresses and mints the invoice */
|
|
@@ -61,4 +61,43 @@ interface Minted {
|
|
|
61
61
|
*/
|
|
62
62
|
declare function lnurlPayEndpoint(config: TriggerConfig): (request: Request) => Promise<Response>;
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/** The wallet's own LUD-21 URL and the hash its preimage has to match */
|
|
65
|
+
interface Relayed {
|
|
66
|
+
url: string;
|
|
67
|
+
hash: string;
|
|
68
|
+
}
|
|
69
|
+
interface LightningVerifyConfig {
|
|
70
|
+
/** The secret the sealed wallet URL was made with, and nothing else uses it */
|
|
71
|
+
secret: string;
|
|
72
|
+
/**
|
|
73
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
74
|
+
* `Cache-Control: max-age`, so the pace is yours rather than the operator's.
|
|
75
|
+
* Five by default, which is what a Lightning checkout wants
|
|
76
|
+
*/
|
|
77
|
+
pollEverySecs?: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A verify endpoint of your own that asks the recipient's wallet for you, so the
|
|
81
|
+
* gateway polls you and never the wallet.
|
|
82
|
+
*
|
|
83
|
+
* `relayedVerifyUrl` seals the wallet's own LUD-21 URL into the query with your
|
|
84
|
+
* secret, so what the gateway stores and replicates is opaque: not the wallet's
|
|
85
|
+
* host, not which provider the recipient uses, nothing but a blob it cannot read.
|
|
86
|
+
* This handler unseals it, asks the wallet, and answers the same LUD-21 shape.
|
|
87
|
+
*
|
|
88
|
+
* It relays rather than decides. The preimage still comes from the recipient's
|
|
89
|
+
* own server and still has to hash to the payment hash, so standing between the
|
|
90
|
+
* two buys privacy and pacing without becoming something anyone has to trust.
|
|
91
|
+
*
|
|
92
|
+
* A wallet it cannot reach answers `502` rather than "not settled", because those
|
|
93
|
+
* are different claims and only one of them is true. The gateway logs a failed
|
|
94
|
+
* poll and asks again, which is what a broken relay should look like.
|
|
95
|
+
*/
|
|
96
|
+
declare function lightningVerifyEndpoint(config: LightningVerifyConfig): (request: Request) => Promise<Response>;
|
|
97
|
+
/**
|
|
98
|
+
* The URL to hand the gateway instead of the wallet's own, with the wallet's
|
|
99
|
+
* sealed inside it. Point it at wherever `lightningVerifyEndpoint` is mounted
|
|
100
|
+
*/
|
|
101
|
+
declare function relayedVerifyUrl(endpoint: string, wallet: Relayed, secret: string): Promise<string>;
|
|
102
|
+
|
|
103
|
+
export { type LightningVerifyConfig, type Minted, type Relayed, type TriggerConfig, lightningVerifyEndpoint, lnurlPayEndpoint, relayedVerifyUrl };
|
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-J8QoYbSr.js';
|
|
2
|
+
export { m as BlindLightningRailConfig, n as blindLightningRail } from './rail-J8QoYbSr.js';
|
|
3
3
|
|
|
4
4
|
interface TriggerConfig {
|
|
5
5
|
/** The gateway that quotes the addresses and mints the invoice */
|
|
@@ -61,4 +61,43 @@ interface Minted {
|
|
|
61
61
|
*/
|
|
62
62
|
declare function lnurlPayEndpoint(config: TriggerConfig): (request: Request) => Promise<Response>;
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/** The wallet's own LUD-21 URL and the hash its preimage has to match */
|
|
65
|
+
interface Relayed {
|
|
66
|
+
url: string;
|
|
67
|
+
hash: string;
|
|
68
|
+
}
|
|
69
|
+
interface LightningVerifyConfig {
|
|
70
|
+
/** The secret the sealed wallet URL was made with, and nothing else uses it */
|
|
71
|
+
secret: string;
|
|
72
|
+
/**
|
|
73
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
74
|
+
* `Cache-Control: max-age`, so the pace is yours rather than the operator's.
|
|
75
|
+
* Five by default, which is what a Lightning checkout wants
|
|
76
|
+
*/
|
|
77
|
+
pollEverySecs?: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* A verify endpoint of your own that asks the recipient's wallet for you, so the
|
|
81
|
+
* gateway polls you and never the wallet.
|
|
82
|
+
*
|
|
83
|
+
* `relayedVerifyUrl` seals the wallet's own LUD-21 URL into the query with your
|
|
84
|
+
* secret, so what the gateway stores and replicates is opaque: not the wallet's
|
|
85
|
+
* host, not which provider the recipient uses, nothing but a blob it cannot read.
|
|
86
|
+
* This handler unseals it, asks the wallet, and answers the same LUD-21 shape.
|
|
87
|
+
*
|
|
88
|
+
* It relays rather than decides. The preimage still comes from the recipient's
|
|
89
|
+
* own server and still has to hash to the payment hash, so standing between the
|
|
90
|
+
* two buys privacy and pacing without becoming something anyone has to trust.
|
|
91
|
+
*
|
|
92
|
+
* A wallet it cannot reach answers `502` rather than "not settled", because those
|
|
93
|
+
* are different claims and only one of them is true. The gateway logs a failed
|
|
94
|
+
* poll and asks again, which is what a broken relay should look like.
|
|
95
|
+
*/
|
|
96
|
+
declare function lightningVerifyEndpoint(config: LightningVerifyConfig): (request: Request) => Promise<Response>;
|
|
97
|
+
/**
|
|
98
|
+
* The URL to hand the gateway instead of the wallet's own, with the wallet's
|
|
99
|
+
* sealed inside it. Point it at wherever `lightningVerifyEndpoint` is mounted
|
|
100
|
+
*/
|
|
101
|
+
declare function relayedVerifyUrl(endpoint: string, wallet: Relayed, secret: string): Promise<string>;
|
|
102
|
+
|
|
103
|
+
export { type LightningVerifyConfig, type Minted, type Relayed, type TriggerConfig, lightningVerifyEndpoint, lnurlPayEndpoint, relayedVerifyUrl };
|
package/dist/server.js
CHANGED
|
@@ -4,6 +4,16 @@ function bytesToHex(bytes) {
|
|
|
4
4
|
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
5
5
|
return hex;
|
|
6
6
|
}
|
|
7
|
+
function hexToBytes(hex) {
|
|
8
|
+
const bytes = new Uint8Array(hex.length >> 1);
|
|
9
|
+
for (let at = 0; at < bytes.length; at++) {
|
|
10
|
+
bytes[at] = parseInt(hex.slice(at * 2, at * 2 + 2), 16);
|
|
11
|
+
}
|
|
12
|
+
return bytes;
|
|
13
|
+
}
|
|
14
|
+
function isHex(text2) {
|
|
15
|
+
return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
|
|
16
|
+
}
|
|
7
17
|
|
|
8
18
|
// ../core/hmac.ts
|
|
9
19
|
async function hmacHex(secret, payload) {
|
|
@@ -211,6 +221,10 @@ function decodeInvoice(bolt11) {
|
|
|
211
221
|
expiresAt: expiryOf(parts.words, tagged)
|
|
212
222
|
};
|
|
213
223
|
}
|
|
224
|
+
function preimageMatchesHash(preimage, paymentHash) {
|
|
225
|
+
if (!isHex(preimage)) return false;
|
|
226
|
+
return bytesToHex(sha256(hexToBytes(preimage))) === paymentHash.toLowerCase();
|
|
227
|
+
}
|
|
214
228
|
function splitBech32(bolt11) {
|
|
215
229
|
const lower = bolt11.toLowerCase();
|
|
216
230
|
if (isBolt12Encoding(lower)) return null;
|
|
@@ -391,19 +405,19 @@ async function refuseUnlessPublic(url) {
|
|
|
391
405
|
if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
|
|
392
406
|
}
|
|
393
407
|
async function answerOf(response) {
|
|
394
|
-
const { status, ok } = response;
|
|
408
|
+
const { status, ok, headers } = response;
|
|
395
409
|
const reader = response.body?.getReader();
|
|
396
|
-
if (!reader) return { status, ok, body: "", truncated: false };
|
|
410
|
+
if (!reader) return { status, ok, body: "", truncated: false, headers };
|
|
397
411
|
const read = [];
|
|
398
412
|
let bytes = 0;
|
|
399
413
|
while (bytes <= BODY_LIMIT_BYTES) {
|
|
400
414
|
const { done, value } = await reader.read();
|
|
401
|
-
if (done) return { status, ok, body: text(read), truncated: false };
|
|
415
|
+
if (done) return { status, ok, body: text(read), truncated: false, headers };
|
|
402
416
|
read.push(value);
|
|
403
417
|
bytes += value.length;
|
|
404
418
|
}
|
|
405
419
|
await reader.cancel();
|
|
406
|
-
return { status, ok, body: text(read), truncated: true };
|
|
420
|
+
return { status, ok, body: text(read), truncated: true, headers };
|
|
407
421
|
}
|
|
408
422
|
function withoutBody(sent) {
|
|
409
423
|
const headers = { ...sent.headers };
|
|
@@ -441,6 +455,10 @@ var NoWalletAvailable = class extends Error {
|
|
|
441
455
|
// ../core/lnurl.ts
|
|
442
456
|
var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
|
|
443
457
|
var RESOLVE_TIMEOUT_MS = 3e4;
|
|
458
|
+
var MIN_PACE_SECS = 1;
|
|
459
|
+
var MAX_PACE_SECS = 3600;
|
|
460
|
+
var MIN_PER_SECOND = 0.01;
|
|
461
|
+
var MAX_PER_SECOND = 100;
|
|
444
462
|
async function resolve(addresses, amountMsat) {
|
|
445
463
|
const served = await firstThatServes(
|
|
446
464
|
addresses,
|
|
@@ -528,6 +546,31 @@ function decodeIssued(address, bolt11) {
|
|
|
528
546
|
}
|
|
529
547
|
return { paymentHash, descriptionHash, amountMsat, expiresAt };
|
|
530
548
|
}
|
|
549
|
+
async function checkSettled(verifyUrl, paymentHash) {
|
|
550
|
+
const answer = await answeredJson(verifyUrl);
|
|
551
|
+
const asked = {
|
|
552
|
+
pace: paceAskedFor(answer.headers),
|
|
553
|
+
ceiling: ceilingAskedFor(answer.headers)
|
|
554
|
+
};
|
|
555
|
+
if (!answer.said.settled || !answer.said.preimage) return { preimage: null, ...asked };
|
|
556
|
+
if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
|
|
557
|
+
throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
|
|
558
|
+
}
|
|
559
|
+
return { preimage: answer.said.preimage, ...asked };
|
|
560
|
+
}
|
|
561
|
+
function paceAskedFor(headers) {
|
|
562
|
+
const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
|
|
563
|
+
if (!asked) return null;
|
|
564
|
+
return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
|
|
565
|
+
}
|
|
566
|
+
function ceilingAskedFor(headers) {
|
|
567
|
+
const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
|
|
568
|
+
if (!asked) return null;
|
|
569
|
+
const perWindow = Number(asked[1]);
|
|
570
|
+
const window = asked[2] === void 0 ? 1 : Number(asked[2]);
|
|
571
|
+
if (perWindow === 0 || window === 0) return null;
|
|
572
|
+
return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
|
|
573
|
+
}
|
|
531
574
|
function cannotReleaseAPreimage(host) {
|
|
532
575
|
const lowered = host.toLowerCase();
|
|
533
576
|
return VERIFY_WITHOUT_PREIMAGE.some(
|
|
@@ -546,12 +589,15 @@ function splitAddress(address) {
|
|
|
546
589
|
return [user, domain];
|
|
547
590
|
}
|
|
548
591
|
async function fetchJson(url, deadline) {
|
|
592
|
+
return (await answeredJson(url, deadline)).said;
|
|
593
|
+
}
|
|
594
|
+
async function answeredJson(url, deadline) {
|
|
549
595
|
const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
|
|
550
596
|
if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
|
|
551
597
|
if (answer.truncated) {
|
|
552
598
|
throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
|
|
553
599
|
}
|
|
554
|
-
return JSON.parse(answer.body);
|
|
600
|
+
return { said: JSON.parse(answer.body), headers: answer.headers };
|
|
555
601
|
}
|
|
556
602
|
|
|
557
603
|
// ../core/sealed.ts
|
|
@@ -573,6 +619,22 @@ async function seal(secret, plaintext) {
|
|
|
573
619
|
joined.set(cipher, iv.length);
|
|
574
620
|
return `${VERSION}.${toBase64Url(joined)}`;
|
|
575
621
|
}
|
|
622
|
+
async function unseal(secret, sealed) {
|
|
623
|
+
const key = await keyFor(secret);
|
|
624
|
+
if (!sealed.startsWith(`${VERSION}.`)) return null;
|
|
625
|
+
const joined = fromBase64Url(sealed.slice(VERSION.length + 1));
|
|
626
|
+
if (joined === null || joined.length <= IV_BYTES) return null;
|
|
627
|
+
try {
|
|
628
|
+
const body = await crypto.subtle.decrypt(
|
|
629
|
+
{ name: "AES-GCM", iv: joined.slice(0, IV_BYTES) },
|
|
630
|
+
key,
|
|
631
|
+
joined.slice(IV_BYTES)
|
|
632
|
+
);
|
|
633
|
+
return new TextDecoder().decode(body);
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
576
638
|
async function keyFor(secret) {
|
|
577
639
|
if (secret.length < MIN_SECRET_CHARS) {
|
|
578
640
|
throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
|
|
@@ -597,6 +659,17 @@ function toBase64Url(bytes) {
|
|
|
597
659
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
598
660
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
599
661
|
}
|
|
662
|
+
function fromBase64Url(text2) {
|
|
663
|
+
if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
|
|
664
|
+
try {
|
|
665
|
+
const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
|
|
666
|
+
const bytes = new Uint8Array(binary.length);
|
|
667
|
+
for (let at = 0; at < bytes.length; at++) bytes[at] = binary.charCodeAt(at);
|
|
668
|
+
return bytes;
|
|
669
|
+
} catch {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
600
673
|
|
|
601
674
|
// src/errors.ts
|
|
602
675
|
var ProblemError = class extends Error {
|
|
@@ -730,14 +803,46 @@ function isLnAddress(destination) {
|
|
|
730
803
|
return at > 0 && destination.slice(at + 1).includes(".");
|
|
731
804
|
}
|
|
732
805
|
|
|
806
|
+
// src/relay.ts
|
|
807
|
+
var DEFAULT_POLL_EVERY_SECS = 5;
|
|
808
|
+
var WALLET = "w";
|
|
809
|
+
function lightningVerifyEndpoint(config) {
|
|
810
|
+
const paced = {
|
|
811
|
+
"cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
|
|
812
|
+
};
|
|
813
|
+
return async (request) => {
|
|
814
|
+
const sealed = new URL(request.url).searchParams.get(WALLET);
|
|
815
|
+
if (sealed === null) return Response.json({ settled: false }, { status: 400 });
|
|
816
|
+
const opened = await unseal(config.secret, sealed);
|
|
817
|
+
if (opened === null) return Response.json({ settled: false }, { status: 403 });
|
|
818
|
+
const wallet = JSON.parse(opened);
|
|
819
|
+
const asked = await checkSettled(wallet.url, wallet.hash).catch(() => null);
|
|
820
|
+
if (asked === null) return Response.json({ settled: false }, { status: 502 });
|
|
821
|
+
return Response.json(
|
|
822
|
+
{ settled: asked.preimage !== null, preimage: asked.preimage },
|
|
823
|
+
{ headers: paced }
|
|
824
|
+
);
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
async function relayedVerifyUrl(endpoint, wallet, secret) {
|
|
828
|
+
const relayed = new URL(endpoint);
|
|
829
|
+
relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
|
|
830
|
+
return relayed.toString();
|
|
831
|
+
}
|
|
832
|
+
|
|
733
833
|
// src/rail.ts
|
|
734
834
|
var LIGHTNING = "lightning";
|
|
735
835
|
function blindLightningRail(config) {
|
|
736
836
|
return async (order) => {
|
|
737
837
|
const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
|
|
838
|
+
const relay = config.relayVerifyThrough;
|
|
738
839
|
const watched = await config.gateway.watchPayment({
|
|
739
840
|
paymentHash: resolved.paymentHash,
|
|
740
|
-
verifyUrl:
|
|
841
|
+
verifyUrl: relay ? await relayedVerifyUrl(
|
|
842
|
+
relay.endpoint,
|
|
843
|
+
{ url: resolved.verifyUrl, hash: resolved.paymentHash },
|
|
844
|
+
relay.secret
|
|
845
|
+
) : resolved.verifyUrl,
|
|
741
846
|
expiresAt: resolved.expiresAt,
|
|
742
847
|
trigger: config.trigger,
|
|
743
848
|
sealed: await config.sealed?.(order),
|
|
@@ -765,5 +870,7 @@ async function invoiceFrom(lnAddresses, amountMsat) {
|
|
|
765
870
|
}
|
|
766
871
|
export {
|
|
767
872
|
blindLightningRail,
|
|
768
|
-
|
|
873
|
+
lightningVerifyEndpoint,
|
|
874
|
+
lnurlPayEndpoint,
|
|
875
|
+
relayedVerifyUrl
|
|
769
876
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thunder-bridge",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
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",
|