lnurlcash-kit 0.10.0 → 0.12.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,36 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 - 2026-09-11
4
+
5
+ **A Part 2 branch rooted in a Nostr key.**
6
+
7
+ - `deriveNostrCashSeed(secretKey)` and `deriveNostrAddressNode(secretKey,
8
+ host)`: a Part 2 address branch rooted in a Nostr identity key, for a holder
9
+ with no BIP-39 words. The seed is `HMAC-SHA256(key, "LNURLcash/nostr-seed")`
10
+ and the path from it is the reference wallet's. heartwood-esp32 derives the
11
+ same branch on the device, and `test/vectors/nostr-seed.json` is the file
12
+ both grade against.
13
+ - `deriveCashMaster(seed)`: the BIP-32 master node, exported beside
14
+ `deriveCashChild` for walking a path the kit does not name, such as
15
+ nsec-tree's `m/44'/1237'/727'/0'/0'`.
16
+
17
+ ## 0.11.0 - 2026-09-11
18
+
19
+ **The wire calls take LUD-25 Part 2 notes.**
20
+
21
+ - A `ck1` goes anywhere a `k1` does. `resolveNoteInput` accepts a note URL
22
+ carrying one, and rotate, split, merge, melt and `fetchNoteInfo` pass it
23
+ through.
24
+ - A `cp1` goes anywhere an output does. `requestInvoice` sends it as the
25
+ comment alone. The `*WithHash` calls send it as `p1`/`p2`, while a hash
26
+ keeps `h`/`h2`, which every mint understands. That is lnurl-wallet's rule.
27
+ - `fetchNoteInfoByHash` and `buildNoteInfoUrlByHash` take a `cp1`, sent as
28
+ `p`.
29
+ - `noteIdOf(k1)` is the id a mint files a note under, for either kind.
30
+ `noteLookupOf(k1)` is what to look it up by without disclosing it.
31
+ - `noteSignatureMessage` and `noteSignatureDigest` build the message over the
32
+ key for a `ck1`.
33
+
3
34
  ## 0.10.0 - 2026-09-11
4
35
 
5
36
  **LUD-25 Part 2 building blocks.** Notes keyed by a public key and spent by a
package/README.md CHANGED
@@ -382,10 +382,14 @@ mint recovers `pk` from it to find the note. The mint's certificate, `cs1…`,
382
382
  is the same signature mints already make, over `hex(pk)` instead of a hash.
383
383
  So a recipient can check a note offline with nothing but `ck1` and `cs1`.
384
384
 
385
- This release has the building blocks: the four encodings, key derivation and
386
- ownership signatures. The wire calls that mint to `cp1` and spend with `ck1`
387
- come next. The names match lnurl-wallet's `src/lib`, so moving to it later is
388
- an import change.
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.
389
393
 
390
394
  ```ts
391
395
  import {
@@ -421,6 +425,16 @@ Three things worth knowing:
421
425
  `test/vectors/part2.json` was generated from lnurl-wallet and checked against
422
426
  lnurl-mint. It moves into lnurlcash-conformance next.
423
427
 
428
+ **A branch rooted in a Nostr key.** A holder with no BIP-39 words, such as a
429
+ hardware signer that keeps only its identity key, or a wallet that never made
430
+ any, can still be paid to keys of its own. `deriveNostrAddressNode(secretKey,
431
+ host)` takes the branch from the key that owns the lightning address:
432
+ `HMAC-SHA256(key = secret key, msg = "LNURLcash/nostr-seed")`, then the path
433
+ above unchanged. heartwood-esp32 derives exactly this on the device, graded
434
+ against the same `test/vectors/nostr-seed.json`, so its notes come back from
435
+ its nsec (or the phrase the nsec came from) without the device. This is ours,
436
+ not LUD-25's; a mint sees an ordinary `cx1` either way.
437
+
424
438
  ## Minting a note you named yourself
425
439
 
426
440
  By default the secret of a freshly minted note is the invoice's payment
package/dist/index.d.ts CHANGED
@@ -262,6 +262,7 @@ type CashNode = {
262
262
  chainCode: Uint8Array;
263
263
  };
264
264
  declare const deriveCashChild: (node: CashNode, index: number) => CashNode;
265
+ declare const deriveCashMaster: (seed: Uint8Array) => CashNode;
265
266
  declare const deriveCashRoot: (seed: Uint8Array) => CashNode;
266
267
  declare const cashDomainIndices: (root: CashNode, host: string) => number[];
267
268
  declare const deriveCashDomainNode: (root: CashNode, host: string) => CashNode;
@@ -293,8 +294,13 @@ declare const deriveNotePubkey: (branchPubkeyXOnly: Uint8Array, chainCode: Uint8
293
294
  declare const deriveNoteSecretKey: (branchPrivateKey: Uint8Array, chainCode: Uint8Array, index: number) => Uint8Array;
294
295
  declare const signNoteOwnership: (secretKey: Uint8Array) => Uint8Array;
295
296
  declare const recoverNoteOwnershipPubkey: (signature: Uint8Array) => Uint8Array | null;
297
+ declare const noteIdOf: (k1: string) => string | null;
298
+ declare const noteLookupOf: (k1: string) => string | null;
296
299
  declare const deriveCashAddressNode: (root: CashNode, host: string) => CashNode;
297
300
  declare const cashNodeToCx1: (node: CashNode) => Cx1;
301
+ declare const NOSTR_CASH_SEED_LABEL = "LNURLcash/nostr-seed";
302
+ declare const deriveNostrCashSeed: (secretKey: Uint8Array) => Uint8Array;
303
+ declare const deriveNostrAddressNode: (secretKey: Uint8Array, host: string) => CashNode;
298
304
 
299
305
  declare const PAYMENT_REQUEST_PREFIX = "lnurlcashreq1";
300
306
  type PaymentRequestMethodDetails = {
@@ -403,4 +409,4 @@ declare const createClient: (options?: LnurlcashOptions) => {
403
409
  };
404
410
  type LnurlcashClient = ReturnType<typeof createClient>;
405
411
 
406
- 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, noteK1, 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 };
412
+ 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, NOSTR_CASH_SEED_LABEL, 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, deriveCashMaster, deriveCashRoot, deriveCashSecret, deriveNostrAddressNode, deriveNostrCashSeed, 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
1
  import { bech32, base64urlnopad, bech32m } from '@scure/base';
2
+ import { secp256k1 } from '@noble/curves/secp256k1.js';
2
3
  import { hmac } from '@noble/hashes/hmac.js';
3
4
  import { sha256, sha512 } from '@noble/hashes/sha2.js';
4
5
  import { utf8ToBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
5
- import { secp256k1 } from '@noble/curves/secp256k1.js';
6
6
 
7
7
  // src/urls.ts
8
8
  var isBech32Lnurl = (data) => data.trim().toUpperCase().startsWith("LNURL1");
@@ -120,104 +120,6 @@ var serverOf = (url) => {
120
120
  return url;
121
121
  }
122
122
  };
123
- var hashK1 = (k1) => bytesToHex(sha256(hexToBytes(k1)));
124
- var defaultRandomSecret = () => bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
125
- var isPreimage = (value) => /^[0-9a-fA-F]{64}$/.test(value.trim());
126
- var NOTE_DERIVATION_DOMAIN = utf8ToBytes("lnurlcash-note-v1");
127
- var deriveNoteRoot = (seed) => hmac(sha256, NOTE_DERIVATION_DOMAIN, seed);
128
- var requireIndex = (index) => {
129
- if (!Number.isSafeInteger(index) || index < 0) {
130
- throw new RangeError(
131
- `A note index must be a non-negative integer, not ${index}.`
132
- );
133
- }
134
- return index;
135
- };
136
- var deriveNoteSecret = (root, host, index) => bytesToHex(hmac(sha256, root, utf8ToBytes(`${host}:${requireIndex(index)}`)));
137
- var derivedSecretSource = (root, host, start = 0) => {
138
- let next = requireIndex(start);
139
- const source = (() => deriveNoteSecret(root, host, next++));
140
- source.index = () => next;
141
- return source;
142
- };
143
-
144
- // src/note.ts
145
- var noteK1 = (url) => {
146
- try {
147
- return new URL(url).searchParams.get("k1")?.toLowerCase() ?? null;
148
- } catch {
149
- return null;
150
- }
151
- };
152
- var requireNoteK1 = (url) => {
153
- const k1 = noteK1(url);
154
- if (!k1) {
155
- throw new Error(
156
- "This note carries no secret in its URL - it may be held on a device."
157
- );
158
- }
159
- return k1;
160
- };
161
- var noteDeclaredAmount = (url) => {
162
- try {
163
- const raw = new URL(url).searchParams.get("amount");
164
- if (raw === null) return null;
165
- const n = Number(raw);
166
- return Number.isFinite(n) ? n : null;
167
- } catch {
168
- return null;
169
- }
170
- };
171
- var noteSignature = (url) => {
172
- try {
173
- return new URL(url).searchParams.get("sig");
174
- } catch {
175
- return null;
176
- }
177
- };
178
- var resolveNoteInput = (value) => {
179
- const url = resolveLnurlInput(value);
180
- const k1 = url ? noteK1(url) : null;
181
- if (!url || !k1 || !isPreimage(k1)) return null;
182
- return url;
183
- };
184
- var isValidNoteInput = (value) => resolveNoteInput(value) !== null;
185
- 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.");
189
- }
190
- const url = new URL(fromLud17(withdrawLink.trim()));
191
- url.searchParams.delete("k1");
192
- url.searchParams.delete("amount");
193
- url.searchParams.delete("sig");
194
- url.searchParams.set("h", hex);
195
- return url.toString();
196
- };
197
- var buildNoteUrl = (withdrawLink, k1, amountMsat) => {
198
- const url = new URL(fromLud17(withdrawLink.trim()));
199
- url.searchParams.set("k1", k1.trim().toLowerCase());
200
- if (amountMsat !== void 0) {
201
- url.searchParams.set("amount", String(amountMsat));
202
- }
203
- return url.toString();
204
- };
205
- var withNewK1 = (url, k1, amountMsat, signature) => {
206
- const newUrl = new URL(url);
207
- newUrl.searchParams.set("k1", k1.toLowerCase());
208
- newUrl.searchParams.set("amount", String(amountMsat));
209
- if (signature) newUrl.searchParams.set("sig", signature);
210
- else newUrl.searchParams.delete("sig");
211
- return newUrl.toString();
212
- };
213
- var withoutK1 = (url, amountMsat, signature) => {
214
- const newUrl = new URL(url);
215
- newUrl.searchParams.delete("k1");
216
- newUrl.searchParams.set("amount", String(amountMsat));
217
- if (signature) newUrl.searchParams.set("sig", signature);
218
- else newUrl.searchParams.delete("sig");
219
- return newUrl.toString();
220
- };
221
123
  var HARDENED = 2147483648;
222
124
  var CURVE_N = secp256k1.Point.Fn.ORDER;
223
125
  var MASTER_KEY_DOMAIN = utf8ToBytes("Bitcoin seed");
@@ -252,6 +154,7 @@ var deriveCashChild = (node, index) => {
252
154
  }
253
155
  return { privateKey: to32Bytes(key), chainCode: material.slice(32) };
254
156
  };
157
+ var deriveCashMaster = (seed) => masterFrom(seed);
255
158
  var masterFrom = (seed) => {
256
159
  if (seed.length < 16 || seed.length > 64) {
257
160
  throw new RangeError(
@@ -272,7 +175,7 @@ var cashDomainIndices = (root, host) => {
272
175
  return [0, 4, 8, 12].map((offset) => readUint32BE(material, offset));
273
176
  };
274
177
  var deriveCashDomainNode = (root, host) => cashDomainIndices(root, host).reduce(deriveCashChild, root);
275
- var requireIndex2 = (index) => {
178
+ var requireIndex = (index) => {
276
179
  if (!Number.isSafeInteger(index) || index < 0 || index >= HARDENED) {
277
180
  throw new RangeError(
278
181
  `A note index must be an integer in [0, 2^31), not ${index}.`
@@ -281,7 +184,7 @@ var requireIndex2 = (index) => {
281
184
  return index;
282
185
  };
283
186
  var cashSecretAt = (domainNode, index) => bytesToHex(
284
- deriveCashChild(domainNode, requireIndex2(index) + HARDENED).privateKey
187
+ deriveCashChild(domainNode, requireIndex(index) + HARDENED).privateKey
285
188
  );
286
189
  var deriveCashSecret = (root, host, index) => cashSecretAt(deriveCashDomainNode(root, host), index);
287
190
  var cashNodeToHex = (node) => bytesToHex(node.privateKey) + bytesToHex(node.chainCode);
@@ -296,11 +199,33 @@ var cashNodeFromHex = (hex) => {
296
199
  };
297
200
  var cashSecretSource = (root, host, start = 0) => {
298
201
  const domainNode = deriveCashDomainNode(root, host);
299
- let next = requireIndex2(start);
202
+ let next = requireIndex(start);
300
203
  const source = (() => cashSecretAt(domainNode, next++));
301
204
  source.index = () => next;
302
205
  return source;
303
206
  };
207
+ var hashK1 = (k1) => bytesToHex(sha256(hexToBytes(k1)));
208
+ var defaultRandomSecret = () => bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
209
+ var isPreimage = (value) => /^[0-9a-fA-F]{64}$/.test(value.trim());
210
+ var NOTE_DERIVATION_DOMAIN = utf8ToBytes("lnurlcash-note-v1");
211
+ var deriveNoteRoot = (seed) => hmac(sha256, NOTE_DERIVATION_DOMAIN, seed);
212
+ var requireIndex2 = (index) => {
213
+ if (!Number.isSafeInteger(index) || index < 0) {
214
+ throw new RangeError(
215
+ `A note index must be a non-negative integer, not ${index}.`
216
+ );
217
+ }
218
+ return index;
219
+ };
220
+ var deriveNoteSecret = (root, host, index) => bytesToHex(hmac(sha256, root, utf8ToBytes(`${host}:${requireIndex2(index)}`)));
221
+ var derivedSecretSource = (root, host, start = 0) => {
222
+ let next = requireIndex2(start);
223
+ const source = (() => deriveNoteSecret(root, host, next++));
224
+ source.index = () => next;
225
+ return source;
226
+ };
227
+
228
+ // src/recoverable.ts
304
229
  var encodeFixed = (hrp, bytes, length) => {
305
230
  if (bytes.length !== length) {
306
231
  throw new RangeError(`A ${hrp}1 payload is ${length} bytes, not ${bytes.length}.`);
@@ -404,11 +329,111 @@ var recoverNoteOwnershipPubkey = (signature) => {
404
329
  return null;
405
330
  }
406
331
  };
332
+ var noteIdOf = (k1) => {
333
+ if (typeof k1 !== "string") return null;
334
+ const value = k1.trim().toLowerCase();
335
+ if (isPreimage(value)) return hashK1(value);
336
+ const signature = decodeCk1(value);
337
+ const pubkey = signature ? recoverNoteOwnershipPubkey(signature) : null;
338
+ return pubkey ? bytesToHex(pubkey) : null;
339
+ };
340
+ var noteLookupOf = (k1) => {
341
+ const id = noteIdOf(k1);
342
+ if (id === null) return null;
343
+ return isCk1(k1.trim().toLowerCase()) ? encodeCp1(hexToBytes(id)) : id;
344
+ };
407
345
  var deriveCashAddressNode = (root, host) => deriveCashDomainNode(deriveCashChild(root, 1 + 2147483648), host);
408
346
  var cashNodeToCx1 = (node) => ({
409
347
  pubkeyXOnly: secp256k1.getPublicKey(node.privateKey, true).slice(1),
410
348
  chainCode: node.chainCode.slice()
411
349
  });
350
+ var NOSTR_CASH_SEED_LABEL = "LNURLcash/nostr-seed";
351
+ var deriveNostrCashSeed = (secretKey) => {
352
+ if (!(secretKey instanceof Uint8Array) || secretKey.length !== 32) {
353
+ throw new RangeError("A Nostr secret key is 32 bytes.");
354
+ }
355
+ return hmac(sha256, secretKey, utf8ToBytes(NOSTR_CASH_SEED_LABEL));
356
+ };
357
+ var deriveNostrAddressNode = (secretKey, host) => deriveCashAddressNode(deriveCashRoot(deriveNostrCashSeed(secretKey)), host);
358
+
359
+ // src/note.ts
360
+ var noteK1 = (url) => {
361
+ try {
362
+ return new URL(url).searchParams.get("k1")?.toLowerCase() ?? null;
363
+ } catch {
364
+ return null;
365
+ }
366
+ };
367
+ var requireNoteK1 = (url) => {
368
+ const k1 = noteK1(url);
369
+ if (!k1) {
370
+ throw new Error(
371
+ "This note carries no secret in its URL - it may be held on a device."
372
+ );
373
+ }
374
+ return k1;
375
+ };
376
+ var noteDeclaredAmount = (url) => {
377
+ try {
378
+ const raw = new URL(url).searchParams.get("amount");
379
+ if (raw === null) return null;
380
+ const n = Number(raw);
381
+ return Number.isFinite(n) ? n : null;
382
+ } catch {
383
+ return null;
384
+ }
385
+ };
386
+ var noteSignature = (url) => {
387
+ try {
388
+ return new URL(url).searchParams.get("sig");
389
+ } catch {
390
+ return null;
391
+ }
392
+ };
393
+ var resolveNoteInput = (value) => {
394
+ const url = resolveLnurlInput(value);
395
+ const k1 = url ? noteK1(url) : null;
396
+ if (!url || !k1 || noteIdOf(k1) === null) return null;
397
+ return url;
398
+ };
399
+ var isValidNoteInput = (value) => resolveNoteInput(value) !== null;
400
+ var buildNoteInfoUrlByHash = (withdrawLink, h) => {
401
+ const value = h.trim().toLowerCase();
402
+ const key = isCp1(value);
403
+ if (!key && !/^[0-9a-f]{64}$/.test(value)) {
404
+ throw new Error("A note hash must be 32 bytes of hex, or a cp1 key.");
405
+ }
406
+ const url = new URL(fromLud17(withdrawLink.trim()));
407
+ url.searchParams.delete("k1");
408
+ url.searchParams.delete("amount");
409
+ url.searchParams.delete("sig");
410
+ url.searchParams.set(key ? "p" : "h", value);
411
+ return url.toString();
412
+ };
413
+ var buildNoteUrl = (withdrawLink, k1, amountMsat) => {
414
+ const url = new URL(fromLud17(withdrawLink.trim()));
415
+ url.searchParams.set("k1", k1.trim().toLowerCase());
416
+ if (amountMsat !== void 0) {
417
+ url.searchParams.set("amount", String(amountMsat));
418
+ }
419
+ return url.toString();
420
+ };
421
+ var withNewK1 = (url, k1, amountMsat, signature) => {
422
+ const newUrl = new URL(url);
423
+ newUrl.searchParams.set("k1", k1.toLowerCase());
424
+ newUrl.searchParams.set("amount", String(amountMsat));
425
+ if (signature) newUrl.searchParams.set("sig", signature);
426
+ else newUrl.searchParams.delete("sig");
427
+ return newUrl.toString();
428
+ };
429
+ var withoutK1 = (url, amountMsat, signature) => {
430
+ const newUrl = new URL(url);
431
+ newUrl.searchParams.delete("k1");
432
+ newUrl.searchParams.set("amount", String(amountMsat));
433
+ if (signature) newUrl.searchParams.set("sig", signature);
434
+ else newUrl.searchParams.delete("sig");
435
+ return newUrl.toString();
436
+ };
412
437
 
413
438
  // src/errors.ts
414
439
  var LnurlcashError = class extends Error {
@@ -851,9 +876,14 @@ var describeMintFee = (fee) => [
851
876
  fee.feePpm > 0 ? `${formatFeePercent(fee.feePpm)}% of the amount paid` : null
852
877
  ].filter(Boolean).join(" + ");
853
878
  var LIGHTNING_SIGNED_MESSAGE_PREFIX = utf8ToBytes("Lightning Signed Message:");
854
- var noteSignatureMessage = (k1, amountMsat) => noteSignatureMessageForHash(hashK1(k1), amountMsat);
879
+ var requireNoteId = (k1) => {
880
+ const id = noteIdOf(k1);
881
+ if (id === null) throw new Error("A k1 is 32 bytes of hex or a ck1.");
882
+ return id;
883
+ };
884
+ var noteSignatureMessage = (k1, amountMsat) => noteSignatureMessageForHash(requireNoteId(k1), amountMsat);
855
885
  var noteSignatureMessageForHash = (h, amountMsat) => `LNURLcash:${amountMsat}:${h.trim().toLowerCase()}`;
856
- var noteSignatureDigest = (k1, amountMsat) => noteSignatureDigestForHash(hashK1(k1), amountMsat);
886
+ var noteSignatureDigest = (k1, amountMsat) => noteSignatureDigestForHash(requireNoteId(k1), amountMsat);
857
887
  var noteSignatureDigestForHash = (h, amountMsat) => sha256(
858
888
  sha256(
859
889
  new Uint8Array([
@@ -864,22 +894,10 @@ var noteSignatureDigestForHash = (h, amountMsat) => sha256(
864
894
  );
865
895
  var NO_MATCH = { valid: false, pubkey: null };
866
896
  var verifyNoteSignatureAgainst = (k1, amountMsat, signatureHex, mintPubkeys) => {
867
- const h = signedNoteId(k1);
897
+ const h = noteIdOf(k1);
868
898
  if (h === null) return NO_MATCH;
869
899
  return verifyNoteSignatureHashAgainst(h, amountMsat, signatureHex, mintPubkeys);
870
900
  };
871
- var signedNoteId = (k1) => {
872
- const ownership = decodeCk1(k1);
873
- if (ownership) {
874
- const pubkey = recoverNoteOwnershipPubkey(ownership);
875
- return pubkey ? bytesToHex(pubkey) : null;
876
- }
877
- try {
878
- return hashK1(k1);
879
- } catch {
880
- return null;
881
- }
882
- };
883
901
  var verifyNoteSignatureHashAgainst = (h, amountMsat, signatureHex, mintPubkeys) => {
884
902
  const targets = (Array.isArray(mintPubkeys) ? mintPubkeys : [mintPubkeys]).filter((key) => typeof key === "string").map((key) => key.trim().toLowerCase());
885
903
  if (targets.length === 0) return NO_MATCH;
@@ -1141,12 +1159,16 @@ var meltNote = async (callback, k1, pr, options = {}) => {
1141
1159
  pr: typeof body.pr === "string" ? body.pr : void 0
1142
1160
  };
1143
1161
  };
1162
+ var outputParam = (value, which) => {
1163
+ const cp1 = isCp1(value.trim().toLowerCase());
1164
+ return [cp1 ? `p${which}` : which === 1 ? "h" : "h2", value];
1165
+ };
1144
1166
  var rotateNoteWithHash = async (callback, k1, h, options = {}) => {
1145
1167
  const body = await replayableCallbackRequest(
1146
1168
  callback,
1147
1169
  [
1148
1170
  ["k1", k1],
1149
- ["h", h]
1171
+ outputParam(h, 1)
1150
1172
  ],
1151
1173
  options
1152
1174
  );
@@ -1158,8 +1180,8 @@ var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) =
1158
1180
  [
1159
1181
  ...k1s.map((k1) => ["k1", k1]),
1160
1182
  ["amount", String(amountMsat)],
1161
- ["h", h],
1162
- ["h2", h2]
1183
+ outputParam(h, 1),
1184
+ outputParam(h2, 2)
1163
1185
  ],
1164
1186
  options
1165
1187
  );
@@ -1171,7 +1193,7 @@ var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) =
1171
1193
  var mergeNotesWithHash = async (callback, k1s, h, options = {}) => {
1172
1194
  const body = await replayableCallbackRequest(
1173
1195
  callback,
1174
- [...k1s.map((k1) => ["k1", k1]), ["h", h]],
1196
+ [...k1s.map((k1) => ["k1", k1]), outputParam(h, 1)],
1175
1197
  options
1176
1198
  );
1177
1199
  return { signature: requireSignature(body.sig, options, "merge") };
@@ -1367,14 +1389,17 @@ var requestInvoice = async (payCallback, amountMsat, options = {}) => {
1367
1389
  const cbUrl = new URL(payCallback);
1368
1390
  cbUrl.searchParams.set("amount", String(amountMsat));
1369
1391
  if (options.h !== void 0) {
1370
- if (!isPreimage(options.h)) {
1392
+ const h = options.h.trim().toLowerCase();
1393
+ if (isCp1(h)) {
1394
+ cbUrl.searchParams.set("comment", h);
1395
+ } else if (isPreimage(h)) {
1396
+ cbUrl.searchParams.set("comment", h);
1397
+ cbUrl.searchParams.set("h", h);
1398
+ } else {
1371
1399
  throw new RequestRefusedError(
1372
- "An output hash must be 32 bytes of hex - no invoice was requested."
1400
+ "An output must be 32 bytes of hex or a cp1 key - no invoice was requested."
1373
1401
  );
1374
1402
  }
1375
- const h = options.h.trim().toLowerCase();
1376
- cbUrl.searchParams.set("comment", h);
1377
- cbUrl.searchParams.set("h", h);
1378
1403
  }
1379
1404
  const body = await lnurlFetch(cbUrl, resolveOptions(options));
1380
1405
  if (typeof body?.pr !== "string") {
@@ -1722,4 +1747,4 @@ var createClient = (options = {}) => ({
1722
1747
  settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
1723
1748
  });
1724
1749
 
1725
- 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, noteK1, 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 };
1750
+ export { AmbiguousMintError, AmbiguousMutationError, HashLookupUnsupportedError, InsufficientValueError, LnurlcashError, NOSTR_CASH_SEED_LABEL, 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, deriveCashMaster, deriveCashRoot, deriveCashSecret, deriveNostrAddressNode, deriveNostrCashSeed, 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
@@ -64,6 +64,15 @@ deriveNoteSecretKey(p, chainCode, i) -> sk negates p first when P has odd y
64
64
  signNoteOwnership(sk) -> 65 bytes RFC6979 over fixed "LNURLcash"
65
65
  recoverNoteOwnershipPubkey(sig) -> pk | null
66
66
  verifyNoteSignature also takes a ck1 as k1 and a cs1 as the signature
67
+ deriveCashMaster(seed) -> CashNode the BIP-32 master, for walking a path the kit does not name
68
+ deriveNostrCashSeed(secretKey) -> 32-byte seed HMAC-SHA256(key, "LNURLcash/nostr-seed")
69
+ deriveNostrAddressNode(secretKey, host) -> CashNode the address path from that seed;
70
+ a branch for a holder with no BIP-39 words (heartwood-esp32 derives the same)
71
+ noteIdOf(k1) -> id | null sha256(k1) for a secret, the recovered key for a
72
+ ck1. One note has many valid ck1 strings: compare notes by this, not by k1
73
+ noteLookupOf(k1) -> string hash, or cp1 for a ck1; for fetchNoteInfoByHash
74
+ Wire: a ck1 goes wherever a k1 does; a cp1 wherever an output does
75
+ (requestInvoice h, *WithHash outputs: cp1 sent as p1/p2, hash as h/h2)
67
76
  deriveNoteRoot(seedBytes) -> Uint8Array LEGACY, pre-spec. Do not mint under
68
77
  deriveNoteSecret(root, host, index) -> k1 it; still scanned so old notes live.
69
78
  derivedSecretSource(root, host, start) -> RandomSecret & {index()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lnurlcash-kit",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",