lnurlcash-kit 0.9.0 → 0.11.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 CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0 - 2026-09-11
4
+
5
+ **The wire calls take LUD-25 Part 2 notes.**
6
+
7
+ - A `ck1` goes anywhere a `k1` does. `resolveNoteInput` accepts a note URL
8
+ carrying one, and rotate, split, merge, melt and `fetchNoteInfo` pass it
9
+ through.
10
+ - A `cp1` goes anywhere an output does. `requestInvoice` sends it as the
11
+ comment alone. The `*WithHash` calls send it as `p1`/`p2`, while a hash
12
+ keeps `h`/`h2`, which every mint understands. That is lnurl-wallet's rule.
13
+ - `fetchNoteInfoByHash` and `buildNoteInfoUrlByHash` take a `cp1`, sent as
14
+ `p`.
15
+ - `noteIdOf(k1)` is the id a mint files a note under, for either kind.
16
+ `noteLookupOf(k1)` is what to look it up by without disclosing it.
17
+ - `noteSignatureMessage` and `noteSignatureDigest` build the message over the
18
+ key for a `ck1`.
19
+
20
+ ## 0.10.0 - 2026-09-11
21
+
22
+ **LUD-25 Part 2 building blocks.** Notes keyed by a public key and spent by a
23
+ recoverable signature. The wire calls come next; this is the part every one of
24
+ them rests on.
25
+
26
+ - The four encodings: `cp1` (a note's public key), `ck1` (its bearer secret),
27
+ `cs1` (the mint's certificate) and `cx1` (a watch-only branch), with
28
+ `encode*`, `decode*` and `is*` for each. Decoders return null rather than
29
+ throw, and refuse mixed case as BIP-350 does.
30
+ - `deriveNotePubkey` and `deriveNoteSecretKey`, the per-note key tweak.
31
+ - `signNoteOwnership` and `recoverNoteOwnershipPubkey`.
32
+ - `deriveCashAddressNode` and `cashNodeToCx1`. The branch sits at
33
+ `m/139'/1'/d1..d4`, which is what lnurl-wallet derives, not the
34
+ `m/139'/d1..d4` the spec text gives.
35
+ - `verifyNoteSignature` now takes a `ck1` note and a `cs1` certificate, so a
36
+ Part 2 note verifies offline the same way a Part 1 note does.
37
+
38
+ Names and signatures match lnurl-wallet's `src/lib`. Every value is graded
39
+ against `test/vectors/part2.json`, generated from lnurl-wallet and checked
40
+ against lnurl-mint.
41
+
3
42
  ## 0.9.0 - 2026-09-09
4
43
 
5
44
  **`fetchMintAddress` reads three more fields.** The reference mint publishes
package/README.md CHANGED
@@ -373,6 +373,58 @@ them for you.
373
373
  The seed is bearer material for every note the wallet will ever hold. Store
374
374
  it the way you store the notes, and never log it.
375
375
 
376
+ ## Notes keyed by a public key (LUD-25 Part 2)
377
+
378
+ A Part 2 note swaps the hash for a key pair. The wallet keeps `sk`. The mint
379
+ only ever sees `pk`, written `cp1…`. To spend the note you hand over `ck1…`,
380
+ a recoverable signature by `sk` over the fixed message `LNURLcash`, and the
381
+ mint recovers `pk` from it to find the note. The mint's certificate, `cs1…`,
382
+ is the same signature mints already make, over `hex(pk)` instead of a hash.
383
+ So a recipient can check a note offline with nothing but `ck1` and `cs1`.
384
+
385
+ The wire calls take both kinds. A `ck1` goes anywhere a `k1` does: a note
386
+ URL, `fetchNoteInfo`, rotate, split, merge and melt. A `cp1` goes anywhere an
387
+ output does: `requestInvoice`'s `h`, and the output of every `*WithHash` call,
388
+ where it is sent as `p1`/`p2` while a hash keeps `h`/`h2`, the same rule as
389
+ lnurl-wallet. `noteIdOf(k1)` gives the id a mint files either kind under, and
390
+ `noteLookupOf(k1)` what to pass `fetchNoteInfoByHash` to check a note without
391
+ disclosing it. The names match lnurl-wallet's `src/lib`, so moving to it
392
+ later is an import change.
393
+
394
+ ```ts
395
+ import {
396
+ deriveCashRoot, deriveCashAddressNode, cashNodeToCx1, encodeCx1,
397
+ deriveNotePubkey, deriveNoteSecretKey, signNoteOwnership, encodeCk1,
398
+ verifyNoteSignature
399
+ } from 'lnurlcash-kit'
400
+
401
+ const node = deriveCashAddressNode(deriveCashRoot(seed), 'mint.example')
402
+ const {pubkeyXOnly, chainCode} = cashNodeToCx1(node)
403
+ const cx1 = encodeCx1(pubkeyXOnly, chainCode) // watch-only
404
+
405
+ const pk = deriveNotePubkey(pubkeyXOnly, chainCode, i) // what a watcher derives
406
+ const sk = deriveNoteSecretKey(node.privateKey, node.chainCode, i)
407
+ const ck1 = encodeCk1(signNoteOwnership(sk)) // the bearer secret
408
+
409
+ verifyNoteSignature(ck1, amountMsat, cs1, mintPubkey) // offline
410
+ ```
411
+
412
+ Three things worth knowing:
413
+
414
+ - **The branch path follows the reference wallet, not the spec text.** It is
415
+ `m/139'/1'/d1/d2/d3/d4`, with the hashing key at `m/139'/1'/0`. The spec
416
+ says `m/139'/d1..d4`, which is the node the Part 1 ladder already uses, and
417
+ a wallet following it finds none of lnurl-wallet's notes.
418
+ - **A `cx1` links every note on its branch.** It cannot spend anything, but
419
+ whoever holds it can list every key on the branch and ask the mint about
420
+ each one. Register it with a mint and that mint sees everything paid to the
421
+ address. Use the branch for receiving and rotate off it.
422
+ - **`i` is any uint32**, serialised as 4 bytes big-endian, never hardened.
423
+ lnurl-wallet and lnurl-mint agree on that; the spec does not say.
424
+
425
+ `test/vectors/part2.json` was generated from lnurl-wallet and checked against
426
+ lnurl-mint. It moves into lnurlcash-conformance next.
427
+
376
428
  ## Minting a note you named yourself
377
429
 
378
430
  By default the secret of a freshly minted note is the invoice's payment
package/dist/index.d.ts CHANGED
@@ -273,6 +273,31 @@ declare const cashSecretSource: (root: CashNode, host: string, start?: number) =
273
273
  index: () => number;
274
274
  };
275
275
 
276
+ declare const encodeCp1: (pubkeyXOnly: Uint8Array) => string;
277
+ declare const decodeCp1: (value: string) => Uint8Array | null;
278
+ declare const isCp1: (value: string) => boolean;
279
+ declare const encodeCk1: (signature: Uint8Array) => string;
280
+ declare const decodeCk1: (value: string) => Uint8Array | null;
281
+ declare const isCk1: (value: string) => boolean;
282
+ declare const encodeCs1: (signature: Uint8Array) => string;
283
+ declare const decodeCs1: (value: string) => Uint8Array | null;
284
+ declare const isCs1: (value: string) => boolean;
285
+ type Cx1 = {
286
+ pubkeyXOnly: Uint8Array;
287
+ chainCode: Uint8Array;
288
+ };
289
+ declare const encodeCx1: (pubkeyXOnly: Uint8Array, chainCode: Uint8Array) => string;
290
+ declare const decodeCx1: (value: string) => Cx1 | null;
291
+ declare const isCx1: (value: string) => boolean;
292
+ declare const deriveNotePubkey: (branchPubkeyXOnly: Uint8Array, chainCode: Uint8Array, index: number) => Uint8Array;
293
+ declare const deriveNoteSecretKey: (branchPrivateKey: Uint8Array, chainCode: Uint8Array, index: number) => Uint8Array;
294
+ declare const signNoteOwnership: (secretKey: Uint8Array) => Uint8Array;
295
+ declare const recoverNoteOwnershipPubkey: (signature: Uint8Array) => Uint8Array | null;
296
+ declare const noteIdOf: (k1: string) => string | null;
297
+ declare const noteLookupOf: (k1: string) => string | null;
298
+ declare const deriveCashAddressNode: (root: CashNode, host: string) => CashNode;
299
+ declare const cashNodeToCx1: (node: CashNode) => Cx1;
300
+
276
301
  declare const PAYMENT_REQUEST_PREFIX = "lnurlcashreq1";
277
302
  type PaymentRequestMethodDetails = {
278
303
  mints: string[];
@@ -380,4 +405,4 @@ declare const createClient: (options?: LnurlcashOptions) => {
380
405
  };
381
406
  type LnurlcashClient = ReturnType<typeof createClient>;
382
407
 
383
- 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 };
408
+ export { AmbiguousMintError, AmbiguousMutationError, type BoundMintCommitment, type CashNode, type Cx1, 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, cashNodeToCx1, cashNodeToHex, cashSecretAt, cashSecretSource, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodeCk1, decodeCp1, decodeCs1, decodeCx1, decodePaymentRequest, defaultRandomSecret, deriveCashAddressNode, deriveCashChild, deriveCashDomainNode, deriveCashRoot, deriveCashSecret, deriveNotePubkey, deriveNoteRoot, deriveNoteSecret, deriveNoteSecretKey, derivedSecretSource, describeMintFee, encodeCk1, encodeCp1, encodeCs1, encodeCx1, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isCk1, isCp1, isCs1, isCx1, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteIdOf, noteK1, noteLookupOf, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, recoverNoteOwnershipPubkey, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreFromSeed, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, signNoteOwnership, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { bech32, base64urlnopad } from '@scure/base';
2
- import { hmac } from '@noble/hashes/hmac.js';
1
+ import { bech32, base64urlnopad, bech32m } from '@scure/base';
2
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
3
3
  import { sha256, sha512 } from '@noble/hashes/sha2.js';
4
4
  import { utf8ToBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
5
- import { secp256k1 } from '@noble/curves/secp256k1.js';
5
+ import { hmac } from '@noble/hashes/hmac.js';
6
6
 
7
7
  // src/urls.ts
8
8
  var isBech32Lnurl = (data) => data.trim().toUpperCase().startsWith("LNURL1");
@@ -120,12 +120,95 @@ var serverOf = (url) => {
120
120
  return url;
121
121
  }
122
122
  };
123
+ var HARDENED = 2147483648;
124
+ var CURVE_N = secp256k1.Point.Fn.ORDER;
125
+ var MASTER_KEY_DOMAIN = utf8ToBytes("Bitcoin seed");
126
+ var CASH_PURPOSE = 139;
127
+ var numberOf = (bytes) => bytes.length === 0 ? 0n : BigInt(`0x${bytesToHex(bytes)}`);
128
+ var to32Bytes = (value) => hexToBytes(value.toString(16).padStart(64, "0"));
129
+ var readUint32BE = (bytes, offset) => (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
130
+ var deriveCashChild = (node, index) => {
131
+ if (!Number.isSafeInteger(index) || index < 0 || index > 4294967295) {
132
+ throw new RangeError(`A BIP-32 child index must be a uint32, not ${index}.`);
133
+ }
134
+ const data = new Uint8Array(37);
135
+ if (index >= HARDENED) {
136
+ data.set(node.privateKey, 1);
137
+ } else {
138
+ data.set(
139
+ secp256k1.Point.BASE.multiply(numberOf(node.privateKey)).toBytes(true),
140
+ 0
141
+ );
142
+ }
143
+ data[33] = index >>> 24 & 255;
144
+ data[34] = index >>> 16 & 255;
145
+ data[35] = index >>> 8 & 255;
146
+ data[36] = index & 255;
147
+ const material = hmac(sha512, node.chainCode, data);
148
+ const left = numberOf(material.subarray(0, 32));
149
+ const key = (left + numberOf(node.privateKey)) % CURVE_N;
150
+ if (left >= CURVE_N || key === 0n) {
151
+ throw new Error(
152
+ `BIP-32 derivation at index ${index} produced an invalid key. Use the next index.`
153
+ );
154
+ }
155
+ return { privateKey: to32Bytes(key), chainCode: material.slice(32) };
156
+ };
157
+ var masterFrom = (seed) => {
158
+ if (seed.length < 16 || seed.length > 64) {
159
+ throw new RangeError(
160
+ `A BIP-32 seed must be 16 to 64 bytes, not ${seed.length}.`
161
+ );
162
+ }
163
+ const material = hmac(sha512, MASTER_KEY_DOMAIN, seed);
164
+ const key = numberOf(material.subarray(0, 32));
165
+ if (key === 0n || key >= CURVE_N) {
166
+ throw new Error("This seed does not produce a valid BIP-32 master key.");
167
+ }
168
+ return { privateKey: material.slice(0, 32), chainCode: material.slice(32) };
169
+ };
170
+ var deriveCashRoot = (seed) => deriveCashChild(masterFrom(seed), CASH_PURPOSE + HARDENED);
171
+ var cashDomainIndices = (root, host) => {
172
+ const hashingKey = deriveCashChild(root, 0).privateKey;
173
+ const material = hmac(sha256, hashingKey, utf8ToBytes(host));
174
+ return [0, 4, 8, 12].map((offset) => readUint32BE(material, offset));
175
+ };
176
+ var deriveCashDomainNode = (root, host) => cashDomainIndices(root, host).reduce(deriveCashChild, root);
177
+ var requireIndex = (index) => {
178
+ if (!Number.isSafeInteger(index) || index < 0 || index >= HARDENED) {
179
+ throw new RangeError(
180
+ `A note index must be an integer in [0, 2^31), not ${index}.`
181
+ );
182
+ }
183
+ return index;
184
+ };
185
+ var cashSecretAt = (domainNode, index) => bytesToHex(
186
+ deriveCashChild(domainNode, requireIndex(index) + HARDENED).privateKey
187
+ );
188
+ var deriveCashSecret = (root, host, index) => cashSecretAt(deriveCashDomainNode(root, host), index);
189
+ var cashNodeToHex = (node) => bytesToHex(node.privateKey) + bytesToHex(node.chainCode);
190
+ var cashNodeFromHex = (hex) => {
191
+ const bytes = hexToBytes(hex.trim().toLowerCase());
192
+ if (bytes.length !== 64) {
193
+ throw new RangeError(
194
+ `A cash node is 64 bytes - a 32-byte key and a 32-byte chain code - not ${bytes.length}.`
195
+ );
196
+ }
197
+ return { privateKey: bytes.slice(0, 32), chainCode: bytes.slice(32) };
198
+ };
199
+ var cashSecretSource = (root, host, start = 0) => {
200
+ const domainNode = deriveCashDomainNode(root, host);
201
+ let next = requireIndex(start);
202
+ const source = (() => cashSecretAt(domainNode, next++));
203
+ source.index = () => next;
204
+ return source;
205
+ };
123
206
  var hashK1 = (k1) => bytesToHex(sha256(hexToBytes(k1)));
124
207
  var defaultRandomSecret = () => bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
125
208
  var isPreimage = (value) => /^[0-9a-fA-F]{64}$/.test(value.trim());
126
209
  var NOTE_DERIVATION_DOMAIN = utf8ToBytes("lnurlcash-note-v1");
127
210
  var deriveNoteRoot = (seed) => hmac(sha256, NOTE_DERIVATION_DOMAIN, seed);
128
- var requireIndex = (index) => {
211
+ var requireIndex2 = (index) => {
129
212
  if (!Number.isSafeInteger(index) || index < 0) {
130
213
  throw new RangeError(
131
214
  `A note index must be a non-negative integer, not ${index}.`
@@ -133,14 +216,137 @@ var requireIndex = (index) => {
133
216
  }
134
217
  return index;
135
218
  };
136
- var deriveNoteSecret = (root, host, index) => bytesToHex(hmac(sha256, root, utf8ToBytes(`${host}:${requireIndex(index)}`)));
219
+ var deriveNoteSecret = (root, host, index) => bytesToHex(hmac(sha256, root, utf8ToBytes(`${host}:${requireIndex2(index)}`)));
137
220
  var derivedSecretSource = (root, host, start = 0) => {
138
- let next = requireIndex(start);
221
+ let next = requireIndex2(start);
139
222
  const source = (() => deriveNoteSecret(root, host, next++));
140
223
  source.index = () => next;
141
224
  return source;
142
225
  };
143
226
 
227
+ // src/recoverable.ts
228
+ var encodeFixed = (hrp, bytes, length) => {
229
+ if (bytes.length !== length) {
230
+ throw new RangeError(`A ${hrp}1 payload is ${length} bytes, not ${bytes.length}.`);
231
+ }
232
+ return bech32m.encode(hrp, bech32m.toWords(bytes), false);
233
+ };
234
+ var decodeFixed = (hrp, value, length) => {
235
+ if (typeof value !== "string") return null;
236
+ try {
237
+ const decoded = bech32m.decode(value.trim(), false);
238
+ if (decoded.prefix !== hrp) return null;
239
+ const bytes = bech32m.fromWords(decoded.words);
240
+ return bytes.length === length ? bytes : null;
241
+ } catch {
242
+ return null;
243
+ }
244
+ };
245
+ var encodeCp1 = (pubkeyXOnly) => encodeFixed("cp", pubkeyXOnly, 32);
246
+ var decodeCp1 = (value) => decodeFixed("cp", value, 32);
247
+ var isCp1 = (value) => decodeCp1(value) !== null;
248
+ var encodeCk1 = (signature) => encodeFixed("ck", signature, 65);
249
+ var decodeCk1 = (value) => decodeFixed("ck", value, 65);
250
+ var isCk1 = (value) => decodeCk1(value) !== null;
251
+ var encodeCs1 = (signature) => encodeFixed("cs", signature, 65);
252
+ var decodeCs1 = (value) => decodeFixed("cs", value, 65);
253
+ var isCs1 = (value) => decodeCs1(value) !== null;
254
+ var encodeCx1 = (pubkeyXOnly, chainCode) => {
255
+ if (pubkeyXOnly.length !== 32 || chainCode.length !== 32) {
256
+ throw new RangeError("A cx1 is a 32-byte x-only public key and a 32-byte chain code.");
257
+ }
258
+ return encodeFixed("cx", new Uint8Array([...pubkeyXOnly, ...chainCode]), 64);
259
+ };
260
+ var decodeCx1 = (value) => {
261
+ const bytes = decodeFixed("cx", value, 64);
262
+ return bytes ? { pubkeyXOnly: bytes.slice(0, 32), chainCode: bytes.slice(32) } : null;
263
+ };
264
+ var isCx1 = (value) => decodeCx1(value) !== null;
265
+ var CURVE_N2 = secp256k1.Point.Fn.ORDER;
266
+ var NOTE_DERIVE_TAG = sha256(utf8ToBytes("LNURLcash/derive"));
267
+ var numberOf2 = (bytes) => BigInt(`0x${bytesToHex(bytes)}`);
268
+ var to32Bytes2 = (value) => hexToBytes(value.toString(16).padStart(64, "0"));
269
+ var requireUint32 = (index) => {
270
+ if (!Number.isSafeInteger(index) || index < 0 || index > 4294967295) {
271
+ throw new RangeError(`A note index must be a uint32, not ${index}.`);
272
+ }
273
+ return index;
274
+ };
275
+ var tweakFor = (pubkeyXOnly, chainCode, index) => {
276
+ if (pubkeyXOnly.length !== 32 || chainCode.length !== 32) {
277
+ throw new RangeError("A branch is a 32-byte x-only public key and a 32-byte chain code.");
278
+ }
279
+ const i = requireUint32(index);
280
+ const ser = new Uint8Array([i >>> 24 & 255, i >>> 16 & 255, i >>> 8 & 255, i & 255]);
281
+ const t = numberOf2(
282
+ sha256(new Uint8Array([...NOTE_DERIVE_TAG, ...NOTE_DERIVE_TAG, ...pubkeyXOnly, ...chainCode, ...ser]))
283
+ );
284
+ if (t >= CURVE_N2) {
285
+ throw new Error(`Note index ${index} is unusable on this branch. Use the next index.`);
286
+ }
287
+ return t;
288
+ };
289
+ var deriveNotePubkey = (branchPubkeyXOnly, chainCode, index) => {
290
+ const t = tweakFor(branchPubkeyXOnly, chainCode, index);
291
+ const branch = secp256k1.Point.fromBytes(new Uint8Array([2, ...branchPubkeyXOnly]));
292
+ const note = t === 0n ? branch : branch.add(secp256k1.Point.BASE.multiply(t));
293
+ if (note.is0()) {
294
+ throw new Error(`Note index ${index} is unusable on this branch. Use the next index.`);
295
+ }
296
+ return note.toBytes(true).slice(1);
297
+ };
298
+ var deriveNoteSecretKey = (branchPrivateKey, chainCode, index) => {
299
+ const p = numberOf2(branchPrivateKey);
300
+ if (branchPrivateKey.length !== 32 || p === 0n || p >= CURVE_N2) {
301
+ throw new RangeError("A branch private key is a 32-byte scalar in [1, n).");
302
+ }
303
+ const branch = secp256k1.Point.BASE.multiply(p);
304
+ const t = tweakFor(branch.toBytes(true).slice(1), chainCode, index);
305
+ const even = branch.y % 2n === 0n ? p : CURVE_N2 - p;
306
+ const key = (even + t) % CURVE_N2;
307
+ if (key === 0n) {
308
+ throw new Error(`Note index ${index} is unusable on this branch. Use the next index.`);
309
+ }
310
+ return to32Bytes2(key);
311
+ };
312
+ var NOTE_OWNERSHIP_DIGEST = sha256(
313
+ sha256(new Uint8Array([...utf8ToBytes("Lightning Signed Message:"), ...utf8ToBytes("LNURLcash")]))
314
+ );
315
+ var signNoteOwnership = (secretKey) => {
316
+ const signature = secp256k1.sign(NOTE_OWNERSHIP_DIGEST, secretKey, {
317
+ format: "recovered",
318
+ prehash: false
319
+ });
320
+ return new Uint8Array([...signature.subarray(1), signature[0]]);
321
+ };
322
+ var recoverNoteOwnershipPubkey = (signature) => {
323
+ if (!(signature instanceof Uint8Array) || signature.length !== 65) return null;
324
+ try {
325
+ const recoveryIdFirst = new Uint8Array([signature[64], ...signature.subarray(0, 64)]);
326
+ return secp256k1.recoverPublicKey(recoveryIdFirst, NOTE_OWNERSHIP_DIGEST, { prehash: false }).slice(1);
327
+ } catch {
328
+ return null;
329
+ }
330
+ };
331
+ var noteIdOf = (k1) => {
332
+ if (typeof k1 !== "string") return null;
333
+ const value = k1.trim().toLowerCase();
334
+ if (isPreimage(value)) return hashK1(value);
335
+ const signature = decodeCk1(value);
336
+ const pubkey = signature ? recoverNoteOwnershipPubkey(signature) : null;
337
+ return pubkey ? bytesToHex(pubkey) : null;
338
+ };
339
+ var noteLookupOf = (k1) => {
340
+ const id = noteIdOf(k1);
341
+ if (id === null) return null;
342
+ return isCk1(k1.trim().toLowerCase()) ? encodeCp1(hexToBytes(id)) : id;
343
+ };
344
+ var deriveCashAddressNode = (root, host) => deriveCashDomainNode(deriveCashChild(root, 1 + 2147483648), host);
345
+ var cashNodeToCx1 = (node) => ({
346
+ pubkeyXOnly: secp256k1.getPublicKey(node.privateKey, true).slice(1),
347
+ chainCode: node.chainCode.slice()
348
+ });
349
+
144
350
  // src/note.ts
145
351
  var noteK1 = (url) => {
146
352
  try {
@@ -178,20 +384,21 @@ var noteSignature = (url) => {
178
384
  var resolveNoteInput = (value) => {
179
385
  const url = resolveLnurlInput(value);
180
386
  const k1 = url ? noteK1(url) : null;
181
- if (!url || !k1 || !isPreimage(k1)) return null;
387
+ if (!url || !k1 || noteIdOf(k1) === null) return null;
182
388
  return url;
183
389
  };
184
390
  var isValidNoteInput = (value) => resolveNoteInput(value) !== null;
185
391
  var buildNoteInfoUrlByHash = (withdrawLink, h) => {
186
- const hex = h.trim().toLowerCase();
187
- if (!/^[0-9a-f]{64}$/.test(hex)) {
188
- throw new Error("A note hash must be 32 bytes of hex.");
392
+ const value = h.trim().toLowerCase();
393
+ const key = isCp1(value);
394
+ if (!key && !/^[0-9a-f]{64}$/.test(value)) {
395
+ throw new Error("A note hash must be 32 bytes of hex, or a cp1 key.");
189
396
  }
190
397
  const url = new URL(fromLud17(withdrawLink.trim()));
191
398
  url.searchParams.delete("k1");
192
399
  url.searchParams.delete("amount");
193
400
  url.searchParams.delete("sig");
194
- url.searchParams.set("h", hex);
401
+ url.searchParams.set(key ? "p" : "h", value);
195
402
  return url.toString();
196
403
  };
197
404
  var buildNoteUrl = (withdrawLink, k1, amountMsat) => {
@@ -218,89 +425,6 @@ var withoutK1 = (url, amountMsat, signature) => {
218
425
  else newUrl.searchParams.delete("sig");
219
426
  return newUrl.toString();
220
427
  };
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
- };
304
428
 
305
429
  // src/errors.ts
306
430
  var LnurlcashError = class extends Error {
@@ -743,9 +867,14 @@ var describeMintFee = (fee) => [
743
867
  fee.feePpm > 0 ? `${formatFeePercent(fee.feePpm)}% of the amount paid` : null
744
868
  ].filter(Boolean).join(" + ");
745
869
  var LIGHTNING_SIGNED_MESSAGE_PREFIX = utf8ToBytes("Lightning Signed Message:");
746
- var noteSignatureMessage = (k1, amountMsat) => noteSignatureMessageForHash(hashK1(k1), amountMsat);
870
+ var requireNoteId = (k1) => {
871
+ const id = noteIdOf(k1);
872
+ if (id === null) throw new Error("A k1 is 32 bytes of hex or a ck1.");
873
+ return id;
874
+ };
875
+ var noteSignatureMessage = (k1, amountMsat) => noteSignatureMessageForHash(requireNoteId(k1), amountMsat);
747
876
  var noteSignatureMessageForHash = (h, amountMsat) => `LNURLcash:${amountMsat}:${h.trim().toLowerCase()}`;
748
- var noteSignatureDigest = (k1, amountMsat) => noteSignatureDigestForHash(hashK1(k1), amountMsat);
877
+ var noteSignatureDigest = (k1, amountMsat) => noteSignatureDigestForHash(requireNoteId(k1), amountMsat);
749
878
  var noteSignatureDigestForHash = (h, amountMsat) => sha256(
750
879
  sha256(
751
880
  new Uint8Array([
@@ -756,22 +885,23 @@ var noteSignatureDigestForHash = (h, amountMsat) => sha256(
756
885
  );
757
886
  var NO_MATCH = { valid: false, pubkey: null };
758
887
  var verifyNoteSignatureAgainst = (k1, amountMsat, signatureHex, mintPubkeys) => {
759
- let h;
760
- try {
761
- h = hashK1(k1);
762
- } catch {
763
- return NO_MATCH;
764
- }
888
+ const h = noteIdOf(k1);
889
+ if (h === null) return NO_MATCH;
765
890
  return verifyNoteSignatureHashAgainst(h, amountMsat, signatureHex, mintPubkeys);
766
891
  };
767
892
  var verifyNoteSignatureHashAgainst = (h, amountMsat, signatureHex, mintPubkeys) => {
768
893
  const targets = (Array.isArray(mintPubkeys) ? mintPubkeys : [mintPubkeys]).filter((key) => typeof key === "string").map((key) => key.trim().toLowerCase());
769
894
  if (targets.length === 0) return NO_MATCH;
770
895
  let wireSig;
771
- try {
772
- wireSig = hexToBytes(signatureHex);
773
- } catch {
774
- return NO_MATCH;
896
+ const certificate = decodeCs1(signatureHex);
897
+ if (certificate) {
898
+ wireSig = certificate;
899
+ } else {
900
+ try {
901
+ wireSig = hexToBytes(signatureHex);
902
+ } catch {
903
+ return NO_MATCH;
904
+ }
775
905
  }
776
906
  if (wireSig.length !== 65) return NO_MATCH;
777
907
  let digest;
@@ -1020,12 +1150,16 @@ var meltNote = async (callback, k1, pr, options = {}) => {
1020
1150
  pr: typeof body.pr === "string" ? body.pr : void 0
1021
1151
  };
1022
1152
  };
1153
+ var outputParam = (value, which) => {
1154
+ const cp1 = isCp1(value.trim().toLowerCase());
1155
+ return [cp1 ? `p${which}` : which === 1 ? "h" : "h2", value];
1156
+ };
1023
1157
  var rotateNoteWithHash = async (callback, k1, h, options = {}) => {
1024
1158
  const body = await replayableCallbackRequest(
1025
1159
  callback,
1026
1160
  [
1027
1161
  ["k1", k1],
1028
- ["h", h]
1162
+ outputParam(h, 1)
1029
1163
  ],
1030
1164
  options
1031
1165
  );
@@ -1037,8 +1171,8 @@ var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) =
1037
1171
  [
1038
1172
  ...k1s.map((k1) => ["k1", k1]),
1039
1173
  ["amount", String(amountMsat)],
1040
- ["h", h],
1041
- ["h2", h2]
1174
+ outputParam(h, 1),
1175
+ outputParam(h2, 2)
1042
1176
  ],
1043
1177
  options
1044
1178
  );
@@ -1050,7 +1184,7 @@ var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) =
1050
1184
  var mergeNotesWithHash = async (callback, k1s, h, options = {}) => {
1051
1185
  const body = await replayableCallbackRequest(
1052
1186
  callback,
1053
- [...k1s.map((k1) => ["k1", k1]), ["h", h]],
1187
+ [...k1s.map((k1) => ["k1", k1]), outputParam(h, 1)],
1054
1188
  options
1055
1189
  );
1056
1190
  return { signature: requireSignature(body.sig, options, "merge") };
@@ -1246,14 +1380,17 @@ var requestInvoice = async (payCallback, amountMsat, options = {}) => {
1246
1380
  const cbUrl = new URL(payCallback);
1247
1381
  cbUrl.searchParams.set("amount", String(amountMsat));
1248
1382
  if (options.h !== void 0) {
1249
- if (!isPreimage(options.h)) {
1383
+ const h = options.h.trim().toLowerCase();
1384
+ if (isCp1(h)) {
1385
+ cbUrl.searchParams.set("comment", h);
1386
+ } else if (isPreimage(h)) {
1387
+ cbUrl.searchParams.set("comment", h);
1388
+ cbUrl.searchParams.set("h", h);
1389
+ } else {
1250
1390
  throw new RequestRefusedError(
1251
- "An output hash must be 32 bytes of hex - no invoice was requested."
1391
+ "An output must be 32 bytes of hex or a cp1 key - no invoice was requested."
1252
1392
  );
1253
1393
  }
1254
- const h = options.h.trim().toLowerCase();
1255
- cbUrl.searchParams.set("comment", h);
1256
- cbUrl.searchParams.set("h", h);
1257
1394
  }
1258
1395
  const body = await lnurlFetch(cbUrl, resolveOptions(options));
1259
1396
  if (typeof body?.pr !== "string") {
@@ -1601,4 +1738,4 @@ var createClient = (options = {}) => ({
1601
1738
  settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
1602
1739
  });
1603
1740
 
1604
- 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 };
1741
+ export { AmbiguousMintError, AmbiguousMutationError, HashLookupUnsupportedError, InsufficientValueError, LnurlcashError, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, PendingNoteError, ProtocolError, RequestRefusedError, ServiceRejectedError, UnverifiableNoteError, applyMintFee, buildNoteInfoUrlByHash, buildNoteUrl, cashDomainIndices, cashNodeFromHex, cashNodeToCx1, cashNodeToHex, cashSecretAt, cashSecretSource, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodeCk1, decodeCp1, decodeCs1, decodeCx1, decodePaymentRequest, defaultRandomSecret, deriveCashAddressNode, deriveCashChild, deriveCashDomainNode, deriveCashRoot, deriveCashSecret, deriveNotePubkey, deriveNoteRoot, deriveNoteSecret, deriveNoteSecretKey, derivedSecretSource, describeMintFee, encodeCk1, encodeCp1, encodeCs1, encodeCx1, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchNoteInfoByHash, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isCk1, isCp1, isCs1, isCx1, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeBatches, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, namesMintOutput, newSecretsOf, noteDeclaredAmount, noteIdOf, noteK1, noteLookupOf, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, recoverNoteOwnershipPubkey, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreFromSeed, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, signNoteOwnership, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
package/llms.txt CHANGED
@@ -48,6 +48,27 @@ cashSecretSource(root, host, start) -> RandomSecret & {index()}
48
48
  cashNodeToHex/cashNodeFromHex privateKey || chainCode, 64 bytes
49
49
  deriveCashChild(node, rawUint32) -> CashNode BIP-32 CKDpriv, for checking
50
50
  this library against BIP-32's own vectors
51
+ LUD-25 Part 2: a note keyed by a public key, spent by a recoverable signature
52
+ encodeCp1/decodeCp1/isCp1 note pubkey, 32-byte x-only, bech32m "cp"
53
+ encodeCk1/decodeCk1/isCk1 note bearer secret, 65-byte r||s||recid, "ck"
54
+ encodeCs1/decodeCs1/isCs1 mint certificate over hex(pk), same layout, "cs"
55
+ encodeCx1/decodeCx1/isCx1 watch-only branch {pubkeyXOnly, chainCode}, "cx"
56
+ decoders return null, never throw; mixed case is refused (BIP-350)
57
+ deriveCashAddressNode(root, host) -> CashNode m/139'/1'/d1..d4, hashing key
58
+ m/139'/1'/0. The REFERENCE WALLET's path, not the spec text's m/139'/d1..d4
59
+ (that is the Part 1 ladder's node, and finds none of its notes).
60
+ cashNodeToCx1(node) -> Cx1
61
+ deriveNotePubkey(P, chainCode, i) -> pk watch-only, BIP-341 style tweak;
62
+ i any uint32, 4-byte big-endian, never hardened
63
+ deriveNoteSecretKey(p, chainCode, i) -> sk negates p first when P has odd y
64
+ signNoteOwnership(sk) -> 65 bytes RFC6979 over fixed "LNURLcash"
65
+ recoverNoteOwnershipPubkey(sig) -> pk | null
66
+ verifyNoteSignature also takes a ck1 as k1 and a cs1 as the signature
67
+ noteIdOf(k1) -> id | null sha256(k1) for a secret, the recovered key for a
68
+ ck1. One note has many valid ck1 strings: compare notes by this, not by k1
69
+ noteLookupOf(k1) -> string hash, or cp1 for a ck1; for fetchNoteInfoByHash
70
+ Wire: a ck1 goes wherever a k1 does; a cp1 wherever an output does
71
+ (requestInvoice h, *WithHash outputs: cp1 sent as p1/p2, hash as h/h2)
51
72
  deriveNoteRoot(seedBytes) -> Uint8Array LEGACY, pre-spec. Do not mint under
52
73
  deriveNoteSecret(root, host, index) -> k1 it; still scanned so old notes live.
53
74
  derivedSecretSource(root, host, start) -> RandomSecret & {index()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lnurlcash-kit",
3
- "version": "0.9.0",
3
+ "version": "0.11.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.7.0",
63
+ "lnurlcash-conformance": "^0.8.0",
64
64
  "tsup": "^8.5.0",
65
65
  "typescript": "^5.7.0",
66
66
  "vitest": "^3.0.0"