lnurlcash-kit 0.7.0 → 0.8.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/CHANGELOG.md +75 -0
- package/README.md +93 -25
- package/dist/index.d.ts +30 -1
- package/dist/index.js +193 -45
- package/llms.txt +23 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.8.0 - 2026-09-04
|
|
6
|
+
|
|
7
|
+
**LUD-25's own derivation, and it is now the one to mint under.** The draft's
|
|
8
|
+
"Seed-recoverable note secrets" section specifies a BIP-32 scheme under
|
|
9
|
+
`m/139'`, and the reference wallet implements it. This kit had shipped its own
|
|
10
|
+
HMAC scheme four days before that section existed. One convention is the whole
|
|
11
|
+
point of writing either of them down, so the specified one wins.
|
|
12
|
+
|
|
13
|
+
- `deriveCashRoot(seed)`, `deriveCashDomainNode(root, host)`,
|
|
14
|
+
`deriveCashSecret(root, host, index)`, `cashSecretAt(domainNode, index)`,
|
|
15
|
+
`cashSecretSource(root, host, start)`, `cashNodeToHex` / `cashNodeFromHex`
|
|
16
|
+
and `deriveCashChild` in a new `cash.ts`. Additive - nothing existing
|
|
17
|
+
changes shape.
|
|
18
|
+
- The scheme, in full, so this entry alone is enough to reimplement it:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
cashHashingKey = m/139'/0
|
|
22
|
+
(d1, d2, d3, d4) = HMAC-SHA256(key = cashHashingKey, msg = utf8(host))[0..16]
|
|
23
|
+
as 4 big-endian uint32
|
|
24
|
+
k1_i = m/139'/d1/d2/d3/d4/i'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`d1..d4` are used **exactly as they fall**. BIP-32 already reads any index
|
|
28
|
+
`>= 2^31` as hardened, so which of the four levels are hardened is decided
|
|
29
|
+
by the mint's host name and roughly half of them will be. Masking the top
|
|
30
|
+
bit, or hardening all four, derives a different tree and restores nothing,
|
|
31
|
+
silently. Only `i` is always hardened. `host` is what `serverOf` produces -
|
|
32
|
+
lowercase, port included - byte-identical to the reference wallet's.
|
|
33
|
+
|
|
34
|
+
Worked example. The BIP39 mnemonic `abandon abandon abandon abandon abandon
|
|
35
|
+
abandon abandon abandon abandon abandon abandon about` with an empty
|
|
36
|
+
passphrase gives `m/139'` as
|
|
37
|
+
`c7a2496e9b453a67c5d2a1f04936ec1259440d45454c795a99a66269e4cd3005111e1cc966fca2fe32f054f14caceab90449e536d94cf6935ea12a087e414f60`
|
|
38
|
+
(privateKey || chainCode). At `mint.example` the four levels are
|
|
39
|
+
`[2589708612, 3693348916, 172082394, 3793182078]`, of which the third is
|
|
40
|
+
the only unhardened one, and index 0 is
|
|
41
|
+
`de5b81405a12e1297b350d80e2ad85043ed5b9436a0c5592d3302778de330499`.
|
|
42
|
+
- **The hardware-signer path.** Every unhardened level sits at or above the
|
|
43
|
+
per-mint node, so a signer provisioned with `deriveCashDomainNode`'s output
|
|
44
|
+
rather than the seed needs no elliptic curve at all: each `i'` beneath it is
|
|
45
|
+
HMAC-SHA512 and one modular addition. Whoever derives that node can derive
|
|
46
|
+
every note secret the wallet will hold at that mint, so it is provisioning
|
|
47
|
+
material - one mint's subtree, not the wallet.
|
|
48
|
+
- BIP-32 is implemented here from its own primitives rather than pulled in as
|
|
49
|
+
a dependency, and tested against BIP-32's published test vector 1. Every
|
|
50
|
+
LUD-25 value above is checked against output from the reference
|
|
51
|
+
implementation's own library.
|
|
52
|
+
|
|
53
|
+
**`restoreFromSeed` walks both schemes.** Notes minted under the old scheme
|
|
54
|
+
are still money and a wallet that walked only the new one would leave them at
|
|
55
|
+
a mint it can no longer name.
|
|
56
|
+
|
|
57
|
+
- `restoreFromSeed(baseUrl, seed, host, {gap?, start?, probeK1?,
|
|
58
|
+
allowSecretDisclosure?}, opts?)`. `RestoredNote.scheme` is `bip32` or
|
|
59
|
+
`hmac`, `next` is `{bip32, hmac}`, and `start` takes one per scheme.
|
|
60
|
+
- `restoreNotes` is unchanged in signature and behaviour, and still walks the
|
|
61
|
+
legacy scheme alone. `RestoredNote` and `UnresolvedIndex` gain a `scheme`
|
|
62
|
+
field.
|
|
63
|
+
- `deriveNoteRoot`, `deriveNoteSecret` and `derivedSecretSource` are not
|
|
64
|
+
deprecated and are not going anywhere. Do not mint under them.
|
|
65
|
+
|
|
66
|
+
**Say plainly what a restore can and cannot do.** LUD-25 requires a hash
|
|
67
|
+
lookup to answer for a burned note exactly as it answers for one that never
|
|
68
|
+
existed, and both reference mints do. So a by-hash walk cannot see a spent
|
|
69
|
+
index at all; and since a rotate burns the *old* index, a wallet's spent
|
|
70
|
+
indices sit below its live ones, and one that has rotated more than `gap`
|
|
71
|
+
times scans as completely empty. The persisted per-host counter is what makes
|
|
72
|
+
recovery work - the scan is the fallback. That counter is not secret, so it
|
|
73
|
+
belongs in an ordinary backup, and a restore should merge counters upwards
|
|
74
|
+
only. Documented on `RestoreOptions.start`, in the README and in `llms.txt`.
|
|
75
|
+
|
|
76
|
+
Graded against `lnurlcash-conformance` 0.7.0, whose `cash-derivation.json`
|
|
77
|
+
cases this suite now runs: the LUD-25 path, the four domain levels per host,
|
|
78
|
+
the hardened-by-magnitude flags, and BIP-32's own published test vector 1.
|
|
79
|
+
|
|
5
80
|
## 0.7.0 - 2026-09-04
|
|
6
81
|
|
|
7
82
|
**Offline verification is mandatory, and this library now insists on it.**
|
package/README.md
CHANGED
|
@@ -245,36 +245,87 @@ information" and fall back to `fetchPayRequest`.
|
|
|
245
245
|
|
|
246
246
|
## Secrets
|
|
247
247
|
|
|
248
|
-
A note's `k1` is generated by the wallet
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
A note's `k1` is generated by the wallet. Draw it from a CSPRNG and the note
|
|
249
|
+
lives only in your wallet file: the mint holds `sha256(k1)` and cannot tell
|
|
250
|
+
you apart from a stranger, so a lost file is lost money. Derive it from a
|
|
251
|
+
seed instead and the wallet restores from words alone, and the same words
|
|
252
|
+
restore the same notes in a *different* wallet.
|
|
253
|
+
|
|
254
|
+
LUD-25 specifies how, and this is the scheme to mint under:
|
|
253
255
|
|
|
254
256
|
```
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
cashHashingKey = m/139'/0
|
|
258
|
+
(d1, d2, d3, d4) = HMAC-SHA256(key = cashHashingKey, msg = utf8(host))[0..16] as 4 uint32
|
|
259
|
+
k1_i = m/139'/d1/d2/d3/d4/i'
|
|
257
260
|
```
|
|
258
261
|
|
|
262
|
+
`d1..d4` are used **exactly as they fall**. BIP-32 reads any index `>= 2^31`
|
|
263
|
+
as hardened, so which of the four levels are hardened is decided by the
|
|
264
|
+
mint's own host name, and half of them will be. Do not mask the top bit and
|
|
265
|
+
do not harden all four: either one derives a different tree, and a wallet
|
|
266
|
+
restoring against it finds nothing, silently. Only `i` is always hardened.
|
|
267
|
+
|
|
259
268
|
`seed` is raw bytes. A 64-byte BIP39 seed is what wallets use in practice,
|
|
260
269
|
but nothing here depends on BIP39, so a device with its own entropy store
|
|
261
270
|
derives the same way and no consumer carries a wordlist it does not need.
|
|
262
271
|
`host` is the mint host exactly as `serverOf` spells it, lowercase and with
|
|
263
272
|
the port where there is one, so `127.0.0.1:8899` and `mint.example` never
|
|
264
|
-
collide. `index`
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
collide. `index` counts from 0. The output is 32 bytes of hex, the size of a
|
|
274
|
+
payment preimage, and the mint sees nothing different: it only ever receives
|
|
275
|
+
`sha256(k1)`.
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import {deriveCashRoot, cashSecretSource, restoreFromSeed} from 'lnurlcash-kit'
|
|
279
|
+
|
|
280
|
+
const root = deriveCashRoot(seed) // seed: Uint8Array, yours to keep safe
|
|
281
|
+
const source = cashSecretSource(root, 'mint.example', counter)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### The hardware-signer path
|
|
285
|
+
|
|
286
|
+
Every unhardened level sits at or above the per-mint node, so a signer given
|
|
287
|
+
`m/139'/d1/d2/d3/d4` rather than the seed needs **no elliptic curve at all**:
|
|
288
|
+
each `i'` beneath it is HMAC-SHA512 and one modular addition. That is the
|
|
289
|
+
difference between a device that can do LUD-25 recovery and one that would
|
|
290
|
+
need secp256k1 added to its firmware.
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import {deriveCashDomainNode, cashNodeToHex, cashNodeFromHex, cashSecretAt} from 'lnurlcash-kit'
|
|
294
|
+
|
|
295
|
+
const node = deriveCashDomainNode(root, 'mint.example')
|
|
296
|
+
provision(cashNodeToHex(node)) // 64 bytes: privateKey || chainCode
|
|
297
|
+
|
|
298
|
+
// on the device, or anywhere holding only that node
|
|
299
|
+
cashSecretAt(cashNodeFromHex(hex), index)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Whoever derives that node can derive every note secret the wallet will ever
|
|
303
|
+
hold **at that mint**. It is provisioning material: one mint's subtree, not
|
|
304
|
+
the wallet.
|
|
305
|
+
|
|
306
|
+
### The legacy scheme
|
|
307
|
+
|
|
308
|
+
This kit shipped its own derivation in 0.2.0, four days before LUD-25 had a
|
|
309
|
+
section on one:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
root = HMAC-SHA256(key = utf8("lnurlcash-note-v1"), msg = seed)
|
|
313
|
+
k1_i = HMAC-SHA256(key = root, msg = utf8(host + ":" + index))
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`deriveNoteRoot`, `deriveNoteSecret` and `derivedSecretSource` still
|
|
317
|
+
implement it and are not going anywhere, because notes minted under it are
|
|
318
|
+
still money. Do not mint under it. `restoreFromSeed` walks it alongside the
|
|
319
|
+
specified scheme so none of those notes goes missing.
|
|
320
|
+
|
|
321
|
+
Both schemes ship with
|
|
322
|
+
[conformance vectors](https://github.com/TheCryptoDonkey/lnurlcash-conformance)
|
|
272
323
|
for the ports.
|
|
273
324
|
|
|
274
325
|
```ts
|
|
275
326
|
import {deriveNoteRoot, derivedSecretSource, restoreNotes} from 'lnurlcash-kit'
|
|
276
327
|
|
|
277
|
-
const root = deriveNoteRoot(seed) //
|
|
328
|
+
const root = deriveNoteRoot(seed) // legacy: existing notes only
|
|
278
329
|
const source = derivedSecretSource(root, 'mint.example', counter)
|
|
279
330
|
|
|
280
331
|
// hand it to any mutating call and the fresh secrets come from the seed
|
|
@@ -288,20 +339,37 @@ request wastes an index, which costs nothing. A crash the other way round
|
|
|
288
339
|
re-derives a secret the mint has already seen, and the second note minted at
|
|
289
340
|
it collides with the first. This is the rule wallets get wrong.
|
|
290
341
|
|
|
342
|
+
### Restoring
|
|
343
|
+
|
|
291
344
|
Restoring walks the indices and asks the mint what each derived secret is
|
|
292
|
-
worth:
|
|
345
|
+
worth. From a seed it walks both schemes at once:
|
|
293
346
|
|
|
294
347
|
```ts
|
|
295
|
-
const {found, next} = await
|
|
348
|
+
const {found, next} = await restoreFromSeed('https://mint.example/w', seed, 'mint.example')
|
|
349
|
+
// found[].scheme is 'bip32' or 'hmac'; next is {bip32, hmac}
|
|
296
350
|
```
|
|
297
351
|
|
|
298
|
-
A live note is recorded,
|
|
299
|
-
|
|
300
|
-
stops after 20 consecutive unknowns. `next` is the counter to resume from.
|
|
301
|
-
Restoring puts every `k1` it walks on the wire and a restored note carries no
|
|
352
|
+
A live note is recorded, an unknown index counts towards the gap, and the
|
|
353
|
+
walk stops after 20 consecutive unknowns. A restored note carries no
|
|
302
354
|
signature, so rotate each one straight after: that closes the exposure and
|
|
303
355
|
gets the signature in the same call.
|
|
304
356
|
|
|
357
|
+
**The scan is the fallback, not the backup.** LUD-25 requires a hash lookup
|
|
358
|
+
to answer for a burned note exactly as it answers for one that never existed,
|
|
359
|
+
so a by-hash walk cannot see a spent index at all. A rotate burns the *old*
|
|
360
|
+
index, which means a wallet's spent indices sit below its live ones: rotate
|
|
361
|
+
more than `gap` times and a scan from 0 finds nothing whatever. The counter
|
|
362
|
+
`next` gives you is the thing that makes recovery work, so **persist it and
|
|
363
|
+
back it up**. It is not secret - an index reveals nothing without the root -
|
|
364
|
+
so it belongs in an ordinary backup, and a restore should merge counters
|
|
365
|
+
upwards only, never down.
|
|
366
|
+
|
|
367
|
+
Only a walk that discloses raw secrets (`allowSecretDisclosure`) sees "spent"
|
|
368
|
+
at all, because only a `k1` lookup gets that answer. It costs the whole
|
|
369
|
+
window it walked: every index it touched is burned whether or not a note was
|
|
370
|
+
ever minted there, since the secret is in someone's log now. `next` skips
|
|
371
|
+
them for you.
|
|
372
|
+
|
|
305
373
|
The seed is bearer material for every note the wallet will ever hold. Store
|
|
306
374
|
it the way you store the notes, and never log it.
|
|
307
375
|
|
|
@@ -323,14 +391,14 @@ had it, and the preimage is ordinary payment proof.
|
|
|
323
391
|
```ts
|
|
324
392
|
import {
|
|
325
393
|
fetchPayRequest, requestInvoice, claimMintedNote,
|
|
326
|
-
|
|
394
|
+
deriveCashRoot, deriveCashSecret, hashK1, namesMintOutput
|
|
327
395
|
} from 'lnurlcash-kit'
|
|
328
396
|
|
|
329
397
|
const pay = await fetchPayRequest(payUrl) // a Lightning Address resolves here
|
|
330
398
|
if (!namesMintOutput(pay)) throw new Error('mint lacks commentAllowed: 64')
|
|
331
399
|
|
|
332
|
-
const root =
|
|
333
|
-
const k1 =
|
|
400
|
+
const root = deriveCashRoot(seed)
|
|
401
|
+
const k1 = deriveCashSecret(root, 'mint.example', nextIndex)
|
|
334
402
|
await persist({k1, index: nextIndex}) // BEFORE the invoice. always.
|
|
335
403
|
|
|
336
404
|
const {pr} = await requestInvoice(pay.callback, 21_000, {h: hashK1(k1)})
|
package/dist/index.d.ts
CHANGED
|
@@ -191,9 +191,11 @@ type SettledForValue = {
|
|
|
191
191
|
};
|
|
192
192
|
declare const settleNoteForValue: (noteUrl: string, { mints, minMsat, requireSignature }: SettleForValueOptions, options?: LnurlcashOptions) => Promise<SettledForValue>;
|
|
193
193
|
|
|
194
|
+
type NoteScheme = 'bip32' | 'hmac';
|
|
194
195
|
type RestoredNote = {
|
|
195
196
|
index: number;
|
|
196
197
|
k1: string;
|
|
198
|
+
scheme: NoteScheme;
|
|
197
199
|
amountMsat: number | null;
|
|
198
200
|
state: 'live' | 'pending';
|
|
199
201
|
callback?: string;
|
|
@@ -201,6 +203,7 @@ type RestoredNote = {
|
|
|
201
203
|
type UnresolvedIndex = {
|
|
202
204
|
index: number;
|
|
203
205
|
k1: string;
|
|
206
|
+
scheme: NoteScheme;
|
|
204
207
|
reason: string;
|
|
205
208
|
};
|
|
206
209
|
type RestoreResult = {
|
|
@@ -217,6 +220,15 @@ type RestoreOptions = {
|
|
|
217
220
|
allowSecretDisclosure?: boolean;
|
|
218
221
|
};
|
|
219
222
|
declare const restoreNotes: (baseUrl: string, root: Uint8Array, host: string, { gap, start, probeK1, allowSecretDisclosure }?: RestoreOptions, options?: LnurlcashOptions) => Promise<RestoreResult>;
|
|
223
|
+
type SeedRestoreOptions = Omit<RestoreOptions, 'start'> & {
|
|
224
|
+
start?: {
|
|
225
|
+
[K in NoteScheme]?: number;
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
type SeedRestoreResult = Omit<RestoreResult, 'next'> & {
|
|
229
|
+
next: Record<NoteScheme, number>;
|
|
230
|
+
};
|
|
231
|
+
declare const restoreFromSeed: (baseUrl: string, seed: Uint8Array, host: string, { gap, start, probeK1, allowSecretDisclosure }?: SeedRestoreOptions, options?: LnurlcashOptions) => Promise<SeedRestoreResult>;
|
|
220
232
|
|
|
221
233
|
declare const isBech32Lnurl: (data: string) => boolean;
|
|
222
234
|
declare const toBech32Lnurl: (url: string) => string;
|
|
@@ -242,6 +254,22 @@ declare const buildNoteUrl: (withdrawLink: string, k1: string, amountMsat?: numb
|
|
|
242
254
|
declare const withNewK1: (url: string, k1: string, amountMsat: number, signature?: string) => string;
|
|
243
255
|
declare const withoutK1: (url: string, amountMsat: number, signature?: string) => string;
|
|
244
256
|
|
|
257
|
+
type CashNode = {
|
|
258
|
+
privateKey: Uint8Array;
|
|
259
|
+
chainCode: Uint8Array;
|
|
260
|
+
};
|
|
261
|
+
declare const deriveCashChild: (node: CashNode, index: number) => CashNode;
|
|
262
|
+
declare const deriveCashRoot: (seed: Uint8Array) => CashNode;
|
|
263
|
+
declare const cashDomainIndices: (root: CashNode, host: string) => number[];
|
|
264
|
+
declare const deriveCashDomainNode: (root: CashNode, host: string) => CashNode;
|
|
265
|
+
declare const cashSecretAt: (domainNode: CashNode, index: number) => string;
|
|
266
|
+
declare const deriveCashSecret: (root: CashNode, host: string, index: number) => string;
|
|
267
|
+
declare const cashNodeToHex: (node: CashNode) => string;
|
|
268
|
+
declare const cashNodeFromHex: (hex: string) => CashNode;
|
|
269
|
+
declare const cashSecretSource: (root: CashNode, host: string, start?: number) => RandomSecret & {
|
|
270
|
+
index: () => number;
|
|
271
|
+
};
|
|
272
|
+
|
|
245
273
|
declare const PAYMENT_REQUEST_PREFIX = "lnurlcashreq1";
|
|
246
274
|
type PaymentRequestMethodDetails = {
|
|
247
275
|
mints: string[];
|
|
@@ -344,8 +372,9 @@ declare const createClient: (options?: LnurlcashOptions) => {
|
|
|
344
372
|
fetchInvoiceVerification: (verifyUrl: string) => Promise<VerifyResult>;
|
|
345
373
|
claimMintedNote: (withdrawLink: string, k1: string) => Promise<MintClaim>;
|
|
346
374
|
restoreNotes: (baseUrl: string, root: Uint8Array, host: string, restoreOptions?: RestoreOptions) => Promise<RestoreResult>;
|
|
375
|
+
restoreFromSeed: (baseUrl: string, seed: Uint8Array, host: string, restoreOptions?: SeedRestoreOptions) => Promise<SeedRestoreResult>;
|
|
347
376
|
settleNoteForValue: (noteUrl: string, terms: SettleForValueOptions) => Promise<SettledForValue>;
|
|
348
377
|
};
|
|
349
378
|
type LnurlcashClient = ReturnType<typeof createClient>;
|
|
350
379
|
|
|
351
|
-
export { AmbiguousMintError, AmbiguousMutationError, type BoundMintCommitment, type DecodeOptions, HashLookupUnsupportedError, 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, type NoteInfoByHash, 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 UnresolvedIndex, UnverifiableNoteError, type ValidatedBoundMintReceipt, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteInfoUrlByHash, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
|
380
|
+
export { AmbiguousMintError, AmbiguousMutationError, type BoundMintCommitment, type CashNode, type DecodeOptions, HashLookupUnsupportedError, 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, type NoteInfoByHash, type NoteScheme, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, type PayRequestInfo, type PaymentRequest, type PaymentRequestMethodDetails, PendingNoteError, ProtocolError, type RandomSecret, RequestRefusedError, type RestoreOptions, type RestoreResult, type RestoredNote, type RotateResult, type SeedRestoreOptions, type SeedRestoreResult, ServiceRejectedError, type SettleForValueOptions, type SettledForValue, type SettledNote, type SignatureCheck, type SplitResult, type UnresolvedIndex, UnverifiableNoteError, type ValidatedBoundMintReceipt, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteInfoUrlByHash, buildNoteUrl, cashDomainIndices, cashNodeFromHex, cashNodeToHex, cashSecretAt, cashSecretSource, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveCashChild, deriveCashDomainNode, deriveCashRoot, deriveCashSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreFromSeed, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bech32, base64urlnopad } from '@scure/base';
|
|
2
2
|
import { hmac } from '@noble/hashes/hmac.js';
|
|
3
|
-
import { sha256 } from '@noble/hashes/sha2.js';
|
|
3
|
+
import { sha256, sha512 } from '@noble/hashes/sha2.js';
|
|
4
4
|
import { utf8ToBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
5
5
|
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
6
6
|
|
|
@@ -218,6 +218,89 @@ var withoutK1 = (url, amountMsat, signature) => {
|
|
|
218
218
|
else newUrl.searchParams.delete("sig");
|
|
219
219
|
return newUrl.toString();
|
|
220
220
|
};
|
|
221
|
+
var HARDENED = 2147483648;
|
|
222
|
+
var CURVE_N = secp256k1.Point.Fn.ORDER;
|
|
223
|
+
var MASTER_KEY_DOMAIN = utf8ToBytes("Bitcoin seed");
|
|
224
|
+
var CASH_PURPOSE = 139;
|
|
225
|
+
var numberOf = (bytes) => bytes.length === 0 ? 0n : BigInt(`0x${bytesToHex(bytes)}`);
|
|
226
|
+
var to32Bytes = (value) => hexToBytes(value.toString(16).padStart(64, "0"));
|
|
227
|
+
var readUint32BE = (bytes, offset) => (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
|
|
228
|
+
var deriveCashChild = (node, index) => {
|
|
229
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > 4294967295) {
|
|
230
|
+
throw new RangeError(`A BIP-32 child index must be a uint32, not ${index}.`);
|
|
231
|
+
}
|
|
232
|
+
const data = new Uint8Array(37);
|
|
233
|
+
if (index >= HARDENED) {
|
|
234
|
+
data.set(node.privateKey, 1);
|
|
235
|
+
} else {
|
|
236
|
+
data.set(
|
|
237
|
+
secp256k1.Point.BASE.multiply(numberOf(node.privateKey)).toBytes(true),
|
|
238
|
+
0
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
data[33] = index >>> 24 & 255;
|
|
242
|
+
data[34] = index >>> 16 & 255;
|
|
243
|
+
data[35] = index >>> 8 & 255;
|
|
244
|
+
data[36] = index & 255;
|
|
245
|
+
const material = hmac(sha512, node.chainCode, data);
|
|
246
|
+
const left = numberOf(material.subarray(0, 32));
|
|
247
|
+
const key = (left + numberOf(node.privateKey)) % CURVE_N;
|
|
248
|
+
if (left >= CURVE_N || key === 0n) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`BIP-32 derivation at index ${index} produced an invalid key. Use the next index.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return { privateKey: to32Bytes(key), chainCode: material.slice(32) };
|
|
254
|
+
};
|
|
255
|
+
var masterFrom = (seed) => {
|
|
256
|
+
if (seed.length < 16 || seed.length > 64) {
|
|
257
|
+
throw new RangeError(
|
|
258
|
+
`A BIP-32 seed must be 16 to 64 bytes, not ${seed.length}.`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const material = hmac(sha512, MASTER_KEY_DOMAIN, seed);
|
|
262
|
+
const key = numberOf(material.subarray(0, 32));
|
|
263
|
+
if (key === 0n || key >= CURVE_N) {
|
|
264
|
+
throw new Error("This seed does not produce a valid BIP-32 master key.");
|
|
265
|
+
}
|
|
266
|
+
return { privateKey: material.slice(0, 32), chainCode: material.slice(32) };
|
|
267
|
+
};
|
|
268
|
+
var deriveCashRoot = (seed) => deriveCashChild(masterFrom(seed), CASH_PURPOSE + HARDENED);
|
|
269
|
+
var cashDomainIndices = (root, host) => {
|
|
270
|
+
const hashingKey = deriveCashChild(root, 0).privateKey;
|
|
271
|
+
const material = hmac(sha256, hashingKey, utf8ToBytes(host));
|
|
272
|
+
return [0, 4, 8, 12].map((offset) => readUint32BE(material, offset));
|
|
273
|
+
};
|
|
274
|
+
var deriveCashDomainNode = (root, host) => cashDomainIndices(root, host).reduce(deriveCashChild, root);
|
|
275
|
+
var requireIndex2 = (index) => {
|
|
276
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= HARDENED) {
|
|
277
|
+
throw new RangeError(
|
|
278
|
+
`A note index must be an integer in [0, 2^31), not ${index}.`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return index;
|
|
282
|
+
};
|
|
283
|
+
var cashSecretAt = (domainNode, index) => bytesToHex(
|
|
284
|
+
deriveCashChild(domainNode, requireIndex2(index) + HARDENED).privateKey
|
|
285
|
+
);
|
|
286
|
+
var deriveCashSecret = (root, host, index) => cashSecretAt(deriveCashDomainNode(root, host), index);
|
|
287
|
+
var cashNodeToHex = (node) => bytesToHex(node.privateKey) + bytesToHex(node.chainCode);
|
|
288
|
+
var cashNodeFromHex = (hex) => {
|
|
289
|
+
const bytes = hexToBytes(hex.trim().toLowerCase());
|
|
290
|
+
if (bytes.length !== 64) {
|
|
291
|
+
throw new RangeError(
|
|
292
|
+
`A cash node is 64 bytes - a 32-byte key and a 32-byte chain code - not ${bytes.length}.`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return { privateKey: bytes.slice(0, 32), chainCode: bytes.slice(32) };
|
|
296
|
+
};
|
|
297
|
+
var cashSecretSource = (root, host, start = 0) => {
|
|
298
|
+
const domainNode = deriveCashDomainNode(root, host);
|
|
299
|
+
let next = requireIndex2(start);
|
|
300
|
+
const source = (() => cashSecretAt(domainNode, next++));
|
|
301
|
+
source.index = () => next;
|
|
302
|
+
return source;
|
|
303
|
+
};
|
|
221
304
|
|
|
222
305
|
// src/errors.ts
|
|
223
306
|
var LnurlcashError = class extends Error {
|
|
@@ -1305,12 +1388,67 @@ var settleNoteForValue = async (noteUrl, { mints, minMsat, requireSignature: req
|
|
|
1305
1388
|
};
|
|
1306
1389
|
|
|
1307
1390
|
// src/restore.ts
|
|
1308
|
-
var restoreNotes = async (baseUrl, root, host, {
|
|
1391
|
+
var restoreNotes = async (baseUrl, root, host, {
|
|
1392
|
+
gap = 20,
|
|
1393
|
+
start = 0,
|
|
1394
|
+
probeK1,
|
|
1395
|
+
allowSecretDisclosure = false
|
|
1396
|
+
} = {}, options = {}) => {
|
|
1397
|
+
const walked = await restoreSchemes(
|
|
1398
|
+
baseUrl,
|
|
1399
|
+
[
|
|
1400
|
+
{
|
|
1401
|
+
scheme: "hmac",
|
|
1402
|
+
start,
|
|
1403
|
+
secretAt: (index) => deriveNoteSecret(root, host, index)
|
|
1404
|
+
}
|
|
1405
|
+
],
|
|
1406
|
+
{ gap, probeK1, allowSecretDisclosure },
|
|
1407
|
+
options
|
|
1408
|
+
);
|
|
1409
|
+
return { ...walked, next: walked.next.hmac };
|
|
1410
|
+
};
|
|
1411
|
+
var restoreFromSeed = async (baseUrl, seed, host, {
|
|
1412
|
+
gap = 20,
|
|
1413
|
+
start = {},
|
|
1414
|
+
probeK1,
|
|
1415
|
+
allowSecretDisclosure = false
|
|
1416
|
+
} = {}, options = {}) => {
|
|
1417
|
+
const domainNode = deriveCashDomainNode(deriveCashRoot(seed), host);
|
|
1418
|
+
const legacyRoot = deriveNoteRoot(seed);
|
|
1419
|
+
const walked = await restoreSchemes(
|
|
1420
|
+
baseUrl,
|
|
1421
|
+
[
|
|
1422
|
+
{
|
|
1423
|
+
scheme: "bip32",
|
|
1424
|
+
start: start.bip32 ?? 0,
|
|
1425
|
+
secretAt: (index) => cashSecretAt(domainNode, index)
|
|
1426
|
+
},
|
|
1427
|
+
{
|
|
1428
|
+
scheme: "hmac",
|
|
1429
|
+
start: start.hmac ?? 0,
|
|
1430
|
+
secretAt: (index) => deriveNoteSecret(legacyRoot, host, index)
|
|
1431
|
+
}
|
|
1432
|
+
],
|
|
1433
|
+
{ gap, probeK1, allowSecretDisclosure },
|
|
1434
|
+
options
|
|
1435
|
+
);
|
|
1436
|
+
return { ...walked, next: { bip32: walked.next.bip32, hmac: walked.next.hmac } };
|
|
1437
|
+
};
|
|
1438
|
+
var restoreSchemes = async (baseUrl, schemes, {
|
|
1439
|
+
gap,
|
|
1440
|
+
probeK1,
|
|
1441
|
+
allowSecretDisclosure
|
|
1442
|
+
}, options) => {
|
|
1309
1443
|
if (!Number.isSafeInteger(gap) || gap < 1) {
|
|
1310
1444
|
throw new RangeError(`The gap limit must be a positive integer, not ${gap}.`);
|
|
1311
1445
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1446
|
+
for (const { scheme, start } of schemes) {
|
|
1447
|
+
if (!Number.isSafeInteger(start) || start < 0) {
|
|
1448
|
+
throw new RangeError(
|
|
1449
|
+
`The start index for the ${scheme} scheme must be a non-negative integer, not ${start}.`
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1314
1452
|
}
|
|
1315
1453
|
let hashLookupsConfirmed = false;
|
|
1316
1454
|
if (probeK1) {
|
|
@@ -1321,66 +1459,75 @@ var restoreNotes = async (baseUrl, root, host, { gap = 20, start = 0, probeK1, a
|
|
|
1321
1459
|
if (!(err instanceof ServiceRejectedError)) throw err;
|
|
1322
1460
|
}
|
|
1323
1461
|
}
|
|
1324
|
-
const byHash =
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1462
|
+
const byHash = [];
|
|
1463
|
+
for (const scheme of schemes) {
|
|
1464
|
+
const outcome = await walk(
|
|
1465
|
+
scheme,
|
|
1466
|
+
gap,
|
|
1467
|
+
async (k1) => {
|
|
1468
|
+
const info = await fetchNoteInfoByHash(baseUrl, hashK1(k1), options);
|
|
1469
|
+
hashLookupsConfirmed = true;
|
|
1470
|
+
return info;
|
|
1471
|
+
}
|
|
1472
|
+
);
|
|
1473
|
+
if (outcome.found.length > 0 || outcome.unresolved.length > 0) {
|
|
1329
1474
|
hashLookupsConfirmed = true;
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
root,
|
|
1333
|
-
host
|
|
1334
|
-
);
|
|
1335
|
-
if (byHash.found.length > 0 || byHash.unresolved.length > 0) hashLookupsConfirmed = true;
|
|
1336
|
-
if (hashLookupsConfirmed) {
|
|
1337
|
-
return {
|
|
1338
|
-
found: byHash.found,
|
|
1339
|
-
unresolved: byHash.unresolved,
|
|
1340
|
-
next: byHash.lastUsed === null ? start : byHash.lastUsed + 1,
|
|
1341
|
-
hashLookupsConfirmed: true,
|
|
1342
|
-
disclosesSecrets: false
|
|
1343
|
-
};
|
|
1475
|
+
}
|
|
1476
|
+
byHash.push(outcome);
|
|
1344
1477
|
}
|
|
1478
|
+
if (hashLookupsConfirmed) return collate(schemes, byHash, false);
|
|
1345
1479
|
if (!allowSecretDisclosure) {
|
|
1346
1480
|
throw new HashLookupUnsupportedError(
|
|
1347
1481
|
"This service never answered a lookup by hash, so a restore cannot tell an empty wallet from a service that only accepts raw secrets. Pass a probeK1 for a note known to exist here, or allowSecretDisclosure to walk by secret instead."
|
|
1348
1482
|
);
|
|
1349
1483
|
}
|
|
1350
|
-
const bySecret =
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1484
|
+
const bySecret = [];
|
|
1485
|
+
for (const scheme of schemes) {
|
|
1486
|
+
bySecret.push(
|
|
1487
|
+
await walk(
|
|
1488
|
+
scheme,
|
|
1489
|
+
gap,
|
|
1490
|
+
(k1) => fetchNoteInfo(buildNoteUrl(baseUrl, k1), options)
|
|
1491
|
+
)
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
return collate(schemes, bySecret, true);
|
|
1495
|
+
};
|
|
1496
|
+
var collate = (schemes, outcomes, disclosesSecrets) => {
|
|
1497
|
+
const next = {};
|
|
1498
|
+
schemes.forEach(({ scheme, start }, at) => {
|
|
1499
|
+
const outcome = outcomes[at];
|
|
1500
|
+
if (!disclosesSecrets) {
|
|
1501
|
+
next[scheme] = outcome.lastUsed === null ? start : outcome.lastUsed + 1;
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
const used = outcome.lastUsed ?? start - 1;
|
|
1505
|
+
const walkedThrough = outcome.highestWalked ?? start - 1;
|
|
1506
|
+
next[scheme] = Math.max(used, walkedThrough) + 1;
|
|
1507
|
+
});
|
|
1359
1508
|
return {
|
|
1360
|
-
found:
|
|
1361
|
-
unresolved:
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
next: Math.max(used, walkedThrough) + 1,
|
|
1366
|
-
hashLookupsConfirmed: false,
|
|
1367
|
-
disclosesSecrets: true
|
|
1509
|
+
found: outcomes.flatMap((outcome) => outcome.found),
|
|
1510
|
+
unresolved: outcomes.flatMap((outcome) => outcome.unresolved),
|
|
1511
|
+
next,
|
|
1512
|
+
hashLookupsConfirmed: !disclosesSecrets,
|
|
1513
|
+
disclosesSecrets
|
|
1368
1514
|
};
|
|
1369
1515
|
};
|
|
1370
|
-
var walk = async (
|
|
1516
|
+
var walk = async ({ scheme, start, secretAt }, gap, lookup) => {
|
|
1371
1517
|
const found = [];
|
|
1372
1518
|
const unresolved = [];
|
|
1373
1519
|
let lastUsed = null;
|
|
1374
1520
|
let highestWalked = null;
|
|
1375
1521
|
let unknownRun = 0;
|
|
1376
1522
|
for (let index = start; unknownRun < gap; index++) {
|
|
1377
|
-
const k1 =
|
|
1523
|
+
const k1 = secretAt(index);
|
|
1378
1524
|
highestWalked = index;
|
|
1379
1525
|
try {
|
|
1380
1526
|
const info = await lookup(k1);
|
|
1381
1527
|
found.push({
|
|
1382
1528
|
index,
|
|
1383
1529
|
k1,
|
|
1530
|
+
scheme,
|
|
1384
1531
|
amountMsat: info.maxWithdrawable,
|
|
1385
1532
|
state: "live",
|
|
1386
1533
|
callback: info.callback
|
|
@@ -1389,7 +1536,7 @@ var walk = async (start, gap, lookup, root, host) => {
|
|
|
1389
1536
|
unknownRun = 0;
|
|
1390
1537
|
} catch (err) {
|
|
1391
1538
|
if (err instanceof PendingNoteError) {
|
|
1392
|
-
found.push({ index, k1, amountMsat: null, state: "pending" });
|
|
1539
|
+
found.push({ index, k1, scheme, amountMsat: null, state: "pending" });
|
|
1393
1540
|
lastUsed = index;
|
|
1394
1541
|
unknownRun = 0;
|
|
1395
1542
|
} else if (err instanceof NoteSpentError) {
|
|
@@ -1398,7 +1545,7 @@ var walk = async (start, gap, lookup, root, host) => {
|
|
|
1398
1545
|
} else if (err instanceof NoteUnknownError) {
|
|
1399
1546
|
unknownRun++;
|
|
1400
1547
|
} else if (err instanceof ServiceRejectedError) {
|
|
1401
|
-
unresolved.push({ index, k1, reason: err.reason });
|
|
1548
|
+
unresolved.push({ index, k1, scheme, reason: err.reason });
|
|
1402
1549
|
lastUsed = index;
|
|
1403
1550
|
unknownRun = 0;
|
|
1404
1551
|
} else {
|
|
@@ -1431,7 +1578,8 @@ var createClient = (options = {}) => ({
|
|
|
1431
1578
|
fetchInvoiceVerification: (verifyUrl) => fetchInvoiceVerification(verifyUrl, options),
|
|
1432
1579
|
claimMintedNote: (withdrawLink, k1) => claimMintedNote(withdrawLink, k1, options),
|
|
1433
1580
|
restoreNotes: (baseUrl, root, host, restoreOptions = {}) => restoreNotes(baseUrl, root, host, restoreOptions, options),
|
|
1581
|
+
restoreFromSeed: (baseUrl, seed, host, restoreOptions = {}) => restoreFromSeed(baseUrl, seed, host, restoreOptions, options),
|
|
1434
1582
|
settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
|
|
1435
1583
|
});
|
|
1436
1584
|
|
|
1437
|
-
export { AmbiguousMintError, AmbiguousMutationError, HashLookupUnsupportedError, InsufficientValueError, LnurlcashError, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, PendingNoteError, ProtocolError, RequestRefusedError, ServiceRejectedError, UnverifiableNoteError, applyMintFee, buildNoteInfoUrlByHash, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
|
1585
|
+
export { AmbiguousMintError, AmbiguousMutationError, HashLookupUnsupportedError, InsufficientValueError, LnurlcashError, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, PendingNoteError, ProtocolError, RequestRefusedError, ServiceRejectedError, UnverifiableNoteError, applyMintFee, buildNoteInfoUrlByHash, buildNoteUrl, cashDomainIndices, cashNodeFromHex, cashNodeToHex, cashSecretAt, cashSecretSource, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveCashChild, deriveCashDomainNode, deriveCashRoot, deriveCashSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreFromSeed, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
package/llms.txt
CHANGED
|
@@ -35,10 +35,31 @@ fetchMintAddress(url, opts?) -> {mintPubkey?, payLink, name?, motd?, contact?,
|
|
|
35
35
|
mintPubkey verifies note signatures. It is NOT the node key in nodeUri.
|
|
36
36
|
nodePubkey is a deprecated alias for mintPubkey, dropped next breaking change.
|
|
37
37
|
mintToHash is the fallback copy of the payRequest's flag - see 13.
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
deriveCashRoot(seedBytes) -> CashNode LUD-25's scheme: BIP-32 m/139'
|
|
39
|
+
deriveCashDomainNode(root, host) -> CashNode m/139'/d1/d2/d3/d4, d1..d4 =
|
|
40
|
+
first 16 bytes of HMAC-SHA256(privkey at m/139'/0, host) as 4 raw uint32.
|
|
41
|
+
RAW: BIP-32 reads >= 2^31 as hardened, so ~half the levels are, by the
|
|
42
|
+
host name alone. Never mask the top bit, never harden all four - either
|
|
43
|
+
derives a different tree and restores nothing, silently.
|
|
44
|
+
deriveCashSecret(root, host, index) -> k1 the hardened child i' of that node
|
|
45
|
+
cashSecretAt(domainNode, index) -> k1 no EC needed below the domain node,
|
|
46
|
+
which is why a hardware signer is provisioned with the node, not the seed
|
|
47
|
+
cashSecretSource(root, host, start) -> RandomSecret & {index()}
|
|
48
|
+
cashNodeToHex/cashNodeFromHex privateKey || chainCode, 64 bytes
|
|
49
|
+
deriveCashChild(node, rawUint32) -> CashNode BIP-32 CKDpriv, for checking
|
|
50
|
+
this library against BIP-32's own vectors
|
|
51
|
+
deriveNoteRoot(seedBytes) -> Uint8Array LEGACY, pre-spec. Do not mint under
|
|
52
|
+
deriveNoteSecret(root, host, index) -> k1 it; still scanned so old notes live.
|
|
40
53
|
derivedSecretSource(root, host, start) -> RandomSecret & {index()}
|
|
54
|
+
restoreFromSeed(baseUrl, seed, host, {gap?, start?, probeK1?,
|
|
55
|
+
allowSecretDisclosure?}, opts?) -> {found[] (.scheme), next: {bip32, hmac}}
|
|
56
|
+
walks BOTH schemes. A by-hash walk CANNOT see a spent index (LUD-25 makes
|
|
57
|
+
spent and never-issued the same answer), and a rotate burns the old index,
|
|
58
|
+
so a wallet that rotated more than `gap` times scans as empty. The
|
|
59
|
+
persisted per-host counter is the real backup; the scan is the fallback.
|
|
60
|
+
The counter is not secret - back it up, and merge it upwards only.
|
|
41
61
|
restoreNotes(baseUrl, root, host, {gap?, start?}, opts?) -> {found[], next}
|
|
62
|
+
legacy scheme only
|
|
42
63
|
fetchPayRequest(url, opts?) -> {callback, minSendable, maxSendable, metadata,
|
|
43
64
|
withdrawLink?, mintFee?, mintToHash?} mintToHash: mint accepts an `h`
|
|
44
65
|
fetchInvoiceVerification(url, opts?)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lnurlcash-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "LNURLcash (LUD-25) bearer note client for TypeScript - mint, rotate, split, merge, melt, and verify offline",
|
|
5
5
|
"author": "TheCryptoDonkey",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^26.2.0",
|
|
63
|
-
"lnurlcash-conformance": "^0.
|
|
63
|
+
"lnurlcash-conformance": "^0.7.0",
|
|
64
64
|
"tsup": "^8.5.0",
|
|
65
65
|
"typescript": "^5.7.0",
|
|
66
66
|
"vitest": "^3.0.0"
|