thunder-bridge 1.1.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
@@ -293,6 +294,40 @@ is only ever talking to servers that asked to be talked to. It costs you a servi
293
294
  that has to stay up: a browser-only integration cannot do this, and should keep
294
295
  letting the gateway poll the wallet.
295
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
+
296
331
  ### How often the gateway asks
297
332
 
298
333
  Your endpoint decides, not the gateway. `bankVerifyEndpoint` answers with
@@ -301,10 +336,59 @@ payment on your host. Set `pollEverySecs` to whatever your bank's own refresh ma
301
336
  sensible: reading a statement that moves once an hour every five seconds only burns
302
337
  your rate limit.
303
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
+
304
344
  The gateway also asks the URL once, before it accepts the watch, and refuses with
305
345
  `424` if it does not answer this shape. So deploy the endpoint first and register
306
346
  second. That is what stops anyone pointing a gateway at a server that never asked to
307
- 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.
308
392
 
309
393
  ## What is still trusted
310
394
 
@@ -501,11 +585,13 @@ on that route and not a JSON one.
501
585
  import express from "express";
502
586
  import { parseWebhook } from "thunder-bridge";
503
587
 
588
+ const signs = { publicKey: await gateway.webhookKey() };
589
+
504
590
  app.post("/hooks/paid", express.raw({ type: "application/json" }), async (request, response) => {
505
591
  const payment = await parseWebhook(
506
592
  request.body,
507
593
  request.get("x-signature") ?? "",
508
- secret,
594
+ signs,
509
595
  request.get("x-timestamp") ?? "",
510
596
  );
511
597
  response.sendStatus(payment === null ? 401 : 200);