lnurlcash-kit 0.1.2 → 0.2.1
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/CHANGELOG.md +342 -0
- package/README.md +314 -0
- package/dist/index.d.ts +169 -68
- package/dist/index.js +531 -135
- package/llms.txt +67 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -122,6 +122,31 @@ This is not hypothetical: the same hazard broke the
|
|
|
122
122
|
development, by two different mechanisms, and is now a named scenario in the
|
|
123
123
|
conformance vectors.
|
|
124
124
|
|
|
125
|
+
Because a retry cannot always be prevented, a mutation refused with the
|
|
126
|
+
input already spent or unknown carries its outputs anyway:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
try {
|
|
130
|
+
await rotateNote(callback, oldK1)
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const secrets = newSecretsOf(err) // works on both error families
|
|
133
|
+
if (secrets.length) {
|
|
134
|
+
await save(secrets) // first. always.
|
|
135
|
+
// then ask: is there a note at that secret?
|
|
136
|
+
const fate = await probeBurnedNote(buildNoteUrl(base, secrets[0]))
|
|
137
|
+
// 'live' -> the mutation landed and you own the output
|
|
138
|
+
// 'gone' -> the refusal was honest, discard
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The class does not change: at the wire a retry and a genuine double spend are
|
|
144
|
+
the same answer, and whether your input was live when the request went out is
|
|
145
|
+
something you know and this library does not. So it hands back the secret
|
|
146
|
+
rather than a verdict. A refusal that cannot be a landed mutation, such as a
|
|
147
|
+
mint refusing on policy grounds, carries nothing, and you can discard your
|
|
148
|
+
staged records at once.
|
|
149
|
+
|
|
125
150
|
**4. A melt's `OK` means "in flight", not "spent".** The service pays
|
|
126
151
|
asynchronously and only burns the note once the payment settles, restoring
|
|
127
152
|
it if the payment fails. A failed melt is never reported back through the
|
|
@@ -136,6 +161,12 @@ who saw the unpaid invoice can poll for it — the payment hash travels inside
|
|
|
136
161
|
the invoice. First rotater wins. A wallet that rotates on settlement wins by
|
|
137
162
|
construction; a human copying a preimage by hand does not.
|
|
138
163
|
|
|
164
|
+
That is a race, and the way to win a race is not to enter it. Where a mint
|
|
165
|
+
advertises `mintToHash`, name the note you are buying and the preimage is
|
|
166
|
+
not its secret at all: see
|
|
167
|
+
[Minting a note you named yourself](#minting-a-note-you-named-yourself).
|
|
168
|
+
The rule above stands for every mint that does not offer it.
|
|
169
|
+
|
|
139
170
|
## Offline verification
|
|
140
171
|
|
|
141
172
|
A service may sign each note with its Lightning node identity key, so a
|
|
@@ -156,6 +187,289 @@ The signature commits to the note's *hash*, not its secret — so you can
|
|
|
156
187
|
prove a mint issued a note, to expose one that will not honour it, without
|
|
157
188
|
handing over what would let anyone spend it.
|
|
158
189
|
|
|
190
|
+
### When a mint rotates its signing key
|
|
191
|
+
|
|
192
|
+
Rotating invalidates nothing. The notes already issued are still genuine and
|
|
193
|
+
their signatures still verify, but only against the key that made them, so a
|
|
194
|
+
wallet holding the new key alone would suddenly read every outstanding note
|
|
195
|
+
as forged. A mint publishes the keys it has retired as `previousPubkeys` on
|
|
196
|
+
its mint address, and verification takes the whole set:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const {mintPubkey, previousPubkeys = []} = await fetchMintAddress(addressUrl)
|
|
200
|
+
const check = verifyNoteSignatureAgainst(k1, amountMsat, sig, [
|
|
201
|
+
mintPubkey,
|
|
202
|
+
...previousPubkeys
|
|
203
|
+
])
|
|
204
|
+
// check.pubkey names the key that signed. A note that verifies only against
|
|
205
|
+
// a retired one is worth rotating: the mint re-signs it under the current key.
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`verifyNoteSignature` takes the same one-or-many argument and returns a plain
|
|
209
|
+
boolean. An empty list is a rejection, not a pass.
|
|
210
|
+
|
|
211
|
+
### What else a mint says about itself
|
|
212
|
+
|
|
213
|
+
`mintPubkey` is the key note signatures verify against, and it is *not* the
|
|
214
|
+
Lightning node's key: that one is embedded in `nodeUri`, and every other
|
|
215
|
+
`node*` field really is about the node. Verifying a note against the key
|
|
216
|
+
pulled out of `nodeUri` fails, and the failure says nothing about why.
|
|
217
|
+
`nodePubkey` is a deprecated alias for the same value, kept for one release.
|
|
218
|
+
|
|
219
|
+
`fetchMintAddress` reads the experimental discovery endpoint, and a mint may
|
|
220
|
+
publish a `name`, a `description`, `contact` details, a `tosUrl`, a `motd`,
|
|
221
|
+
its structured `fees` and its software `version` there. All optional, all
|
|
222
|
+
absent on most mints, and none of it is needed to spend a note. Surface the
|
|
223
|
+
MOTD when it changes: it is how an operator announces maintenance, a fee
|
|
224
|
+
change or a sunset date, and there is no other channel to a bearer holder.
|
|
225
|
+
The endpoint carries no LUD number, so treat a rejection as "no extra
|
|
226
|
+
information" and fall back to `fetchPayRequest`.
|
|
227
|
+
|
|
228
|
+
## Secrets
|
|
229
|
+
|
|
230
|
+
A note's `k1` is generated by the wallet, and LUD-25 says nothing about how.
|
|
231
|
+
Draw it from a CSPRNG and the note lives only in your wallet file: the mint
|
|
232
|
+
holds `sha256(k1)` and cannot tell you apart from a stranger, so a lost file
|
|
233
|
+
is lost money. Derive it from a seed instead and the wallet restores from
|
|
234
|
+
words alone.
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
root = HMAC-SHA256(key = utf8("lnurlcash-note-v1"), msg = seed)
|
|
238
|
+
k1_i = HMAC-SHA256(key = root, msg = utf8(host + ":" + index))
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
`seed` is raw bytes. A 64-byte BIP39 seed is what wallets use in practice,
|
|
242
|
+
but nothing here depends on BIP39, so a device with its own entropy store
|
|
243
|
+
derives the same way and no consumer carries a wordlist it does not need.
|
|
244
|
+
`host` is the mint host exactly as `serverOf` spells it, lowercase and with
|
|
245
|
+
the port where there is one, so `127.0.0.1:8899` and `mint.example` never
|
|
246
|
+
collide. `index` is decimal ASCII from 0. The output is 32 bytes of hex, the
|
|
247
|
+
size of a payment preimage, and the mint sees nothing different: it only
|
|
248
|
+
ever receives `sha256(k1)`.
|
|
249
|
+
|
|
250
|
+
Because the scheme is written down here rather than invented per wallet, the
|
|
251
|
+
same words restore the same notes in a *different* wallet. That
|
|
252
|
+
cross-wallet portability is the point of putting it in the kit, with
|
|
253
|
+
[a conformance vector](https://github.com/TheCryptoDonkey/lnurlcash-conformance)
|
|
254
|
+
for the ports.
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import {deriveNoteRoot, derivedSecretSource, restoreNotes} from 'lnurlcash-kit'
|
|
258
|
+
|
|
259
|
+
const root = deriveNoteRoot(seed) // seed: Uint8Array, yours to keep safe
|
|
260
|
+
const source = derivedSecretSource(root, 'mint.example', counter)
|
|
261
|
+
|
|
262
|
+
// hand it to any mutating call and the fresh secrets come from the seed
|
|
263
|
+
const {k1, change} = await splitNote(callback, [note], 40_000, {randomSecret: source})
|
|
264
|
+
saveCounter('mint.example', source.index()) // a split consumed two indices
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Persist that counter in the **same write that stages the new records**, and
|
|
268
|
+
do it **before** the hash goes on the wire. A crash between the bump and the
|
|
269
|
+
request wastes an index, which costs nothing. A crash the other way round
|
|
270
|
+
re-derives a secret the mint has already seen, and the second note minted at
|
|
271
|
+
it collides with the first. This is the rule wallets get wrong.
|
|
272
|
+
|
|
273
|
+
Restoring walks the indices and asks the mint what each derived secret is
|
|
274
|
+
worth:
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
const {found, next} = await restoreNotes('https://mint.example/w', root, 'mint.example')
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
A live note is recorded, a spent index still counts as used (re-deriving it
|
|
281
|
+
would mint a duplicate), an unknown one counts towards the gap, and the walk
|
|
282
|
+
stops after 20 consecutive unknowns. `next` is the counter to resume from.
|
|
283
|
+
Restoring puts every `k1` it walks on the wire and a restored note carries no
|
|
284
|
+
signature, so rotate each one straight after: that closes the exposure and
|
|
285
|
+
gets the signature in the same call.
|
|
286
|
+
|
|
287
|
+
The seed is bearer material for every note the wallet will ever hold. Store
|
|
288
|
+
it the way you store the notes, and never log it.
|
|
289
|
+
|
|
290
|
+
## Minting a note you named yourself
|
|
291
|
+
|
|
292
|
+
By default the secret of a freshly minted note is the invoice's payment
|
|
293
|
+
preimage, which means the money is a thing two sets of people learn without
|
|
294
|
+
being trusted. Every routing node on the payment path sees it, because that
|
|
295
|
+
is how HTLC settlement works. And anyone who merely saw the unpaid invoice
|
|
296
|
+
can poll LUD-21 `verify` with the payment hash inside it and take the
|
|
297
|
+
preimage the moment it settles, which is what a QR code on a desktop screen
|
|
298
|
+
hands out.
|
|
299
|
+
|
|
300
|
+
A mint can instead bind the note to a hash you supply, the same `h` you
|
|
301
|
+
already send on every rotate, split and merge. Then you chose the secret,
|
|
302
|
+
nobody else ever had it, and the preimage is an ordinary payment proof.
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
import {
|
|
306
|
+
fetchPayRequest, requestInvoice, claimMintedNote,
|
|
307
|
+
deriveNoteRoot, deriveNoteSecret, hashK1
|
|
308
|
+
} from 'lnurlcash-kit'
|
|
309
|
+
|
|
310
|
+
const pay = await fetchPayRequest(payUrl) // a Lightning Address resolves here
|
|
311
|
+
if (!pay.mintToHash) { /* preimage path, rotate on claim */ }
|
|
312
|
+
|
|
313
|
+
const root = deriveNoteRoot(seed)
|
|
314
|
+
const k1 = deriveNoteSecret(root, 'mint.example', nextIndex)
|
|
315
|
+
await persist({k1, index: nextIndex}) // BEFORE the invoice. always.
|
|
316
|
+
|
|
317
|
+
const {pr} = await requestInvoice(pay.callback, 21_000, {h: hashK1(k1)})
|
|
318
|
+
|
|
319
|
+
// pay `pr`, then poll. No verify, because you already know the secret.
|
|
320
|
+
const claim = await claimMintedNote(pay.withdrawLink!, k1)
|
|
321
|
+
// 'unminted' -> not settled yet, ask again
|
|
322
|
+
// 'minted' -> claim.amountMsat is what it is worth, claim.callback melts it
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Ask before you buy. `mintToHash` is how a mint says it accepts the parameter,
|
|
326
|
+
and reading it first is the difference between naming your own note and paying
|
|
327
|
+
for one whose secret three other parties can learn. It turns up in three
|
|
328
|
+
places, saying three different things:
|
|
329
|
+
|
|
330
|
+
| Where | What it means |
|
|
331
|
+
| --- | --- |
|
|
332
|
+
| `PayRequestInfo.mintToHash` | "I accept an `h`." **Decide from this one.** |
|
|
333
|
+
| `MintAddressInfo.mintToHash` | the same fact on the experimental discovery document |
|
|
334
|
+
| `InvoiceResult.mintToHash` | "I bound *this quote* to the hash you named" |
|
|
335
|
+
|
|
336
|
+
Prefer the payRequest: it is the only endpoint every mint has, it is where
|
|
337
|
+
your wallet already is when it is about to mint, and it sits next to the
|
|
338
|
+
`withdrawLink` the draft already hangs there for LNURLcash's sake. Fall back
|
|
339
|
+
to `fetchMintAddress` for a mint that only advertises on that document. A mint
|
|
340
|
+
that says it in neither place ignores the `h`, keys the note by the preimage
|
|
341
|
+
as it always has, and the verify path is unchanged. Anything that is not
|
|
342
|
+
exactly `true` is a no, everywhere, and `false` on the invoice result is
|
|
343
|
+
silence rather than a refusal, so decide from the advertisement and claim by
|
|
344
|
+
probing.
|
|
345
|
+
|
|
346
|
+
**Persist the secret before you ask for the invoice.** Paying for a note and
|
|
347
|
+
then losing the secret is the one way this is worse than the preimage scheme,
|
|
348
|
+
and persisting first removes it. Derive it rather than drawing it at random
|
|
349
|
+
and there is a second reason: the note is then seed-derived *from birth*, so
|
|
350
|
+
`restoreNotes` finds it without any rotate having happened. Under the preimage
|
|
351
|
+
scheme a minted note lives outside your derivation until the immediate rotate
|
|
352
|
+
pulls it in, and a wallet that crashes in that window cannot recover the note
|
|
353
|
+
from its words.
|
|
354
|
+
|
|
355
|
+
No rotate follows a bound claim. The preimage scheme needs one because the
|
|
356
|
+
mint made the secret and hands it out; here the mint never had it, so the note
|
|
357
|
+
is yours from the moment it exists. The claim GET does show the secret to the
|
|
358
|
+
mint it is a claim on, which is a different thing from showing it to whoever
|
|
359
|
+
scanned the QR, and you can still rotate if you want the offline signature.
|
|
360
|
+
|
|
361
|
+
## Asking to be paid
|
|
362
|
+
|
|
363
|
+
"Send me 500 sat" today means handing over a Lightning Address, which is a
|
|
364
|
+
mint-and-zap round trip through the mint's node for something neither party
|
|
365
|
+
needed a node for. A payment request names the amount, the mints the payee
|
|
366
|
+
will accept and where to deliver, and the payer's wallet splits a note and
|
|
367
|
+
sends it straight across. Wallet to wallet; the mint only ever sees a split.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import {encodePaymentRequest, decodePaymentRequest, paymentRequestAmountMsat} from 'lnurlcash-kit'
|
|
371
|
+
|
|
372
|
+
const encoded = encodePaymentRequest({
|
|
373
|
+
v: 1,
|
|
374
|
+
id: '0123456789abcdef', // 8 random bytes, hex
|
|
375
|
+
amount: '500', // whole sats, decimal string
|
|
376
|
+
currency: 'sat',
|
|
377
|
+
methodDetails: {mints: ['mint.example']},
|
|
378
|
+
to: 'npub1...', // or alice@mint.example
|
|
379
|
+
memo: 'lunch'
|
|
380
|
+
})
|
|
381
|
+
// lnurlcashreq1eyJhbW91bnQiOiI1MDAiLCJjdXJyZW5jeSI6InNhdCIsImlkIjoiMDEy...
|
|
382
|
+
|
|
383
|
+
const request = decodePaymentRequest(scanned) // throws ProtocolError if it is not one
|
|
384
|
+
const owed = paymentRequestAmountMsat(request) // 500_000
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The encoding is `lnurlcashreq1` followed by base64url of the request as
|
|
388
|
+
[JCS](https://www.rfc-editor.org/rfc/rfc8785)-canonical JSON, which is
|
|
389
|
+
NUT-18's `creqA` idiom with our own prefix. Canonical because a request is a
|
|
390
|
+
thing people copy, quote back and match against a record of what they asked
|
|
391
|
+
for: two encodings of the same request must be the same string, or none of
|
|
392
|
+
that works. It stays short enough for one static QR.
|
|
393
|
+
|
|
394
|
+
The object is the same charge request an HTTP 402 lnurlcash rail serves,
|
|
395
|
+
plus the transport fields a wallet-to-wallet send needs, so one encoder
|
|
396
|
+
covers both. Validation is strict in both directions, including an
|
|
397
|
+
unrecognised field: quietly paying a request you did not fully understand is
|
|
398
|
+
how you pay the wrong person.
|
|
399
|
+
|
|
400
|
+
`amount` is in **sat**, and it is the one exception to this library's
|
|
401
|
+
msat-everywhere rule. That is deliberate: the field is shared with the 402
|
|
402
|
+
rail and the Cashu payment method, both of which count in whole units. Use
|
|
403
|
+
`paymentRequestAmountMsat` rather than multiplying by hand.
|
|
404
|
+
|
|
405
|
+
An expired request will not decode, because paying one is always wrong. At
|
|
406
|
+
the expiry counts as expired, not merely past it, so a payer whose clock is a
|
|
407
|
+
second behind the payee's does not send a note against a request the payee
|
|
408
|
+
has already written off. `isPaymentRequest` still returns true for it, so a
|
|
409
|
+
scanner routes it to the pay screen and the user is told it lapsed rather
|
|
410
|
+
than that their input was gibberish; `decodePaymentRequest(input, {now: 0})`
|
|
411
|
+
returns it for display.
|
|
412
|
+
|
|
413
|
+
`to` is checked, not merely shape-matched: an npub has to survive its bech32
|
|
414
|
+
checksum, because a request naming a destination nobody can route to is a
|
|
415
|
+
request nobody can pay.
|
|
416
|
+
|
|
417
|
+
**`lnurlcashreq1` means this schema and nothing else.** An earlier HTTP 402
|
|
418
|
+
rail emitted a shorter object under the same prefix (`{"a": 21, "m":
|
|
419
|
+
["mint.example"], "u": "sat"}`: amount as a number, no version, no id). Two
|
|
420
|
+
schemas under one prefix cannot both be right, and this is the one the
|
|
421
|
+
vectors pin. The decoder reads the short form anyway, because refusing a
|
|
422
|
+
string it can plainly understand helps nobody, and gives it a deterministic
|
|
423
|
+
id derived from its own bytes. Nothing here ever emits it.
|
|
424
|
+
|
|
425
|
+
## Taking a note as payment
|
|
426
|
+
|
|
427
|
+
A server that accepts bearer notes for something makes the same decisions
|
|
428
|
+
every time, in this order, and `settleNoteForValue` is that order written
|
|
429
|
+
once:
|
|
430
|
+
|
|
431
|
+
```ts
|
|
432
|
+
import {settleNoteForValue, InsufficientValueError} from 'lnurlcash-kit'
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
const {note, newUrl} = await settleNoteForValue(offered, {
|
|
436
|
+
mints: ['mint.example'], // hosts this server accepts. An empty list accepts nothing.
|
|
437
|
+
minMsat: 21_000, // the price
|
|
438
|
+
requireSignature: false // demand offline proof of issuance first
|
|
439
|
+
})
|
|
440
|
+
grantAccess() // note.k1 is yours now; newUrl is a note to store or melt
|
|
441
|
+
} catch (err) {
|
|
442
|
+
if (err instanceof InsufficientValueError) refuse(err.amountMsat, err.minMsat)
|
|
443
|
+
else refuse()
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
1. the input parses as a note at all
|
|
448
|
+
2. its mint is one this server accepts (checked before any round trip, so an
|
|
449
|
+
unaccepted mint is never contacted)
|
|
450
|
+
3. an informational GET for the **authoritative** value and the mint's key
|
|
451
|
+
4. the signature, where the server demands one, over the value the mint
|
|
452
|
+
stated rather than the one the URL claims
|
|
453
|
+
5. that value covers the price
|
|
454
|
+
6. **rotate**
|
|
455
|
+
|
|
456
|
+
Step 6 is the settlement, not bookkeeping after it. Rotating burns the
|
|
457
|
+
secret the payer handed over and mints a replacement only this server knows,
|
|
458
|
+
in one atomic request: it transfers ownership and rejects a replay in the
|
|
459
|
+
same call, because a second presentation of the same note finds it spent. A
|
|
460
|
+
server that checks a note's value and grants access without rotating has
|
|
461
|
+
verified a photograph of a banknote.
|
|
462
|
+
|
|
463
|
+
Refusals are typed, and nothing is burned by any of them: `ServiceRejectedError`
|
|
464
|
+
for an unaccepted mint or a signature that will not verify,
|
|
465
|
+
`InsufficientValueError` (carrying both amounts) for a note worth too
|
|
466
|
+
little, `NoteSpentError` for one already spent or presented twice,
|
|
467
|
+
`PendingNoteError` for one with a melt in flight, which is worth retrying
|
|
468
|
+
rather than refusing outright. `AmbiguousMutationError` from the rotate is
|
|
469
|
+
the case to handle with care: persist `err.newSecrets` before anything else,
|
|
470
|
+
because if the request landed then that secret is the money and it is now
|
|
471
|
+
this server's.
|
|
472
|
+
|
|
159
473
|
## Scope
|
|
160
474
|
|
|
161
475
|
This library speaks the protocol. It does not store notes, hold keys, manage
|
package/dist/index.d.ts
CHANGED
|
@@ -1,34 +1,19 @@
|
|
|
1
|
-
declare const isBech32Lnurl: (data: string) => boolean;
|
|
2
|
-
declare const toBech32Lnurl: (url: string) => string;
|
|
3
|
-
declare const fromBech32Lnurl: (data: string) => string | null;
|
|
4
|
-
declare const isAllowedServiceUrl: (value: string) => boolean;
|
|
5
|
-
declare const fromLud17: (url: string) => string;
|
|
6
|
-
declare const toLud17w: (url: string) => string;
|
|
7
|
-
declare const isLightningAddress: (value: string) => boolean;
|
|
8
|
-
declare const resolveMintInput: (value: string) => string | null;
|
|
9
|
-
declare const mintAddressUrl: (payUrl: string) => string | null;
|
|
10
|
-
declare const lightningAddressUsername: (payUrl: string) => string | null;
|
|
11
|
-
declare const resolveLnurlInput: (value: string) => string | null;
|
|
12
|
-
declare const serverOf: (url: string) => string;
|
|
13
|
-
|
|
14
|
-
declare const noteK1: (url: string) => string | null;
|
|
15
|
-
declare const requireNoteK1: (url: string) => string;
|
|
16
|
-
declare const noteDeclaredAmount: (url: string) => number | null;
|
|
17
|
-
declare const noteSignature: (url: string) => string | null;
|
|
18
|
-
declare const resolveNoteInput: (value: string) => string | null;
|
|
19
|
-
declare const isValidNoteInput: (value: string) => boolean;
|
|
20
|
-
declare const buildNoteUrl: (withdrawLink: string, k1: string, amountMsat?: number) => string;
|
|
21
|
-
declare const withNewK1: (url: string, k1: string, amountMsat: number, signature?: string) => string;
|
|
22
|
-
declare const withoutK1: (url: string, amountMsat: number, signature?: string) => string;
|
|
23
|
-
|
|
24
1
|
declare const hashK1: (k1: string) => string;
|
|
25
2
|
type RandomSecret = () => string;
|
|
26
3
|
declare const defaultRandomSecret: RandomSecret;
|
|
27
4
|
declare const isPreimage: (value: string) => boolean;
|
|
5
|
+
declare const deriveNoteRoot: (seed: Uint8Array) => Uint8Array;
|
|
6
|
+
declare const deriveNoteSecret: (root: Uint8Array, host: string, index: number) => string;
|
|
7
|
+
declare const derivedSecretSource: (root: Uint8Array, host: string, start?: number) => RandomSecret & {
|
|
8
|
+
index: () => number;
|
|
9
|
+
};
|
|
28
10
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
11
|
+
type LnurlcashOptions = {
|
|
12
|
+
fetch?: typeof globalThis.fetch;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
offline?: boolean;
|
|
15
|
+
randomSecret?: RandomSecret;
|
|
16
|
+
};
|
|
32
17
|
|
|
33
18
|
type MintFee = {
|
|
34
19
|
baseFeeMsat: number;
|
|
@@ -46,45 +31,6 @@ declare const grossUpForMintFee: (netMsat: number, fee: MintFee) => number;
|
|
|
46
31
|
declare const formatFeePercent: (ppm: number) => string;
|
|
47
32
|
declare const describeMintFee: (fee: MintFee) => string;
|
|
48
33
|
|
|
49
|
-
declare const isBolt11Invoice: (value: string) => boolean;
|
|
50
|
-
declare const sameInvoice: (a: string, b: string) => boolean;
|
|
51
|
-
declare const decodeBolt11AmountMsat: (pr: string) => number | null;
|
|
52
|
-
|
|
53
|
-
declare class LnurlcashError extends Error {
|
|
54
|
-
constructor(message: string);
|
|
55
|
-
}
|
|
56
|
-
declare class RequestRefusedError extends LnurlcashError {
|
|
57
|
-
}
|
|
58
|
-
declare class ProtocolError extends LnurlcashError {
|
|
59
|
-
}
|
|
60
|
-
declare class ServiceRejectedError extends LnurlcashError {
|
|
61
|
-
readonly reason: string;
|
|
62
|
-
constructor(reason: string);
|
|
63
|
-
}
|
|
64
|
-
declare class PendingNoteError extends ServiceRejectedError {
|
|
65
|
-
constructor(reason?: string);
|
|
66
|
-
}
|
|
67
|
-
declare class NoteSpentError extends ServiceRejectedError {
|
|
68
|
-
constructor(reason: string);
|
|
69
|
-
}
|
|
70
|
-
declare class NoteUnknownError extends ServiceRejectedError {
|
|
71
|
-
constructor(reason: string);
|
|
72
|
-
}
|
|
73
|
-
declare class AmbiguousMintError extends LnurlcashError {
|
|
74
|
-
}
|
|
75
|
-
declare class AmbiguousMutationError extends AmbiguousMintError {
|
|
76
|
-
readonly newSecrets: string[];
|
|
77
|
-
constructor(message: string, newSecrets: string[]);
|
|
78
|
-
}
|
|
79
|
-
declare const classifyNoteError: (reason: string) => ServiceRejectedError;
|
|
80
|
-
|
|
81
|
-
type LnurlcashOptions = {
|
|
82
|
-
fetch?: typeof globalThis.fetch;
|
|
83
|
-
timeoutMs?: number;
|
|
84
|
-
offline?: boolean;
|
|
85
|
-
randomSecret?: RandomSecret;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
34
|
type WithdrawRequestInfo = {
|
|
89
35
|
tag: 'withdrawRequest';
|
|
90
36
|
callback: string;
|
|
@@ -93,15 +39,22 @@ type WithdrawRequestInfo = {
|
|
|
93
39
|
maxWithdrawable: number;
|
|
94
40
|
defaultDescription?: string;
|
|
95
41
|
mintPubkey?: string;
|
|
42
|
+
payLink?: string;
|
|
96
43
|
};
|
|
97
44
|
declare const fetchNoteInfo: (url: string, options?: LnurlcashOptions) => Promise<WithdrawRequestInfo>;
|
|
98
45
|
declare const probeBurnedNote: (url: string, options?: LnurlcashOptions) => Promise<"live" | "gone" | "unknown">;
|
|
46
|
+
type MintContact = {
|
|
47
|
+
nostr?: string;
|
|
48
|
+
email?: string;
|
|
49
|
+
url?: string;
|
|
50
|
+
};
|
|
99
51
|
type MintAddressInfo = {
|
|
100
52
|
tag: 'withdrawRequest';
|
|
101
53
|
callback: string;
|
|
102
54
|
minWithdrawable: number;
|
|
103
55
|
maxWithdrawable: number;
|
|
104
56
|
defaultDescription?: string;
|
|
57
|
+
mintPubkey?: string;
|
|
105
58
|
nodePubkey?: string;
|
|
106
59
|
payLink: string;
|
|
107
60
|
nodeAlias?: string;
|
|
@@ -110,6 +63,15 @@ type MintAddressInfo = {
|
|
|
110
63
|
nodeCapacityMsat?: number;
|
|
111
64
|
nodeNumChannels?: number;
|
|
112
65
|
nodeNumPeers?: number;
|
|
66
|
+
name?: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
contact?: MintContact;
|
|
69
|
+
tosUrl?: string;
|
|
70
|
+
motd?: string;
|
|
71
|
+
fees?: MintFee;
|
|
72
|
+
version?: string;
|
|
73
|
+
previousPubkeys?: string[];
|
|
74
|
+
mintToHash?: boolean;
|
|
113
75
|
};
|
|
114
76
|
declare const fetchMintAddress: (url: string, options?: LnurlcashOptions) => Promise<MintAddressInfo>;
|
|
115
77
|
type WithdrawSuccessResponse = {
|
|
@@ -163,20 +125,156 @@ type PayRequestInfo = {
|
|
|
163
125
|
withdrawLink?: string;
|
|
164
126
|
mintPubkey?: string;
|
|
165
127
|
mintFee?: MintFee;
|
|
128
|
+
mintToHash?: boolean;
|
|
166
129
|
};
|
|
167
130
|
declare const fetchPayRequest: (url: string, options?: LnurlcashOptions) => Promise<PayRequestInfo>;
|
|
168
131
|
type InvoiceResult = {
|
|
169
132
|
pr: string;
|
|
170
133
|
verify?: string;
|
|
171
134
|
disposable: boolean;
|
|
135
|
+
mintToHash: boolean;
|
|
136
|
+
};
|
|
137
|
+
type InvoiceRequestOptions = LnurlcashOptions & {
|
|
138
|
+
h?: string;
|
|
172
139
|
};
|
|
173
|
-
declare const requestInvoice: (payCallback: string, amountMsat: number, options?:
|
|
140
|
+
declare const requestInvoice: (payCallback: string, amountMsat: number, options?: InvoiceRequestOptions) => Promise<InvoiceResult>;
|
|
174
141
|
type VerifyResult = {
|
|
175
142
|
settled: boolean;
|
|
176
143
|
preimage: string | null;
|
|
177
144
|
pr: string;
|
|
178
145
|
};
|
|
179
146
|
declare const fetchInvoiceVerification: (verifyUrl: string, options?: LnurlcashOptions) => Promise<VerifyResult>;
|
|
147
|
+
type MintClaim = {
|
|
148
|
+
state: 'minted' | 'unminted' | 'pending' | 'spent';
|
|
149
|
+
k1: string;
|
|
150
|
+
amountMsat: number | null;
|
|
151
|
+
callback: string | null;
|
|
152
|
+
};
|
|
153
|
+
declare const claimMintedNote: (withdrawLink: string, k1: string, options?: LnurlcashOptions) => Promise<MintClaim>;
|
|
154
|
+
|
|
155
|
+
type SettleForValueOptions = {
|
|
156
|
+
mints: string[];
|
|
157
|
+
minMsat: number;
|
|
158
|
+
requireSignature?: boolean;
|
|
159
|
+
};
|
|
160
|
+
type SettledForValue = {
|
|
161
|
+
note: SettledNote;
|
|
162
|
+
newUrl: string;
|
|
163
|
+
};
|
|
164
|
+
declare const settleNoteForValue: (noteUrl: string, { mints, minMsat, requireSignature }: SettleForValueOptions, options?: LnurlcashOptions) => Promise<SettledForValue>;
|
|
165
|
+
|
|
166
|
+
type RestoredNote = {
|
|
167
|
+
index: number;
|
|
168
|
+
k1: string;
|
|
169
|
+
amountMsat: number | null;
|
|
170
|
+
state: 'live' | 'pending';
|
|
171
|
+
};
|
|
172
|
+
type RestoreResult = {
|
|
173
|
+
found: RestoredNote[];
|
|
174
|
+
next: number;
|
|
175
|
+
};
|
|
176
|
+
type RestoreOptions = {
|
|
177
|
+
gap?: number;
|
|
178
|
+
start?: number;
|
|
179
|
+
};
|
|
180
|
+
declare const restoreNotes: (baseUrl: string, root: Uint8Array, host: string, { gap, start }?: RestoreOptions, options?: LnurlcashOptions) => Promise<RestoreResult>;
|
|
181
|
+
|
|
182
|
+
declare const isBech32Lnurl: (data: string) => boolean;
|
|
183
|
+
declare const toBech32Lnurl: (url: string) => string;
|
|
184
|
+
declare const fromBech32Lnurl: (data: string) => string | null;
|
|
185
|
+
declare const isAllowedServiceUrl: (value: string) => boolean;
|
|
186
|
+
declare const fromLud17: (url: string) => string;
|
|
187
|
+
declare const toLud17w: (url: string) => string;
|
|
188
|
+
declare const isLightningAddress: (value: string) => boolean;
|
|
189
|
+
declare const resolveMintInput: (value: string) => string | null;
|
|
190
|
+
declare const mintAddressUrl: (payUrl: string) => string | null;
|
|
191
|
+
declare const lightningAddressUsername: (payUrl: string) => string | null;
|
|
192
|
+
declare const resolveLnurlInput: (value: string) => string | null;
|
|
193
|
+
declare const serverOf: (url: string) => string;
|
|
194
|
+
|
|
195
|
+
declare const noteK1: (url: string) => string | null;
|
|
196
|
+
declare const requireNoteK1: (url: string) => string;
|
|
197
|
+
declare const noteDeclaredAmount: (url: string) => number | null;
|
|
198
|
+
declare const noteSignature: (url: string) => string | null;
|
|
199
|
+
declare const resolveNoteInput: (value: string) => string | null;
|
|
200
|
+
declare const isValidNoteInput: (value: string) => boolean;
|
|
201
|
+
declare const buildNoteUrl: (withdrawLink: string, k1: string, amountMsat?: number) => string;
|
|
202
|
+
declare const withNewK1: (url: string, k1: string, amountMsat: number, signature?: string) => string;
|
|
203
|
+
declare const withoutK1: (url: string, amountMsat: number, signature?: string) => string;
|
|
204
|
+
|
|
205
|
+
declare const PAYMENT_REQUEST_PREFIX = "lnurlcashreq1";
|
|
206
|
+
type PaymentRequestMethodDetails = {
|
|
207
|
+
mints: string[];
|
|
208
|
+
mintPubkeys?: string[];
|
|
209
|
+
};
|
|
210
|
+
type PaymentRequest = {
|
|
211
|
+
v: 1;
|
|
212
|
+
id: string;
|
|
213
|
+
amount: string;
|
|
214
|
+
currency: 'sat';
|
|
215
|
+
methodDetails: PaymentRequestMethodDetails;
|
|
216
|
+
to?: string;
|
|
217
|
+
memo?: string;
|
|
218
|
+
expires?: number;
|
|
219
|
+
};
|
|
220
|
+
declare const paymentRequestAmountMsat: (request: PaymentRequest) => number;
|
|
221
|
+
declare const encodePaymentRequest: (request: PaymentRequest) => string;
|
|
222
|
+
type DecodeOptions = {
|
|
223
|
+
now?: number;
|
|
224
|
+
};
|
|
225
|
+
declare const decodePaymentRequest: (value: string, { now }?: DecodeOptions) => PaymentRequest;
|
|
226
|
+
declare const isPaymentRequest: (value: string) => boolean;
|
|
227
|
+
|
|
228
|
+
declare const noteSignatureMessage: (k1: string, amountMsat: number) => string;
|
|
229
|
+
declare const noteSignatureDigest: (k1: string, amountMsat: number) => Uint8Array;
|
|
230
|
+
type SignatureCheck = {
|
|
231
|
+
valid: true;
|
|
232
|
+
pubkey: string;
|
|
233
|
+
} | {
|
|
234
|
+
valid: false;
|
|
235
|
+
pubkey: null;
|
|
236
|
+
};
|
|
237
|
+
declare const verifyNoteSignatureAgainst: (k1: string, amountMsat: number, signatureHex: string, mintPubkeys: string | string[]) => SignatureCheck;
|
|
238
|
+
declare const verifyNoteSignature: (k1: string, amountMsat: number, signatureHex: string, mintPubkeys: string | string[]) => boolean;
|
|
239
|
+
|
|
240
|
+
declare const isBolt11Invoice: (value: string) => boolean;
|
|
241
|
+
declare const sameInvoice: (a: string, b: string) => boolean;
|
|
242
|
+
declare const decodeBolt11AmountMsat: (pr: string) => number | null;
|
|
243
|
+
|
|
244
|
+
declare class LnurlcashError extends Error {
|
|
245
|
+
constructor(message: string);
|
|
246
|
+
}
|
|
247
|
+
declare class RequestRefusedError extends LnurlcashError {
|
|
248
|
+
}
|
|
249
|
+
declare class ProtocolError extends LnurlcashError {
|
|
250
|
+
}
|
|
251
|
+
declare class ServiceRejectedError extends LnurlcashError {
|
|
252
|
+
readonly reason: string;
|
|
253
|
+
newSecrets?: string[];
|
|
254
|
+
constructor(reason: string);
|
|
255
|
+
}
|
|
256
|
+
declare class PendingNoteError extends ServiceRejectedError {
|
|
257
|
+
constructor(reason?: string);
|
|
258
|
+
}
|
|
259
|
+
declare class NoteSpentError extends ServiceRejectedError {
|
|
260
|
+
constructor(reason: string);
|
|
261
|
+
}
|
|
262
|
+
declare class NoteUnknownError extends ServiceRejectedError {
|
|
263
|
+
constructor(reason: string);
|
|
264
|
+
}
|
|
265
|
+
declare class AmbiguousMintError extends LnurlcashError {
|
|
266
|
+
}
|
|
267
|
+
declare class AmbiguousMutationError extends AmbiguousMintError {
|
|
268
|
+
readonly newSecrets: string[];
|
|
269
|
+
constructor(message: string, newSecrets: string[]);
|
|
270
|
+
}
|
|
271
|
+
declare class InsufficientValueError extends ServiceRejectedError {
|
|
272
|
+
readonly amountMsat: number;
|
|
273
|
+
readonly minMsat: number;
|
|
274
|
+
constructor(amountMsat: number, minMsat: number);
|
|
275
|
+
}
|
|
276
|
+
declare const newSecretsOf: (err: unknown) => string[];
|
|
277
|
+
declare const classifyNoteError: (reason: string) => ServiceRejectedError;
|
|
180
278
|
|
|
181
279
|
declare const createClient: (options?: LnurlcashOptions) => {
|
|
182
280
|
fetchNoteInfo: (url: string) => Promise<WithdrawRequestInfo>;
|
|
@@ -191,9 +289,12 @@ declare const createClient: (options?: LnurlcashOptions) => {
|
|
|
191
289
|
mergeNotesWithHash: (callback: string, k1s: string[], h: string) => Promise<HashedMutationResult>;
|
|
192
290
|
settleNote: (baseUrl: string, k1: string, expectedAmountMsat: number, signature?: string) => Promise<SettledNote>;
|
|
193
291
|
fetchPayRequest: (url: string) => Promise<PayRequestInfo>;
|
|
194
|
-
requestInvoice: (payCallback: string, amountMsat: number) => Promise<InvoiceResult>;
|
|
292
|
+
requestInvoice: (payCallback: string, amountMsat: number, h?: string) => Promise<InvoiceResult>;
|
|
195
293
|
fetchInvoiceVerification: (verifyUrl: string) => Promise<VerifyResult>;
|
|
294
|
+
claimMintedNote: (withdrawLink: string, k1: string) => Promise<MintClaim>;
|
|
295
|
+
restoreNotes: (baseUrl: string, root: Uint8Array, host: string, restoreOptions?: RestoreOptions) => Promise<RestoreResult>;
|
|
296
|
+
settleNoteForValue: (noteUrl: string, terms: SettleForValueOptions) => Promise<SettledForValue>;
|
|
196
297
|
};
|
|
197
298
|
type LnurlcashClient = ReturnType<typeof createClient>;
|
|
198
299
|
|
|
199
|
-
export { AmbiguousMintError, AmbiguousMutationError, type HashedMutationResult, type HashedSplitResult, type InvoiceResult, type LnurlcashClient, LnurlcashError, type LnurlcashOptions, type MeltResult, type MintAddressInfo, type MintFee, type MintFeeBand, NoteSpentError, NoteUnknownError, type PayRequestInfo, PendingNoteError, ProtocolError, type RandomSecret, RequestRefusedError, type RotateResult, ServiceRejectedError, type SettledNote, type SplitResult, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteUrl, classifyNoteError, createClient, decodeBolt11AmountMsat, defaultRandomSecret, describeMintFee, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureMessage, parseMintFee, probeBurnedNote, requestInvoice, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, verifyNoteSignature, withNewK1, withinMintFeeBand, withoutK1 };
|
|
300
|
+
export { AmbiguousMintError, AmbiguousMutationError, type DecodeOptions, type HashedMutationResult, type HashedSplitResult, InsufficientValueError, type InvoiceRequestOptions, type InvoiceResult, type LnurlcashClient, LnurlcashError, type LnurlcashOptions, type MeltResult, type MintAddressInfo, type MintClaim, type MintContact, type MintFee, type MintFeeBand, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, type PayRequestInfo, type PaymentRequest, type PaymentRequestMethodDetails, PendingNoteError, ProtocolError, type RandomSecret, RequestRefusedError, type RestoreOptions, type RestoreResult, type RestoredNote, type RotateResult, ServiceRejectedError, type SettleForValueOptions, type SettledForValue, type SettledNote, type SignatureCheck, type SplitResult, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureMessage, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, verifyNoteSignature, verifyNoteSignatureAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|