thunder-bridge 1.0.0 → 1.3.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 +96 -4
- package/dist/index.cjs +1650 -1302
- package/dist/index.d.cts +214 -170
- package/dist/index.d.ts +214 -170
- package/dist/index.js +1646 -1301
- package/dist/{rail-CL9QkiHo.d.cts → rail-DZzlN-bi.d.cts} +166 -1
- package/dist/{rail-CL9QkiHo.d.ts → rail-DZzlN-bi.d.ts} +166 -1
- package/dist/server.cjs +976 -382
- package/dist/server.d.cts +47 -41
- package/dist/server.d.ts +47 -41
- package/dist/server.js +966 -382
- package/openapi.yaml +1 -1
- package/package.json +33 -13
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ them and this table does not repeat them.
|
|
|
91
91
|
| `gateway.waitForWatched(id, options?)` | the same for a watched one, answering the shape both rails share |
|
|
92
92
|
| `gateway.firstToSettle(ids, options?)` | wait on several legs, keep the first really paid, drop the losers |
|
|
93
93
|
| `gateway.watchPayment(params)` | hand over an invoice you obtained yourself, without the address or the amount |
|
|
94
|
-
| `gateway.followTrigger(secret, options)` | stream every payment carrying one trigger, reconnecting on its own |
|
|
94
|
+
| `gateway.followTrigger(secret, options)` | stream every payment carrying one trigger, reconnecting on its own. `replay` asks for how many past settlements on connect |
|
|
95
95
|
| `gateway.isPrivate` | whether a token was given |
|
|
96
96
|
|
|
97
97
|
**Proving it** - [`src/verify.ts`](src/verify.ts)
|
|
@@ -108,7 +108,7 @@ them and this table does not repeat them.
|
|
|
108
108
|
|
|
109
109
|
| Export | What it does |
|
|
110
110
|
|---|---|
|
|
111
|
-
| `lnurlPayEndpoint(config)` | a whole LNURL-pay endpoint as one Fetch handler, so a static QR points at your domain. From `thunder-bridge/server` |
|
|
111
|
+
| `lnurlPayEndpoint(config)` | a whole LNURL-pay endpoint as one Fetch handler, so a static QR points at your domain. `replay` keeps its last settlements on the gateway for a page that opens later. From `thunder-bridge/server` |
|
|
112
112
|
| `seal(secret, plaintext)`, `unseal` | the blob the gateway stores and cannot read |
|
|
113
113
|
| `toLnurl(url)` | bech32-encode an endpoint url |
|
|
114
114
|
| `bankTransfer(params)` | register a Czech QR platba as a watched payment. Refuses a gateway that serves strangers |
|
|
@@ -132,6 +132,7 @@ whatever runtime you keep state in.
|
|
|
132
132
|
| `bankRail(config)` | a `Rail` selling for a bank transfer, reading back through any `Statement` |
|
|
133
133
|
| `lightningRail(config)` | a `Rail` where the gateway mints the invoice, so it learns the address and the amount |
|
|
134
134
|
| `blindLightningRail(config)` | a `Rail` that resolves the address itself and tells the gateway only a hash. From `thunder-bridge/server` |
|
|
135
|
+
| `nwcRail(config)` | a `Rail` that mints on a wallet of your own over NIP-47, for a wallet with no LUD-21 address to be watched at. From `thunder-bridge/server` |
|
|
135
136
|
|
|
136
137
|
`Rail` is `(order: Order) => Promise<Leg>`. Everything that differs between rails
|
|
137
138
|
is bound once when the rail is built, so the only thing passed per sale is which
|
|
@@ -169,6 +170,12 @@ const toPay = invoiceToSvg(payment.bolt11, { size: 320, color: "#1a1a2e" });
|
|
|
169
170
|
const tipJar = lnurlToSvg("https://agora.gripe/tip");
|
|
170
171
|
```
|
|
171
172
|
|
|
173
|
+
**Minting your own invoice** - [`src/rail.ts`](src/rail.ts): `invoiceFrom`, from
|
|
174
|
+
`thunder-bridge/server`. A gateway that does not mint is one that never sees an
|
|
175
|
+
address or an amount, so this is how a client gets a provable invoice itself and hands
|
|
176
|
+
the gateway only a hash, a url and an expiry. Server side, because it resolves
|
|
177
|
+
hostnames and refuses a private one.
|
|
178
|
+
|
|
172
179
|
**Webhooks** - [`src/webhook.ts`](src/webhook.ts): `parseSettlementRequest`,
|
|
173
180
|
`parseSettlement`, `isProvablySettled`, `parseWebhookRequest`, `parseWebhook`,
|
|
174
181
|
`parseWatchedWebhookRequest`, `parseWatchedWebhook`, `verifyWebhookSignature`,
|
|
@@ -287,6 +294,40 @@ is only ever talking to servers that asked to be talked to. It costs you a servi
|
|
|
287
294
|
that has to stay up: a browser-only integration cannot do this, and should keep
|
|
288
295
|
letting the gateway poll the wallet.
|
|
289
296
|
|
|
297
|
+
### A wallet with no LUD-21 address at all
|
|
298
|
+
|
|
299
|
+
`nwcRail` is the same arrangement with the far side swapped. Your wallet answers
|
|
300
|
+
over [NIP-47](https://github.com/nostr-protocol/nips/blob/master/47.md) instead of
|
|
301
|
+
over an address, so a recipient whose provider publishes no `verify` - or publishes
|
|
302
|
+
one with no preimage, which is worse - is watchable anyway.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import { nwcConnection, nwcRail, nwcVerifyEndpoint } from "thunder-bridge/server";
|
|
306
|
+
|
|
307
|
+
const connection = nwcConnection(process.env.NWC_URI);
|
|
308
|
+
|
|
309
|
+
app.get("/verify/nwc", (context) =>
|
|
310
|
+
nwcVerifyEndpoint({ connection, secret: NWC_SECRET })(context.req.raw),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const rail = nwcRail({
|
|
314
|
+
gateway,
|
|
315
|
+
connection,
|
|
316
|
+
amountMsat: (order) => order.amountMinor * 40,
|
|
317
|
+
verifyThrough: { endpoint: "https://shop.example/verify/nwc", secret: NWC_SECRET },
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`make_invoice` mints it, `lookup_invoice` reads the preimage back, and the payment
|
|
322
|
+
hash is sealed into the query for the reason the wallet's URL is sealed above: it
|
|
323
|
+
is what stops a stranger driving your wallet through your own handler. Only the
|
|
324
|
+
hash travels, so the gateway never holds the connection, the relay or the wallet
|
|
325
|
+
key, and every answer is refused unless the wallet's own key signed it.
|
|
326
|
+
|
|
327
|
+
Take the connection string scoped. ZEUS, Alby Hub and Blink all issue one per app
|
|
328
|
+
with its own permissions and budget, and this needs `make_invoice` and
|
|
329
|
+
`lookup_invoice` and nothing else - never `pay_invoice`.
|
|
330
|
+
|
|
290
331
|
### How often the gateway asks
|
|
291
332
|
|
|
292
333
|
Your endpoint decides, not the gateway. `bankVerifyEndpoint` answers with
|
|
@@ -295,10 +336,59 @@ payment on your host. Set `pollEverySecs` to whatever your bank's own refresh ma
|
|
|
295
336
|
sensible: reading a statement that moves once an hour every five seconds only burns
|
|
296
337
|
your rate limit.
|
|
297
338
|
|
|
339
|
+
How long one answer may take is the other half of the pacing. The gateway abandons a
|
|
340
|
+
poll after 15 seconds, and `askTimeoutMs` is one deadline over the whole connection
|
|
341
|
+
rather than one per relay, so a connection listing four relays still answers inside
|
|
342
|
+
that window.
|
|
343
|
+
|
|
298
344
|
The gateway also asks the URL once, before it accepts the watch, and refuses with
|
|
299
345
|
`424` if it does not answer this shape. So deploy the endpoint first and register
|
|
300
346
|
second. That is what stops anyone pointing a gateway at a server that never asked to
|
|
301
|
-
be polled for
|
|
347
|
+
be polled for thirty days.
|
|
348
|
+
|
|
349
|
+
## Paying through an operator who fronts the liquidity
|
|
350
|
+
|
|
351
|
+
A recipient with no inbound liquidity cannot be paid at all. An operator with a node
|
|
352
|
+
can stand in the middle without holding anything: it takes the recipient's own
|
|
353
|
+
invoice, mints a **hold invoice on the same payment hash** for the amount plus a fee,
|
|
354
|
+
and can settle its own only by revealing the preimage it learned from paying the
|
|
355
|
+
recipient. Claiming and delivering are one act, so there is no moment where it keeps
|
|
356
|
+
the money and walks away.
|
|
357
|
+
|
|
358
|
+
This SDK does not wrap. `proveWrapped` checks a wrap somebody else offers, and the
|
|
359
|
+
NIP-47 primitives an operator would build one from, `nwcHoldInvoice` and `nwcPay`,
|
|
360
|
+
are on `thunder-bridge/server` with nothing here driving them.
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
import { invoiceFrom } from "thunder-bridge/server";
|
|
364
|
+
import { proveWrapped, wrapFeeCeiling } from "thunder-bridge";
|
|
365
|
+
|
|
366
|
+
const real = await invoiceFrom(["you@blink.sv"], 21_000_000);
|
|
367
|
+
const wrapped = await myOperator.wrap(real.bolt11);
|
|
368
|
+
|
|
369
|
+
proveWrapped(wrapped, real.bolt11);
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`proveWrapped` compares two invoices and asks nobody anything, so it runs in a
|
|
373
|
+
browser. It refuses a wrap on another hash, one that cannot cover what the recipient
|
|
374
|
+
asked, one charging over the allowance, and one that outlives the invoice it has to
|
|
375
|
+
forward to.
|
|
376
|
+
|
|
377
|
+
The allowance is the **client's ceiling**, not a fee the operator names per payment,
|
|
378
|
+
so it sits deliberately above any list price: `1%` by default with a floor of one
|
|
379
|
+
satoshi. An operator running this charges `0.75%`, which leaves room for a wrap a
|
|
380
|
+
shade over list to still go through. Set `proportion` under an operator's price and
|
|
381
|
+
you refuse that operator, which is the point of it being yours.
|
|
382
|
+
`wrapFeeCeiling(amountMsat, allowance)` is the same number if you want to show it.
|
|
383
|
+
|
|
384
|
+
There is no settlement check to add. Both invoices carry one payment hash, so the
|
|
385
|
+
preimage that settles the wrap is the one the recipient released, and
|
|
386
|
+
`proveSettlement` already reads it from the recipient's own server. The gateway needs
|
|
387
|
+
no change either: it watches that hash and polls the recipient's verify URL, and a
|
|
388
|
+
wrap is invisible to it.
|
|
389
|
+
|
|
390
|
+
What this does not cover: an operator that accepts the payment and stalls until the
|
|
391
|
+
HTLC times out. Your money comes back, and it was locked meanwhile.
|
|
302
392
|
|
|
303
393
|
## What is still trusted
|
|
304
394
|
|
|
@@ -495,11 +585,13 @@ on that route and not a JSON one.
|
|
|
495
585
|
import express from "express";
|
|
496
586
|
import { parseWebhook } from "thunder-bridge";
|
|
497
587
|
|
|
588
|
+
const signs = { publicKey: await gateway.webhookKey() };
|
|
589
|
+
|
|
498
590
|
app.post("/hooks/paid", express.raw({ type: "application/json" }), async (request, response) => {
|
|
499
591
|
const payment = await parseWebhook(
|
|
500
592
|
request.body,
|
|
501
593
|
request.get("x-signature") ?? "",
|
|
502
|
-
|
|
594
|
+
signs,
|
|
503
595
|
request.get("x-timestamp") ?? "",
|
|
504
596
|
);
|
|
505
597
|
response.sendStatus(payment === null ? 401 : 200);
|