thunder-bridge 1.4.2 → 1.5.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 +135 -125
- package/dist/bank-B3mHISn1.d.cts +92 -0
- package/dist/bank-B3mHISn1.d.ts +92 -0
- package/dist/bank.cjs +141 -0
- package/dist/bank.d.cts +44 -0
- package/dist/bank.d.ts +44 -0
- package/dist/bank.js +114 -0
- package/dist/client-Cc5gtjGV.d.ts +733 -0
- package/dist/client-CzGZcByI.d.cts +733 -0
- package/dist/errors-0vbVoISA.d.ts +126 -0
- package/dist/errors-Dmh-Uoh8.d.cts +126 -0
- package/dist/index.cjs +1576 -1094
- package/dist/index.d.cts +11 -503
- package/dist/index.d.ts +11 -503
- package/dist/index.js +1556 -1052
- package/dist/{server.cjs → nwc.cjs} +261 -689
- package/dist/nwc.d.cts +120 -0
- package/dist/nwc.d.ts +120 -0
- package/dist/{server.js → nwc.js} +256 -667
- package/dist/price.cjs +240 -0
- package/dist/price.d.cts +17 -0
- package/dist/price.d.ts +17 -0
- package/dist/price.js +205 -0
- package/dist/qr-CF-YeXU1.d.cts +55 -0
- package/dist/qr-CF-YeXU1.d.ts +55 -0
- package/dist/qr.cjs +262 -0
- package/dist/qr.d.cts +1 -0
- package/dist/qr.d.ts +1 -0
- package/dist/qr.js +227 -0
- package/dist/types-BNPmVnA7.d.cts +252 -0
- package/dist/types-BNPmVnA7.d.ts +252 -0
- package/openapi.yaml +1 -1
- package/package.json +48 -9
- package/dist/rail-CqUfuYXJ.d.cts +0 -615
- package/dist/rail-CqUfuYXJ.d.ts +0 -615
- package/dist/server.d.cts +0 -145
- package/dist/server.d.ts +0 -145
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ so the same wallet on another domain can answer differently.
|
|
|
34
34
|
A refusal happens at creation rather than leaving a payment pending until a
|
|
35
35
|
watcher gives up, so a recipient finds out before a payer sees a QR code.
|
|
36
36
|
|
|
37
|
-
If your recipient is on a refused name, `nwcRail` is the way round it: your own
|
|
37
|
+
If your recipient is on a refused name, `nwcRail` from `thunder-bridge/nwc` is the way round it: your own
|
|
38
38
|
wallet answers over NIP-47 instead of over an address, and the gateway watches the
|
|
39
39
|
hash exactly the same. [docs/lud21-coverage.md](../docs/lud21-coverage.md) is the
|
|
40
40
|
measured list rather than a reading of changelogs, last surveyed 2026-08-12. Read
|
|
@@ -47,23 +47,27 @@ as refused here until the next survey says otherwise.
|
|
|
47
47
|
npm install thunder-bridge
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Node 22 or newer, for anything that opens a socket: `
|
|
51
|
-
`
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`
|
|
50
|
+
Node 22 or newer, for anything that opens a socket: `requestPayment`, `settled`,
|
|
51
|
+
`firstSettled` and `follow` on the gateway side, and every NWC call on the wallet
|
|
52
|
+
side, since a nostr relay is a socket too. Node only exposes a global `WebSocket`
|
|
53
|
+
from 22 onwards and there is no fallback to install. The rest, which is every call
|
|
54
|
+
that is one or more `fetch` requests, runs on any runtime with `fetch` and
|
|
55
|
+
`crypto.subtle`.
|
|
56
56
|
|
|
57
|
-
The
|
|
57
|
+
One import is the whole thing. The other four are for what a checkout page has no
|
|
58
|
+
reason to download.
|
|
58
59
|
|
|
59
|
-
| Import |
|
|
60
|
-
|
|
61
|
-
| `thunder-bridge` |
|
|
62
|
-
| `thunder-bridge/
|
|
60
|
+
| Import | What it is for |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `thunder-bridge` | the gateway, the proofs, the errors and the amounts. Everything is reached through one instance |
|
|
63
|
+
| `thunder-bridge/qr` | a payload as an SVG or a data URL, for a page that draws a QR of its own |
|
|
64
|
+
| `thunder-bridge/price` | the exchange venues behind `fiat`, for pricing off your own book instead |
|
|
65
|
+
| `thunder-bridge/bank` | a bank statement reader, currently Fio |
|
|
66
|
+
| `thunder-bridge/nwc` | your own wallet over NIP-47, which carries the nostr crypto no browser wants |
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
`invoiceFrom` resolves lightning addresses and refuses a private host, so it needs
|
|
69
|
+
`node:dns` and will not run on Cloudflare Workers. Nothing else on the main import
|
|
70
|
+
does.
|
|
67
71
|
|
|
68
72
|
## Quick start
|
|
69
73
|
|
|
@@ -74,49 +78,52 @@ gives a `404` and [`/health`](https://public.thunder-bridge.agora.gripe/health)
|
|
|
74
78
|
what tells you it is up.
|
|
75
79
|
|
|
76
80
|
```ts
|
|
77
|
-
import {
|
|
78
|
-
ThunderBridge,
|
|
79
|
-
invoiceToSvg,
|
|
80
|
-
proveSettlement,
|
|
81
|
-
type CreatePaymentParams,
|
|
82
|
-
} from "thunder-bridge";
|
|
81
|
+
import { sats, ThunderBridge } from "thunder-bridge";
|
|
83
82
|
|
|
84
83
|
const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe");
|
|
85
84
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const payment = await gateway.createPayment(request);
|
|
85
|
+
const asked = await gateway.requestPayment({
|
|
86
|
+
paidTo: ["iamfatik@blink.sv", "iamfatik@coinos.io"],
|
|
87
|
+
amount: sats(21),
|
|
88
|
+
signal: AbortSignal.timeout(600_000),
|
|
89
|
+
});
|
|
92
90
|
|
|
93
91
|
const target = document.querySelector("#qr");
|
|
94
92
|
if (target !== null) {
|
|
95
|
-
target.innerHTML =
|
|
93
|
+
target.innerHTML = asked.qr;
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
signal: AbortSignal.timeout(600_000),
|
|
100
|
-
});
|
|
96
|
+
await asked.paid();
|
|
101
97
|
|
|
102
|
-
const preimage =
|
|
98
|
+
const preimage = await asked.prove();
|
|
103
99
|
```
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
101
|
+
Two names and one call. `requestPayment` mints the invoice on the first address that can
|
|
102
|
+
prove one, checks that invoice against the recipient's own domain before returning,
|
|
103
|
+
and draws the QR.
|
|
104
|
+
|
|
105
|
+
Then there are two different questions and both are worth asking. `paid` tells you
|
|
106
|
+
what the gateway says, and it is the fast one. `prove` asks the recipient's own
|
|
107
|
+
server, and only that is evidence the money arrived.
|
|
110
108
|
[docs/proving-a-payment.md](../docs/proving-a-payment.md) is the whole argument for
|
|
111
109
|
why.
|
|
112
110
|
|
|
111
|
+
An amount is `sats(21)`, `msat(21_000)` or `fiat("4.99", "USD")`, and a bare number
|
|
112
|
+
does not compile: the type is branded, so `21` cannot pass for a price and quietly
|
|
113
|
+
mean twenty-one thousandths of a satoshi. A fiat price is converted when the invoice
|
|
114
|
+
is minted, off the median of four MiCA authorised venues unless you pass your own,
|
|
115
|
+
and a decimal string is read digit by digit rather than through a float. Every
|
|
116
|
+
refusal is an `AmountError` carrying a `code`, and `AmountError.is(error)` is how
|
|
117
|
+
you recognise one: each entry point bundles its own copy of the class, so
|
|
118
|
+
`instanceof` holds within one import and that static holds across all of them.
|
|
119
|
+
|
|
113
120
|
## How each payment method gets verified
|
|
114
121
|
|
|
115
122
|
Every rail ends the same way, with a preimage that has to hash to the payment hash
|
|
116
123
|
the gateway was given. What differs is who obtains the invoice, who is asked for the
|
|
117
124
|
preimage, and which side does the checking.
|
|
118
125
|
|
|
119
|
-
| `
|
|
126
|
+
| `rails.lightning` | the gateway asks, at the recipient's LNURL callback |
|
|
120
127
|
|---|---|
|
|
121
128
|
| the gateway is told | the address list and the amount, and nothing else. It derives the hash and the `verify` URL by resolving the address, and hands both back |
|
|
122
129
|
| the invoice is checked by | you, `proveOrigin` runs five checks against the recipient's own domain |
|
|
@@ -125,12 +132,12 @@ preimage, and which side does the checking.
|
|
|
125
132
|
| `settled` comes from | the wallet releasing its preimage |
|
|
126
133
|
| the pace is set by | the wallet, when it sends `Cache-Control: max-age`. When it sends none the gateway's own schedule decides |
|
|
127
134
|
|
|
128
|
-
| `
|
|
135
|
+
| `rails.blindLightning` | you ask, with `invoiceFrom` on your server |
|
|
129
136
|
|---|---|
|
|
130
137
|
| the gateway is told | a hash, an expiry and your URL, with the wallet's sealed inside |
|
|
131
138
|
| the invoice is checked by | nobody needs to, you resolved the address yourself |
|
|
132
139
|
| the gateway probes first | `speaksVerify`: a GET on the URL, then a signed POST nonce it must echo |
|
|
133
|
-
| the gateway polls | your `
|
|
140
|
+
| the gateway polls | your `serve.verify` endpoint, once `relayThrough` is set. Leave it off and the gateway polls the wallet directly, as on the minted rail |
|
|
134
141
|
| `settled` comes from | your endpoint, which unseals, asks the wallet and relays the answer |
|
|
135
142
|
| the pace is set by | you, `pollEverySecs` |
|
|
136
143
|
|
|
@@ -143,12 +150,12 @@ preimage, and which side does the checking.
|
|
|
143
150
|
| `settled` comes from | `lookup_invoice`, refused unless the wallet's own key signed it |
|
|
144
151
|
| the pace is set by | you, `pollEverySecs` |
|
|
145
152
|
|
|
146
|
-
| `
|
|
153
|
+
| `rails.bank` | nobody, there is no invoice |
|
|
147
154
|
|---|---|
|
|
148
155
|
| the gateway is told | a hash and your URL, which names the amount and the reference |
|
|
149
156
|
| the invoice is checked by | nobody, there is no invoice to check |
|
|
150
157
|
| the gateway probes first | the same GET and signed nonce |
|
|
151
|
-
| the gateway polls | your `
|
|
158
|
+
| the gateway polls | your `serve.bankVerify` endpoint |
|
|
152
159
|
| `settled` comes from | a `Statement` credit matching amount and currency exactly, with the reference anywhere in the payer's text |
|
|
153
160
|
| the pace is set by | you, `pollEverySecs` |
|
|
154
161
|
|
|
@@ -160,7 +167,7 @@ the whole defence. A `webhookUrl` is challenged on both paths. On every watched
|
|
|
160
167
|
and challenges it with a nonce before accepting the watch, refusing with `424` if
|
|
161
168
|
nothing answers. Deploy the endpoint before you register it. The challenge is on
|
|
162
169
|
unless the operator set `VERIFY_CHALLENGE=0`, which is also why a bare wallet
|
|
163
|
-
`verify` URL cannot be handed to `
|
|
170
|
+
`verify` URL cannot be handed to `watch`: a wallet will not echo a nonce.
|
|
164
171
|
|
|
165
172
|
**What a preimage proves is the same on all four, and narrower than it looks:** that
|
|
166
173
|
the server holding the secret says the money arrived, made unforgeable by anyone
|
|
@@ -175,7 +182,7 @@ without saying whether either invoice was paid.
|
|
|
175
182
|
|
|
176
183
|
### What each one costs you
|
|
177
184
|
|
|
178
|
-
**`
|
|
185
|
+
**`rails.lightning`**
|
|
179
186
|
|
|
180
187
|
- the gateway holds the address and the amount, so your order book is readable
|
|
181
188
|
from its own logs
|
|
@@ -184,7 +191,7 @@ without saying whether either invoice was paid.
|
|
|
184
191
|
- the wallet's `Cache-Control` sets the poll pace, so how fast a settlement is
|
|
185
192
|
noticed is not yours to decide
|
|
186
193
|
|
|
187
|
-
**`
|
|
194
|
+
**`rails.blindLightning`**
|
|
188
195
|
|
|
189
196
|
- a service of your own that has to stay up, so a browser-only integration cannot
|
|
190
197
|
use this rail at all
|
|
@@ -204,7 +211,7 @@ without saying whether either invoice was paid.
|
|
|
204
211
|
the wallet
|
|
205
212
|
- relays unreachable means no verification
|
|
206
213
|
|
|
207
|
-
**`
|
|
214
|
+
**`rails.bank`**
|
|
208
215
|
|
|
209
216
|
- the gateway has to be one of your own: the verify URL names the amount and the
|
|
210
217
|
reference, so whoever runs the gateway reads your order book from the watches
|
|
@@ -249,7 +256,7 @@ Four sharp edges, worth reading before you build:
|
|
|
249
256
|
host answering `302` to a private address is followed there. `invoiceFrom` is not
|
|
250
257
|
like this: it resolves through the outbound guard, which sets `redirect: "manual"`
|
|
251
258
|
and re-vets every hop. Keep egress control outside this package if that matters.
|
|
252
|
-
- **A payment read cold is only as pinned as its creation.** `
|
|
259
|
+
- **A payment read cold is only as pinned as its creation.** `payment` checks
|
|
253
260
|
the preimage against the `paymentHash` in the same record, and it was
|
|
254
261
|
`proveOrigin` at creation, against the request you wrote, that tied that hash to
|
|
255
262
|
an invoice the recipient issued. Store the request alongside the payment id, or a
|
|
@@ -259,49 +266,58 @@ Four sharp edges, worth reading before you build:
|
|
|
259
266
|
proving an invoice belongs to an address never proves the address belongs to
|
|
260
267
|
whoever you think it does.
|
|
261
268
|
|
|
262
|
-
`
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
checks and their failure codes, is in
|
|
269
|
+
`carriesProof` is the one to be careful with: it asks only whether a report holds
|
|
270
|
+
together, so a gateway that generates a preimage, hashes it and builds an invoice
|
|
271
|
+
around that hash passes it. If a payment matters, ask the recipient with
|
|
272
|
+
`prove` on the payment request or with `proveSettlement`. The full argument, including the five
|
|
273
|
+
origin checks and their failure codes, is in
|
|
267
274
|
[docs/proving-a-payment.md](../docs/proving-a-payment.md).
|
|
268
275
|
|
|
269
276
|
## What you call
|
|
270
277
|
|
|
271
|
-
|
|
272
|
-
|
|
278
|
+
[docs/api.md](../docs/api.md) is the whole surface, generated from the TSDoc on
|
|
279
|
+
every export by [`tools/api-reference.ts`](../tools/api-reference.ts) and checked in
|
|
280
|
+
CI, so nothing there can be out of date and nothing here repeats it. Your editor
|
|
281
|
+
has the same text on the export itself.
|
|
273
282
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
- **
|
|
297
|
-
`
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
283
|
+
The shape of it is worth stating once, because it is the thing that makes the rest
|
|
284
|
+
findable. One instance is the entry to everything:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { sats, ThunderBridge } from "thunder-bridge";
|
|
288
|
+
|
|
289
|
+
const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe", {
|
|
290
|
+
secret: "a-long-lived-server-side-secret",
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const asked = await gateway.requestPayment({ paidTo: "iamfatik@blink.sv", amount: sats(21) });
|
|
294
|
+
const read = await gateway.payment(asked.id);
|
|
295
|
+
const quoted = await gateway.quote({ paidTo: "iamfatik@blink.sv", amount: sats(21) });
|
|
296
|
+
|
|
297
|
+
const lnurl = gateway.serve.lnurlPay({
|
|
298
|
+
paidTo: "iamfatik@blink.sv",
|
|
299
|
+
amount: sats(21),
|
|
300
|
+
secret: "a-long-lived-server-side-secret",
|
|
301
|
+
});
|
|
302
|
+
const rail = gateway.rails.lightning({ paidTo: "iamfatik@blink.sv", amount: () => sats(21) });
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
- **the payments themselves** are `requestPayment`, `mint`, `quote`, `watch`, `payment`,
|
|
306
|
+
`payments`, `settled`, `firstSettled`, `follow`, `ticket`, `nameFor` and
|
|
307
|
+
`webhookKey`, all on the instance
|
|
308
|
+
- **what you mount** is on `gateway.serve`: an LNURL-pay endpoint, the two ticket
|
|
309
|
+
endpoints, the verify endpoints for Lightning and for a bank, the webhook route,
|
|
310
|
+
and the readers under it
|
|
311
|
+
- **one call per sale** is on `gateway.rails`: `lightning`, `blindLightning`,
|
|
312
|
+
`bank`, and `transfer` for a bank transfer on its own
|
|
313
|
+
- **the proofs** are free functions, deliberately, because a proof you cannot run
|
|
314
|
+
without the thing being audited is not a proof: `proveOrigin`, `proveSettlement`,
|
|
315
|
+
`proveWrapped`, `carriesProof`, `decodeInvoice`, `preimageMatchesHash`
|
|
316
|
+
- **a payment reads without an assertion.** `Payment` is `MintedPayment |
|
|
317
|
+
WatchedPayment`, so checking `kind` is what makes the address, the amount and the
|
|
318
|
+
invoice non-null. The gateway writes those three together or writes none of them,
|
|
319
|
+
and a record carrying some of the three is refused rather than read
|
|
320
|
+
- **the amounts** are `sats`, `msat` and `fiat`
|
|
305
321
|
|
|
306
322
|
What your service answers once those handlers are mounted is written out in
|
|
307
323
|
[`openapi.yaml`](openapi.yaml), shipped with this package.
|
|
@@ -312,18 +328,20 @@ Every failure from the gateway is an RFC 9457 problem document. Branch on `type`
|
|
|
312
328
|
never on prose. `error.status` is what the transport carried, and a document naming
|
|
313
329
|
a different status in its own body does not override it.
|
|
314
330
|
|
|
315
|
-
Every `type` below is prefixed `urn:problem-type:thunder-bridge:`, and
|
|
316
|
-
|
|
331
|
+
Every `type` below is prefixed `urn:problem-type:thunder-bridge:`, and every one of
|
|
332
|
+
them is a static string on `ProblemError`, so nothing has to be copied out of this
|
|
333
|
+
table by hand. `ProblemError.is(error, ProblemError.PAYMENT_ALREADY_WATCHED)` is how
|
|
334
|
+
you branch on a type that has no error class of its own.
|
|
317
335
|
|
|
318
336
|
| `type` | Status | What it is |
|
|
319
337
|
|---|---|---|
|
|
320
338
|
| `invalid-request` | 400, or 413 for a body over the size ceiling | `detail` names the field |
|
|
321
|
-
| `no-wallet-available` | 502, else 422, else 400, following the worst wallet | `NoWalletAvailableError`, `wallets` says why each failed
|
|
322
|
-
| `request-in-flight` | 409 | a request with this `Idempotency-Key` is still running
|
|
323
|
-
| `idempotency-key-reused` | 409 | that key was used for a different request
|
|
324
|
-
| `payment-already-watched` | 409 | that payment hash is already watched here
|
|
339
|
+
| `no-wallet-available` | 502, else 422, else 400, following the worst wallet | `NoWalletAvailableError`, `wallets` says why each failed |
|
|
340
|
+
| `request-in-flight` | 409 | a request with this `Idempotency-Key` is still running, as `IdempotencyConflictError` |
|
|
341
|
+
| `idempotency-key-reused` | 409 | that key was used for a different request, as `IdempotencyConflictError` |
|
|
342
|
+
| `payment-already-watched` | 409 | that payment hash is already watched here |
|
|
325
343
|
| `caller-unknown` | 403 | the instance keeps a list of callers and your key is not on it |
|
|
326
|
-
| `verify-host-refused` | 403 | this instance will not mint, because minting is off or `VERIFY_HOSTS` pins it to a list. Resolve the address yourself and use `
|
|
344
|
+
| `verify-host-refused` | 403 | this instance will not mint, because minting is off or `VERIFY_HOSTS` pins it to a list. Resolve the address yourself and use `watch`. A verify URL that is not public https is `invalid-request` instead |
|
|
327
345
|
| `verify-unconfirmed` | 424 | the URL did not answer the LUD-21 shape |
|
|
328
346
|
| `verify-unconsented` | 424 | the URL did not echo the challenge nonce |
|
|
329
347
|
| `webhook-unconfirmed` | 424 | the webhook URL did not answer its challenge |
|
|
@@ -334,7 +352,7 @@ problem body: `401` when the bearer token does not match, `404` both for an id t
|
|
|
334
352
|
gateway never heard of and for one it knows that belongs to a different caller key,
|
|
335
353
|
so a `403` can never confirm an id exists, `410` when you replay an
|
|
336
354
|
`Idempotency-Key` whose payment has since been pruned, `500`, and `503` while the
|
|
337
|
-
instance is draining or its own health check reads stalled. On a `404` `
|
|
355
|
+
instance is draining or its own health check reads stalled. On a `404` `payment`
|
|
338
356
|
returns `null` rather than throwing.
|
|
339
357
|
|
|
340
358
|
`GatewayCheatError` is different in kind. It reports a gateway that demonstrably
|
|
@@ -347,6 +365,7 @@ unproven invoice, and decide it explicitly.
|
|
|
347
365
|
```ts
|
|
348
366
|
import {
|
|
349
367
|
GatewayCheatError,
|
|
368
|
+
msat,
|
|
350
369
|
NoWalletAvailableError,
|
|
351
370
|
ProblemError,
|
|
352
371
|
ThunderBridge,
|
|
@@ -357,7 +376,7 @@ declare const gateway: ThunderBridge;
|
|
|
357
376
|
declare function report(line: string): void;
|
|
358
377
|
|
|
359
378
|
try {
|
|
360
|
-
await gateway.
|
|
379
|
+
await gateway.mint({ paidTo: "iamfatik@blink.sv", amount: msat(21_000) });
|
|
361
380
|
} catch (error) {
|
|
362
381
|
if (error instanceof GatewayCheatError) {
|
|
363
382
|
report(`the gateway cheated: ${error.code} on payment ${error.paymentId}`);
|
|
@@ -390,44 +409,33 @@ open, and refuses the payment with a `424` unless the nonce comes back, so deplo
|
|
|
390
409
|
the endpoint before you register it.
|
|
391
410
|
|
|
392
411
|
```ts
|
|
393
|
-
import {
|
|
394
|
-
ThunderBridge,
|
|
395
|
-
answerWebhookChallengeRequest,
|
|
396
|
-
isProvablySettled,
|
|
397
|
-
parseSettlementRequest,
|
|
398
|
-
} from "thunder-bridge";
|
|
412
|
+
import { ThunderBridge } from "thunder-bridge";
|
|
399
413
|
|
|
400
414
|
declare function fulfil(paymentId: string, preimage: string): Promise<void>;
|
|
401
415
|
|
|
402
416
|
const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe");
|
|
403
|
-
const signer = { publicKey: await gateway.webhookKey() };
|
|
404
417
|
|
|
405
|
-
export
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const settled = await parseSettlementRequest(request, signer);
|
|
412
|
-
if (settled === null) {
|
|
413
|
-
return new Response("bad signature", { status: 401 });
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const preimage = isProvablySettled(settled) ? settled.preimage : null;
|
|
417
|
-
if (preimage === null) {
|
|
418
|
-
return new Response("no preimage that hashes to it", { status: 402 });
|
|
419
|
-
}
|
|
418
|
+
export const POST = gateway.serve.webhook({
|
|
419
|
+
onSettled: async (settlement) => {
|
|
420
|
+
await fulfil(settlement.id, settlement.preimage);
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
```
|
|
420
424
|
|
|
421
|
-
|
|
425
|
+
That route answers the challenge, reads the gateway's own published key, checks the
|
|
426
|
+
signature, and calls you only for a delivery that proves itself: it says paid and it
|
|
427
|
+
carries a preimage that hashes to the payment hash the same body names. A delivery
|
|
428
|
+
that proves nothing gets a `202` and no callback, because acting on an unproven
|
|
429
|
+
claim is the one thing this refuses to do. Pass `onUnproven` when an expiry is news
|
|
430
|
+
you want.
|
|
422
431
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
```
|
|
432
|
+
`onSettled` is handed a `Proven<Settlement>`, so the preimage is a `string` rather
|
|
433
|
+
than something to coerce: the check the route already ran is what narrows it.
|
|
426
434
|
|
|
427
|
-
`
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
435
|
+
`gateway.serve.readSettlement` and `gateway.serve.readPayment` are the same checks
|
|
436
|
+
without the route, for a handler you would rather write yourself. Ask the
|
|
437
|
+
recipient's own server with `proveSettlement` when you want the proof to come from
|
|
438
|
+
somewhere other than the delivery.
|
|
431
439
|
|
|
432
440
|
## More
|
|
433
441
|
|
|
@@ -436,8 +444,10 @@ from somewhere other than the delivery.
|
|
|
436
444
|
- [docs/proving-a-payment.md](../docs/proving-a-payment.md) - the five origin checks
|
|
437
445
|
and their failure codes, what settlement means, making the gateway poll nobody but
|
|
438
446
|
you, the NWC rail, wrapped invoices, and webhooks in full
|
|
447
|
+
- [docs/api.md](../docs/api.md) - every export, generated from the code
|
|
448
|
+
- [docs/recipes.md](../docs/recipes.md) - one runnable program per use case, every name in
|
|
449
|
+
it linked to its own entry in the reference
|
|
439
450
|
- [the gateway](../README.md) - one level up in this repository
|
|
440
|
-
- [examples](../examples) - a paywall on Deno Deploy, a trigger watcher, a bank rail
|
|
441
451
|
|
|
442
452
|
## Development
|
|
443
453
|
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** One incoming payment as the bank booked it, in the smallest unit of its currency */
|
|
2
|
+
interface Credit {
|
|
3
|
+
amountMinor: number;
|
|
4
|
+
currency: string;
|
|
5
|
+
/** Whatever the payer wrote, wherever this bank puts it. Matching is a substring, so noise around it is fine */
|
|
6
|
+
reference: string;
|
|
7
|
+
/**
|
|
8
|
+
* Unix seconds. A bank that books a day rather than an instant, as Fio does,
|
|
9
|
+
* gives the day's midnight in its own zone, so rendering this in UTC can show
|
|
10
|
+
* the day before. Nothing here matches on it, it is yours to read
|
|
11
|
+
*/
|
|
12
|
+
bookedAt: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recent credits on one account, oldest or newest first, it makes no difference.
|
|
16
|
+
* This is the whole plugin seam: a bank is a function of this shape, and
|
|
17
|
+
* `fioStatement` is one implementation of it
|
|
18
|
+
*/
|
|
19
|
+
type Statement = (sinceUnix: number) => Promise<Credit[]>;
|
|
20
|
+
/** One transfer to ask for: what is owed, where it lands, and where its arrival is read back from */
|
|
21
|
+
interface BankTransferParams {
|
|
22
|
+
/** Long lived and server side. The preimage is derived from it, so losing it loses every proof */
|
|
23
|
+
secret: string;
|
|
24
|
+
/** What the payer must leave on the transfer, an order id or a nonce. It is matched, not stored */
|
|
25
|
+
reference: string;
|
|
26
|
+
/** The price in the smallest unit, so 48055 is 480.55 CZK */
|
|
27
|
+
amountMinor: number;
|
|
28
|
+
/** The account the money goes to, as an IBAN */
|
|
29
|
+
iban: string;
|
|
30
|
+
/** Where `bankVerifyEndpoint` is mounted, a public https URL with no query of its own */
|
|
31
|
+
verifyUrl: string;
|
|
32
|
+
/** When the offer dies, in unix seconds. Money in a bank moves on banking days, so give it days */
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
/** Defaults to CZK */
|
|
35
|
+
currency?: string;
|
|
36
|
+
/** Up to ten digits, for accounting systems that still want one */
|
|
37
|
+
variableSymbol?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Groups this transfer with everything else paid to the same secret, so one
|
|
40
|
+
* `followTrigger` socket hears about it. Give the Lightning leg of the same
|
|
41
|
+
* order the same secret and both rails arrive on one stream
|
|
42
|
+
*/
|
|
43
|
+
trigger?: string;
|
|
44
|
+
/** How many settlements of that trigger the gateway keeps replayable past the hour, needs `trigger` */
|
|
45
|
+
replay?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Handed back untouched on that stream, so a watcher learns which order settled
|
|
48
|
+
* without asking anyone. `seal` it and the gateway cannot read it either
|
|
49
|
+
*/
|
|
50
|
+
sealed?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Where the gateway posts once the money lands, a public https URL. Without one
|
|
53
|
+
* a transfer is only ever learned by following the trigger or asking
|
|
54
|
+
*/
|
|
55
|
+
webhookUrl?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Register on a gateway you do not own anyway. The verify URL names the amount
|
|
58
|
+
* and the reference, so its operator ends up reading your order book, and the
|
|
59
|
+
* URL itself answers whether that order was paid. Say true only when the order
|
|
60
|
+
* book is not worth hiding
|
|
61
|
+
*/
|
|
62
|
+
allowPublicGateway?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** A transfer the gateway is now watching, and the descriptor the payer scans */
|
|
65
|
+
interface BankTransfer {
|
|
66
|
+
/** The watched payment's id at the gateway, which is how you read this order back */
|
|
67
|
+
id: string;
|
|
68
|
+
/** What the gateway was given, and what the preimage has to hash to */
|
|
69
|
+
paymentHash: string;
|
|
70
|
+
/** The same URL you mounted, carrying what to look for and a signature over it */
|
|
71
|
+
verifyUrl: string;
|
|
72
|
+
/** The payer scans this, it is a Short Payment Descriptor, the Czech QR platba format */
|
|
73
|
+
spd: string;
|
|
74
|
+
}
|
|
75
|
+
/** The endpoint the gateway polls for a bank transfer, answering off your own statement */
|
|
76
|
+
interface BankVerifyConfig {
|
|
77
|
+
/** The same secret `bankTransfer` was given */
|
|
78
|
+
secret: string;
|
|
79
|
+
/** The account to read */
|
|
80
|
+
statement: Statement;
|
|
81
|
+
/** How far back a credit still counts, seven days by default */
|
|
82
|
+
lookBackSecs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
85
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
86
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
87
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
88
|
+
*/
|
|
89
|
+
pollEverySecs?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { BankTransfer as B, Credit as C, Statement as S, BankTransferParams as a, BankVerifyConfig as b };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** One incoming payment as the bank booked it, in the smallest unit of its currency */
|
|
2
|
+
interface Credit {
|
|
3
|
+
amountMinor: number;
|
|
4
|
+
currency: string;
|
|
5
|
+
/** Whatever the payer wrote, wherever this bank puts it. Matching is a substring, so noise around it is fine */
|
|
6
|
+
reference: string;
|
|
7
|
+
/**
|
|
8
|
+
* Unix seconds. A bank that books a day rather than an instant, as Fio does,
|
|
9
|
+
* gives the day's midnight in its own zone, so rendering this in UTC can show
|
|
10
|
+
* the day before. Nothing here matches on it, it is yours to read
|
|
11
|
+
*/
|
|
12
|
+
bookedAt: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Recent credits on one account, oldest or newest first, it makes no difference.
|
|
16
|
+
* This is the whole plugin seam: a bank is a function of this shape, and
|
|
17
|
+
* `fioStatement` is one implementation of it
|
|
18
|
+
*/
|
|
19
|
+
type Statement = (sinceUnix: number) => Promise<Credit[]>;
|
|
20
|
+
/** One transfer to ask for: what is owed, where it lands, and where its arrival is read back from */
|
|
21
|
+
interface BankTransferParams {
|
|
22
|
+
/** Long lived and server side. The preimage is derived from it, so losing it loses every proof */
|
|
23
|
+
secret: string;
|
|
24
|
+
/** What the payer must leave on the transfer, an order id or a nonce. It is matched, not stored */
|
|
25
|
+
reference: string;
|
|
26
|
+
/** The price in the smallest unit, so 48055 is 480.55 CZK */
|
|
27
|
+
amountMinor: number;
|
|
28
|
+
/** The account the money goes to, as an IBAN */
|
|
29
|
+
iban: string;
|
|
30
|
+
/** Where `bankVerifyEndpoint` is mounted, a public https URL with no query of its own */
|
|
31
|
+
verifyUrl: string;
|
|
32
|
+
/** When the offer dies, in unix seconds. Money in a bank moves on banking days, so give it days */
|
|
33
|
+
expiresAt: number;
|
|
34
|
+
/** Defaults to CZK */
|
|
35
|
+
currency?: string;
|
|
36
|
+
/** Up to ten digits, for accounting systems that still want one */
|
|
37
|
+
variableSymbol?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Groups this transfer with everything else paid to the same secret, so one
|
|
40
|
+
* `followTrigger` socket hears about it. Give the Lightning leg of the same
|
|
41
|
+
* order the same secret and both rails arrive on one stream
|
|
42
|
+
*/
|
|
43
|
+
trigger?: string;
|
|
44
|
+
/** How many settlements of that trigger the gateway keeps replayable past the hour, needs `trigger` */
|
|
45
|
+
replay?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Handed back untouched on that stream, so a watcher learns which order settled
|
|
48
|
+
* without asking anyone. `seal` it and the gateway cannot read it either
|
|
49
|
+
*/
|
|
50
|
+
sealed?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Where the gateway posts once the money lands, a public https URL. Without one
|
|
53
|
+
* a transfer is only ever learned by following the trigger or asking
|
|
54
|
+
*/
|
|
55
|
+
webhookUrl?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Register on a gateway you do not own anyway. The verify URL names the amount
|
|
58
|
+
* and the reference, so its operator ends up reading your order book, and the
|
|
59
|
+
* URL itself answers whether that order was paid. Say true only when the order
|
|
60
|
+
* book is not worth hiding
|
|
61
|
+
*/
|
|
62
|
+
allowPublicGateway?: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** A transfer the gateway is now watching, and the descriptor the payer scans */
|
|
65
|
+
interface BankTransfer {
|
|
66
|
+
/** The watched payment's id at the gateway, which is how you read this order back */
|
|
67
|
+
id: string;
|
|
68
|
+
/** What the gateway was given, and what the preimage has to hash to */
|
|
69
|
+
paymentHash: string;
|
|
70
|
+
/** The same URL you mounted, carrying what to look for and a signature over it */
|
|
71
|
+
verifyUrl: string;
|
|
72
|
+
/** The payer scans this, it is a Short Payment Descriptor, the Czech QR platba format */
|
|
73
|
+
spd: string;
|
|
74
|
+
}
|
|
75
|
+
/** The endpoint the gateway polls for a bank transfer, answering off your own statement */
|
|
76
|
+
interface BankVerifyConfig {
|
|
77
|
+
/** The same secret `bankTransfer` was given */
|
|
78
|
+
secret: string;
|
|
79
|
+
/** The account to read */
|
|
80
|
+
statement: Statement;
|
|
81
|
+
/** How far back a credit still counts, seven days by default */
|
|
82
|
+
lookBackSecs?: number;
|
|
83
|
+
/**
|
|
84
|
+
* How often you want the gateway to ask, in seconds. It goes out as
|
|
85
|
+
* `Cache-Control: max-age`, so the pace is yours to set rather than the
|
|
86
|
+
* gateway's, and a bank that updates once a minute should say so instead of
|
|
87
|
+
* being polled every few seconds. Thirty by default, clamped to an hour
|
|
88
|
+
*/
|
|
89
|
+
pollEverySecs?: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type { BankTransfer as B, Credit as C, Statement as S, BankTransferParams as a, BankVerifyConfig as b };
|