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 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 three days.
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
- secret,
594
+ signs,
503
595
  request.get("x-timestamp") ?? "",
504
596
  );
505
597
  response.sendStatus(payment === null ? 401 : 200);