lnurlcash-kit 0.1.1 → 0.2.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 +345 -0
- package/README.md +318 -0
- package/dist/index.d.ts +174 -68
- package/dist/index.js +515 -122
- package/llms.txt +67 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,351 @@
|
|
|
3
3
|
Semantic versioning. While the LUD-25 draft is unmerged, `0.x` minor bumps
|
|
4
4
|
may carry breaking changes; pin an exact version.
|
|
5
5
|
|
|
6
|
+
## 0.2.0 - 2026-08-22
|
|
7
|
+
|
|
8
|
+
### Deterministic note secrets and restore from a seed
|
|
9
|
+
|
|
10
|
+
- `deriveNoteRoot(seed)`, `deriveNoteSecret(root, host, index)` and
|
|
11
|
+
`derivedSecretSource(root, host, start)` in `secrets.ts`, plus
|
|
12
|
+
`restoreNotes(baseUrl, root, host, {gap, start}, opts)` in a new
|
|
13
|
+
`restore.ts`. All additive; nothing existing changes shape.
|
|
14
|
+
- The scheme, in full, so this entry alone is enough to reimplement it:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
root = HMAC-SHA256(key = utf8("lnurlcash-note-v1"), msg = seed)
|
|
18
|
+
k1_i = HMAC-SHA256(key = root, msg = utf8(host + ":" + index))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`seed` is raw bytes of any length. A 64-byte BIP39 seed (12 words,
|
|
22
|
+
English wordlist, no passphrase) is what wallets use in practice, but the
|
|
23
|
+
kit is seed-format agnostic and depends on no wordlist. `host` is the
|
|
24
|
+
mint host exactly as `serverOf` produces it: lowercase, port included
|
|
25
|
+
where there is one, so `127.0.0.1:8899` and `mint.example` derive
|
|
26
|
+
different secrets. `index` is decimal ASCII counting from 0, and the
|
|
27
|
+
separator is a single colon. The HMAC output is 32 bytes, rendered
|
|
28
|
+
lowercase hex, which is the size of a payment preimage and therefore
|
|
29
|
+
indistinguishable from a randomly drawn `k1` on the wire. `hashK1`
|
|
30
|
+
applies unchanged, so the mint only ever receives `sha256(k1)` and sees
|
|
31
|
+
nothing different from before.
|
|
32
|
+
|
|
33
|
+
Worked example. The BIP39 mnemonic `abandon abandon abandon abandon
|
|
34
|
+
abandon abandon abandon abandon abandon abandon abandon about` with an
|
|
35
|
+
empty passphrase gives the seed
|
|
36
|
+
`5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4`.
|
|
37
|
+
Its root is
|
|
38
|
+
`948f8f49347549cf2726e8b53f673a4185379344d2d7ba8877d3ded45d34d127`, and
|
|
39
|
+
index 0 at host `mint.example` is
|
|
40
|
+
`1f6016c80339b45dfdd1b3877c1a97d74b063cad54c4ccb866be39ed25ee2ab0`.
|
|
41
|
+
- Why it is in the kit rather than in a wallet: because the derivation is
|
|
42
|
+
written down once, the same words restore the same notes in a different
|
|
43
|
+
wallet, and the Kotlin, Python and Go ports agree with this one. That
|
|
44
|
+
cross-wallet portability is the point, and it is the reason the scheme
|
|
45
|
+
ships with a conformance vector.
|
|
46
|
+
- Counters are the wallet's, one per mint host. `derivedSecretSource` is a
|
|
47
|
+
`RandomSecret`, so it drops straight into
|
|
48
|
+
`LnurlcashOptions.randomSecret` and rotate, split and merge draw derived
|
|
49
|
+
secrets without knowing anything about derivation; `source.index()` reads
|
|
50
|
+
back the next unused index afterwards. A rotate consumes one index, a
|
|
51
|
+
split consumes two. Minting can draw from the same source where the mint
|
|
52
|
+
advertises `mintToHash` - see "Name the note you are buying" below - and
|
|
53
|
+
the note is then derived from birth rather than from its first rotate. Persist that counter in the SAME write that stages
|
|
54
|
+
the new records, and do it BEFORE the hash goes on the wire: a crash
|
|
55
|
+
between the bump and the request wastes an index, which costs nothing,
|
|
56
|
+
while a crash the other way round re-derives a secret the mint has
|
|
57
|
+
already seen and the second note minted at it collides with the first.
|
|
58
|
+
- `restoreNotes` walks indices from `start`, asking the mint what each
|
|
59
|
+
derived secret is worth. A live note is recorded; a note the mint reports
|
|
60
|
+
as spent still counts the index as used, since re-deriving it would mint
|
|
61
|
+
a duplicate id; a note the mint reports as pending is recorded with a
|
|
62
|
+
null amount for the caller to reconcile later; an unknown note counts
|
|
63
|
+
towards the gap. The walk stops after `gap` consecutive unknowns,
|
|
64
|
+
defaulting to 20, and `next` is one past the highest index the mint
|
|
65
|
+
recognised. It reads only, so an interrupted restore has changed nothing.
|
|
66
|
+
Any other failure is thrown rather than swallowed, because a short walk
|
|
67
|
+
reported as a finished one would leave the wallet re-deriving live
|
|
68
|
+
secrets.
|
|
69
|
+
- A restored note carries no signature and its `k1` has just been on the
|
|
70
|
+
wire, so rotate each one straight after restoring. That closes the
|
|
71
|
+
exposure and gets the signature in the same call.
|
|
72
|
+
- The seed is bearer material for every note the wallet will ever hold.
|
|
73
|
+
Store it the way the notes are stored, and never log it.
|
|
74
|
+
- `classifyNoteError` now returns `PendingNoteError` for the exact reason
|
|
75
|
+
string `pending`, which LUD-25 fixes verbatim. Previously only the
|
|
76
|
+
mutating callback classified it, so an informational GET on a note with a
|
|
77
|
+
melt in flight raised a bare `ServiceRejectedError` that callers had to
|
|
78
|
+
re-parse. `PendingNoteError` extends `ServiceRejectedError`, so anything
|
|
79
|
+
catching the parent is unaffected.
|
|
80
|
+
|
|
81
|
+
### Name the note you are buying
|
|
82
|
+
|
|
83
|
+
- `requestInvoice(payCallback, amountMsat, {h, ...opts})` takes an optional
|
|
84
|
+
`h`: the sha256 of a secret the wallet chose, sent on the LUD-06 pay
|
|
85
|
+
callback exactly as `h` is sent on the withdraw callback. A mint that
|
|
86
|
+
accepts it credits the minted note at that hash on settlement, so the
|
|
87
|
+
wallet names the note it is buying instead of being handed one. The
|
|
88
|
+
options argument is the same object as before with one more optional
|
|
89
|
+
field on it, so every existing call is unaffected.
|
|
90
|
+
- Why it matters. Without `h` the payment preimage IS the money, and two
|
|
91
|
+
sets of people learn it without being trusted: every routing node on the
|
|
92
|
+
payment path, because that is how HTLC settlement works, and anyone who
|
|
93
|
+
merely saw the invoice, because they can poll LUD-21 `verify` with the
|
|
94
|
+
payment hash that travels inside it and take the preimage the moment it
|
|
95
|
+
settles. A QR on a desktop screen is exactly that. "Rotate immediately"
|
|
96
|
+
is a race against a thief in a tight polling loop with a warm connection.
|
|
97
|
+
Choosing the secret yourself is not a race at all.
|
|
98
|
+
- The capability appears in three places, and they say different things.
|
|
99
|
+
`PayRequestInfo.mintToHash` is "I accept an `h`", and is the one to
|
|
100
|
+
decide from: the payRequest is the only endpoint every mint has, it is
|
|
101
|
+
where a wallet already is at the moment it is about to mint, and it sits
|
|
102
|
+
alongside the `withdrawLink` the draft already hangs there for
|
|
103
|
+
LNURLcash's sake. `MintAddressInfo.mintToHash` is the same statement on
|
|
104
|
+
the experimental discovery document, kept for consistency with the other
|
|
105
|
+
capability fields there and the fallback for a mint that only says it
|
|
106
|
+
there. `InvoiceResult.mintToHash` is "I bound THIS quote to the hash you
|
|
107
|
+
named": per quote, and the one that matters at the moment money moves.
|
|
108
|
+
- So the order is: read the payRequest, fall back to the mint address, and
|
|
109
|
+
a mint that advertises in neither place ignores the `h`. Undefined means
|
|
110
|
+
the mint said nothing, which a wallet reads as no. Anything that is not
|
|
111
|
+
exactly the boolean `true` is no, everywhere, which matters on the
|
|
112
|
+
payRequest because that response is spread through and a truthy string
|
|
113
|
+
would otherwise land on the typed field.
|
|
114
|
+
- `InvoiceResult.mintToHash` being `false` is not a refusal: it means the
|
|
115
|
+
quote said nothing about `h`, and a mint may accept the parameter without
|
|
116
|
+
echoing it back. Decide from the advertisement, claim by probing.
|
|
117
|
+
- A malformed `h` is refused with `RequestRefusedError` before anything is
|
|
118
|
+
sent, so a wallet never pays for a quote the mint was always going to
|
|
119
|
+
reject. The hash is normalised to lowercase on the way out.
|
|
120
|
+
- Persist the secret BEFORE calling `requestInvoice`. Paying for a note and
|
|
121
|
+
then losing the secret is the one way this is worse than the preimage
|
|
122
|
+
scheme, and persisting first removes it.
|
|
123
|
+
- `claimMintedNote(withdrawLink, k1, opts)` is the claim half, returning
|
|
124
|
+
`{state, k1, amountMsat, callback}` with `state` one of `'minted'`,
|
|
125
|
+
`'unminted'`, `'pending'` or `'spent'`. A wallet that chose its own
|
|
126
|
+
secret has nothing to fetch, so it asks the mint what the note at that
|
|
127
|
+
secret is worth and a live answer is the claim. Poll it while the invoice
|
|
128
|
+
is unpaid; it reads only, so an `'unminted'` answer has changed nothing.
|
|
129
|
+
A mint that cannot be reached throws rather than reporting `'unminted'`,
|
|
130
|
+
which a caller would fairly read as "not yet" and give up on.
|
|
131
|
+
- No rotate follows a bound claim, and that is the point. The preimage
|
|
132
|
+
scheme needs one because the mint generated the secret and `verify` hands
|
|
133
|
+
it to anyone who saw the invoice. Here the mint never had it and no third
|
|
134
|
+
party can learn it, so the note belongs to the wallet from the moment it
|
|
135
|
+
exists. The claim GET does disclose the secret to the mint it is a claim
|
|
136
|
+
on, which is not the same exposure, and a wallet that wants an offline
|
|
137
|
+
signature on the note can still rotate to get one.
|
|
138
|
+
- **This changes what the derivation section above says about minting.**
|
|
139
|
+
Until now a freshly minted note was never derived: its secret was the
|
|
140
|
+
mint's preimage, which nothing in a seed produces, so the note existed
|
|
141
|
+
outside the wallet's derivation until the immediate rotate moved it back
|
|
142
|
+
in. Draw the secret from `deriveNoteSecret` at the next index, send its
|
|
143
|
+
hash as `h`, and the minted note is seed-derived from birth. `restoreNotes`
|
|
144
|
+
finds it with no rotate having happened at all, which is what closes the
|
|
145
|
+
window where a wallet that crashed between paying and rotating could not
|
|
146
|
+
recover the note from its words. The counter rule is unchanged and applies
|
|
147
|
+
to the mint too: persist the bumped index in the same write that stages the
|
|
148
|
+
pending mint, before the hash goes on the wire.
|
|
149
|
+
- Purely additive on the wire. A mint that does not offer `mintToHash`
|
|
150
|
+
ignores the parameter, keys the note by the preimage as it always has,
|
|
151
|
+
and the LUD-21 verify path is unchanged and still the way in.
|
|
152
|
+
- `createClient(opts).requestInvoice(payCallback, amountMsat, h?)` takes the
|
|
153
|
+
hash as a third argument, and `claimMintedNote` is bound alongside it.
|
|
154
|
+
|
|
155
|
+
### Mint info, and verifying against a key history
|
|
156
|
+
|
|
157
|
+
- `MintAddressInfo` gains the operator fields a mint may publish on the
|
|
158
|
+
experimental discovery endpoint: `name`, `description`, `contact`
|
|
159
|
+
(`{nostr?, email?, url?}`), `tosUrl`, `motd`, `fees` (`{baseFeeMsat,
|
|
160
|
+
feePpm}`, the same shape `parseMintFee` returns, so it feeds
|
|
161
|
+
`applyMintFee` and `mintFeeBand` directly), `version` and
|
|
162
|
+
`previousPubkeys`. All optional, all absent on most mints, and none of
|
|
163
|
+
them is needed to spend a note.
|
|
164
|
+
- `fetchMintAddress` now maps the response field by field instead of
|
|
165
|
+
spreading it through. The spread is what hid `nodeCapacity` under its
|
|
166
|
+
wire name until 0.1.1, and it also put whatever a mint decided to send on
|
|
167
|
+
a typed object with no type behind it. An unrecognised wire field is now
|
|
168
|
+
dropped rather than carried, so a caller reading one off the object with
|
|
169
|
+
a cast will find it undefined; the version of this library that
|
|
170
|
+
understands that field will map it deliberately.
|
|
171
|
+
- `nodeCapacityMsat` is now populated from either spelling. The bare
|
|
172
|
+
`nodeCapacity` is what the reference mint, the mock and everything that
|
|
173
|
+
copied them emit, and it wins where a mint sends both; one live mint
|
|
174
|
+
emits `nodeCapacityMsat` instead, which previously survived only by
|
|
175
|
+
riding the spread.
|
|
176
|
+
- `verifyNoteSignature(k1, amountMsat, sig, keys)` accepts a single pubkey
|
|
177
|
+
or an array, and is true if any of them signed. New
|
|
178
|
+
`verifyNoteSignatureAgainst(...)` returns `{valid, pubkey}` so a caller
|
|
179
|
+
learns WHICH key signed. An empty array is a rejection, never a pass.
|
|
180
|
+
- Why: a mint that rotates its signing key would otherwise invalidate every
|
|
181
|
+
outstanding signature at once, and a wallet holding only the new key would
|
|
182
|
+
read every note it already had as forged. The mint publishes its retired
|
|
183
|
+
keys as `previousPubkeys`, the wallet verifies against the current key and
|
|
184
|
+
that history together, and a note that verifies only against a retired key
|
|
185
|
+
is one to rotate so the mint re-signs it. Only one recovery is performed
|
|
186
|
+
per signature layout, so a long key history costs a string comparison
|
|
187
|
+
each, not a recovery each.
|
|
188
|
+
|
|
189
|
+
### Accepting a note as payment
|
|
190
|
+
|
|
191
|
+
- `settleNoteForValue(noteUrl, {mints, minMsat, requireSignature}, opts)`
|
|
192
|
+
in a new `settle.ts`, returning `{note, newUrl}`. It is the decision
|
|
193
|
+
sequence every server accepting a bearer note performs, written once:
|
|
194
|
+
parse the input; check the note's mint is one the server accepts, before
|
|
195
|
+
any round trip, so an unaccepted mint is never contacted; fetch the
|
|
196
|
+
authoritative value and the mint's signing key; verify the signature over
|
|
197
|
+
that value where the server demands one; compare against the price;
|
|
198
|
+
rotate.
|
|
199
|
+
- The rotate is the settlement, not bookkeeping after it. It burns the
|
|
200
|
+
secret the payer handed over and mints a replacement only the server
|
|
201
|
+
knows, in one atomic request at the mint, so it transfers ownership and
|
|
202
|
+
rejects a replay in the same call: a second presentation of the same note
|
|
203
|
+
finds it spent. A server that checks a note's value and grants access
|
|
204
|
+
without rotating has verified a photograph of a banknote.
|
|
205
|
+
- New `InsufficientValueError`, extending `ServiceRejectedError` and
|
|
206
|
+
carrying `amountMsat` and `minMsat`, so a server can say how short a note
|
|
207
|
+
was rather than "declined". An unaccepted mint or a signature that will
|
|
208
|
+
not verify raises `ServiceRejectedError`; a spent note passes
|
|
209
|
+
`NoteSpentError` through; a note with a melt in flight raises
|
|
210
|
+
`PendingNoteError`, which is worth retrying rather than refusing. Nothing
|
|
211
|
+
is burned by any refusal, so a rejected note is still the payer's, intact.
|
|
212
|
+
- `AmbiguousMutationError` from the rotate reaches the caller unchanged,
|
|
213
|
+
carrying the fresh secret. If that request landed, the secret is the money
|
|
214
|
+
and it belongs to the server: persist it before anything else.
|
|
215
|
+
- An empty `mints` list accepts nothing. A note is a claim on one specific
|
|
216
|
+
operator, and "any mint" is not a policy a server should be able to hold
|
|
217
|
+
by accident.
|
|
218
|
+
- The value compared against the price is always the one the mint states.
|
|
219
|
+
A note URL's own `amount` is a claim by whoever encoded it, and a
|
|
220
|
+
signature, where one is required, is checked over the mint's figure, so an
|
|
221
|
+
inflated URL fails rather than passing on a signature issued for the true
|
|
222
|
+
amount.
|
|
223
|
+
|
|
224
|
+
### A retried mutation no longer loses the secret it minted
|
|
225
|
+
|
|
226
|
+
- Every mutation is a GET, and HTTP stacks retry a GET whose connection
|
|
227
|
+
dropped: browsers on a stale keep-alive connection, Go's `net/http` on a
|
|
228
|
+
reused idle one, the JDK's `HttpClient` on any idempotent method with no
|
|
229
|
+
switch to stop it. The retry is byte-identical, so the mint sees the same
|
|
230
|
+
request twice and answers the second with its ordinary refusal for a
|
|
231
|
+
burned input, its inputs having been burned by the first. The caller was
|
|
232
|
+
told the mutation never happened while a note sat at the hash it had
|
|
233
|
+
disclosed, and the only copy of that secret went out of scope with the
|
|
234
|
+
call. The money was not stolen, it was made unspendable by anyone at all.
|
|
235
|
+
- `rotateNote`, `splitNote` and `mergeNotes` now attach the secrets they
|
|
236
|
+
generated to a `NoteSpentError` or a `NoteUnknownError`, the way
|
|
237
|
+
`AmbiguousMutationError` already carried them. New `newSecretsOf(err)`
|
|
238
|
+
reads them off any error in one line, returning an empty array when there
|
|
239
|
+
are none, so a caller's catch block does not have to know which family it
|
|
240
|
+
is holding.
|
|
241
|
+
- Only those two classes carry anything. They are the refusals that mean
|
|
242
|
+
"this input is not spendable", which is exactly what a landed-then-retried
|
|
243
|
+
mutation looks like. `PendingNoteError` means the input is alive and
|
|
244
|
+
untouched, and a refusal on policy grounds (dust, a fee, a sunsetting
|
|
245
|
+
mint) burned nothing, so both carry nothing and a caller may discard its
|
|
246
|
+
staged records at once.
|
|
247
|
+
- The classification itself is unchanged, deliberately. At the wire a retry
|
|
248
|
+
and a genuine double spend are the same answer, and whether the input was
|
|
249
|
+
live when the request went out is knowledge the caller has and this
|
|
250
|
+
library does not. So it hands back the secret rather than a verdict:
|
|
251
|
+
persist it, then ask the mint what the note at that secret is worth. A
|
|
252
|
+
live note means the mutation landed.
|
|
253
|
+
|
|
254
|
+
### The mint's signing key is called mintPubkey
|
|
255
|
+
|
|
256
|
+
- `MintAddressInfo.mintPubkey` carries the wire value unchanged and is the
|
|
257
|
+
name to reach for. `nodePubkey` remains, populated with the same value,
|
|
258
|
+
and is deprecated: it will be removed at the next breaking change.
|
|
259
|
+
Nothing breaks in this release.
|
|
260
|
+
- The two keys in a discovery document are different keys. `mintPubkey` is
|
|
261
|
+
what a note's signature verifies against; the Lightning node's identity
|
|
262
|
+
key is embedded in `nodeUri`. Every other `node*` field on the type
|
|
263
|
+
really is about the node - alias, colour, capacity, channel and peer
|
|
264
|
+
counts - so the signing key was the one exception, and its name said
|
|
265
|
+
nothing about that. A reader who pulled the pubkey out of `nodeUri` and
|
|
266
|
+
tried to verify a note with it got a failure that explained nothing.
|
|
267
|
+
- It also makes the package internally consistent: the same key is already
|
|
268
|
+
called `mintPubkey` on a note's own info, so a reader moving between the
|
|
269
|
+
two objects met one key under two names.
|
|
270
|
+
|
|
271
|
+
### Payment requests
|
|
272
|
+
|
|
273
|
+
- `encodePaymentRequest(request)`, `decodePaymentRequest(string, {now})`,
|
|
274
|
+
`isPaymentRequest(string)` and `paymentRequestAmountMsat(request)` in a
|
|
275
|
+
new `request.ts`, with the `PaymentRequest` type and the
|
|
276
|
+
`PAYMENT_REQUEST_PREFIX` constant.
|
|
277
|
+
- A request names an amount, the mints the payee accepts and where to
|
|
278
|
+
deliver, so a payer's wallet can split a note and send it straight across
|
|
279
|
+
instead of doing a mint-and-zap round trip through the mint's node for
|
|
280
|
+
something neither party needed a node for:
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{"v": 1, "id": "0123456789abcdef", "amount": "500", "currency": "sat",
|
|
284
|
+
"methodDetails": {"mints": ["mint.example"]},
|
|
285
|
+
"to": "npub1...", "memo": "lunch", "expires": 1756000000}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`id` is 16 lowercase hex characters, `amount` is whole sats as a decimal
|
|
289
|
+
string with no leading zeros, `to` is a Nostr npub or a Lightning Address
|
|
290
|
+
and is absent on a charge request served over HTTP, and `expires` is unix
|
|
291
|
+
seconds. `methodDetails` also accepts an optional `mintPubkeys`.
|
|
292
|
+
- Encoded as `lnurlcashreq1` followed by base64url (unpadded) of the
|
|
293
|
+
request serialised as RFC 8785 JCS-canonical JSON: keys sorted by UTF-16
|
|
294
|
+
code unit at every level, no whitespace, integers only. Canonical because
|
|
295
|
+
a request is a thing people copy, quote back and match against a record of
|
|
296
|
+
what they asked for, so two encodings of the same request must be the same
|
|
297
|
+
string. This is NUT-18's `creqA` idiom with our own prefix, and it stays
|
|
298
|
+
short enough for a single static QR.
|
|
299
|
+
- The object is the same charge request an HTTP 402 lnurlcash rail serves,
|
|
300
|
+
plus the transport fields a wallet-to-wallet send needs, so one encoder
|
|
301
|
+
covers both.
|
|
302
|
+
- Validation is strict in both directions, an unrecognised field included:
|
|
303
|
+
quietly paying a request one did not fully understand is how a payer pays
|
|
304
|
+
the wrong person. Every refusal is a `ProtocolError`.
|
|
305
|
+
- `amount` is in sat, which is the one exception to this library's
|
|
306
|
+
msat-everywhere rule, because the field is shared with the 402 rail and
|
|
307
|
+
the Cashu payment method and both count in whole units.
|
|
308
|
+
`paymentRequestAmountMsat` converts exactly, so nothing has to multiply by
|
|
309
|
+
hand.
|
|
310
|
+
- An expired request does not decode, since paying one is always wrong. At
|
|
311
|
+
the expiry counts as expired, not merely past it, so a payer whose clock
|
|
312
|
+
is a second behind the payee's does not send a note against a request the
|
|
313
|
+
payee has already written off. `isPaymentRequest` still returns true for
|
|
314
|
+
it, so a scanner routes it to the pay screen and the holder is told it
|
|
315
|
+
lapsed rather than that their input was gibberish, and
|
|
316
|
+
`decodePaymentRequest(input, {now: 0})` returns it for display.
|
|
317
|
+
- `to` is checked rather than shape-matched: an npub must survive its bech32
|
|
318
|
+
checksum. A request naming a destination nobody can route to is a request
|
|
319
|
+
nobody can pay, and a mistyped npub passes any regex.
|
|
320
|
+
- **`lnurlcashreq1` means the schema above and nothing else.** An earlier
|
|
321
|
+
HTTP 402 rail emitted a shorter object under the same prefix -
|
|
322
|
+
`{"a": 21, "m": ["mint.example"], "u": "sat"}`, with the amount as a
|
|
323
|
+
number, no version and no id - and two schemas under one prefix cannot
|
|
324
|
+
both be right. This is the one the conformance vectors pin, so it is the
|
|
325
|
+
definition. The decoder reads the short form anyway, because returning
|
|
326
|
+
nothing for a string it can plainly understand helps no one, and gives it
|
|
327
|
+
a deterministic id derived from its own canonical bytes so the same
|
|
328
|
+
challenge always reads back as the same request. Nothing in this library
|
|
329
|
+
ever emits the short form.
|
|
330
|
+
|
|
331
|
+
## 0.1.2 - 2026-08-21
|
|
332
|
+
|
|
333
|
+
- `mintFeeBand` and `withinMintFeeBand`. LUD-25 states the mint fee as
|
|
334
|
+
`base_fee_msat` plus a ppm cut and says nothing about rounding, and the
|
|
335
|
+
two live implementations read that differently: dni's lnurl-mint - the
|
|
336
|
+
reference, and what every public mint on the awesome list except moneyer
|
|
337
|
+
runs - ceilings the fee to a whole sat on purpose so the mint is "never
|
|
338
|
+
short a sat", while moneyer withholds the msat-exact amount.
|
|
339
|
+
- So `applyMintFee` is right about exactly one of them, and a wallet
|
|
340
|
+
comparing a credited note against it warns spuriously against the other.
|
|
341
|
+
Measured on real sats: 40,000 msat at a 1000 + 1000 ppm mint credited
|
|
342
|
+
38,000, not the 38,960 the formula gives.
|
|
343
|
+
- `mintFeeBand` returns the range - the formula is the most a holder can be
|
|
344
|
+
credited, the sat-ceilinged fee the least - and `withinMintFeeBand` is
|
|
345
|
+
what a caller should compare against. `applyMintFee` is unchanged and
|
|
346
|
+
still means the formula; it is now documented as the generous edge rather
|
|
347
|
+
than the answer.
|
|
348
|
+
- Nothing here decides which reading is correct. That is a question for
|
|
349
|
+
lnurl/luds#301.
|
|
350
|
+
|
|
6
351
|
## 0.1.1 - 2026-08-20
|
|
7
352
|
|
|
8
353
|
- `fetchMintAddress` now populates `nodeCapacityMsat`. The wire field is
|