lnurlcash-kit 0.9.0 → 0.10.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 +22 -0
- package/README.md +48 -0
- package/dist/index.d.ts +24 -1
- package/dist/index.js +131 -10
- package/llms.txt +16 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 - 2026-09-11
|
|
4
|
+
|
|
5
|
+
**LUD-25 Part 2 building blocks.** Notes keyed by a public key and spent by a
|
|
6
|
+
recoverable signature. The wire calls come next; this is the part every one of
|
|
7
|
+
them rests on.
|
|
8
|
+
|
|
9
|
+
- The four encodings: `cp1` (a note's public key), `ck1` (its bearer secret),
|
|
10
|
+
`cs1` (the mint's certificate) and `cx1` (a watch-only branch), with
|
|
11
|
+
`encode*`, `decode*` and `is*` for each. Decoders return null rather than
|
|
12
|
+
throw, and refuse mixed case as BIP-350 does.
|
|
13
|
+
- `deriveNotePubkey` and `deriveNoteSecretKey`, the per-note key tweak.
|
|
14
|
+
- `signNoteOwnership` and `recoverNoteOwnershipPubkey`.
|
|
15
|
+
- `deriveCashAddressNode` and `cashNodeToCx1`. The branch sits at
|
|
16
|
+
`m/139'/1'/d1..d4`, which is what lnurl-wallet derives, not the
|
|
17
|
+
`m/139'/d1..d4` the spec text gives.
|
|
18
|
+
- `verifyNoteSignature` now takes a `ck1` note and a `cs1` certificate, so a
|
|
19
|
+
Part 2 note verifies offline the same way a Part 1 note does.
|
|
20
|
+
|
|
21
|
+
Names and signatures match lnurl-wallet's `src/lib`. Every value is graded
|
|
22
|
+
against `test/vectors/part2.json`, generated from lnurl-wallet and checked
|
|
23
|
+
against lnurl-mint.
|
|
24
|
+
|
|
3
25
|
## 0.9.0 - 2026-09-09
|
|
4
26
|
|
|
5
27
|
**`fetchMintAddress` reads three more fields.** The reference mint publishes
|
package/README.md
CHANGED
|
@@ -373,6 +373,54 @@ 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
|
+
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.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
import {
|
|
392
|
+
deriveCashRoot, deriveCashAddressNode, cashNodeToCx1, encodeCx1,
|
|
393
|
+
deriveNotePubkey, deriveNoteSecretKey, signNoteOwnership, encodeCk1,
|
|
394
|
+
verifyNoteSignature
|
|
395
|
+
} from 'lnurlcash-kit'
|
|
396
|
+
|
|
397
|
+
const node = deriveCashAddressNode(deriveCashRoot(seed), 'mint.example')
|
|
398
|
+
const {pubkeyXOnly, chainCode} = cashNodeToCx1(node)
|
|
399
|
+
const cx1 = encodeCx1(pubkeyXOnly, chainCode) // watch-only
|
|
400
|
+
|
|
401
|
+
const pk = deriveNotePubkey(pubkeyXOnly, chainCode, i) // what a watcher derives
|
|
402
|
+
const sk = deriveNoteSecretKey(node.privateKey, node.chainCode, i)
|
|
403
|
+
const ck1 = encodeCk1(signNoteOwnership(sk)) // the bearer secret
|
|
404
|
+
|
|
405
|
+
verifyNoteSignature(ck1, amountMsat, cs1, mintPubkey) // offline
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Three things worth knowing:
|
|
409
|
+
|
|
410
|
+
- **The branch path follows the reference wallet, not the spec text.** It is
|
|
411
|
+
`m/139'/1'/d1/d2/d3/d4`, with the hashing key at `m/139'/1'/0`. The spec
|
|
412
|
+
says `m/139'/d1..d4`, which is the node the Part 1 ladder already uses, and
|
|
413
|
+
a wallet following it finds none of lnurl-wallet's notes.
|
|
414
|
+
- **A `cx1` links every note on its branch.** It cannot spend anything, but
|
|
415
|
+
whoever holds it can list every key on the branch and ask the mint about
|
|
416
|
+
each one. Register it with a mint and that mint sees everything paid to the
|
|
417
|
+
address. Use the branch for receiving and rotate off it.
|
|
418
|
+
- **`i` is any uint32**, serialised as 4 bytes big-endian, never hardened.
|
|
419
|
+
lnurl-wallet and lnurl-mint agree on that; the spec does not say.
|
|
420
|
+
|
|
421
|
+
`test/vectors/part2.json` was generated from lnurl-wallet and checked against
|
|
422
|
+
lnurl-mint. It moves into lnurlcash-conformance next.
|
|
423
|
+
|
|
376
424
|
## Minting a note you named yourself
|
|
377
425
|
|
|
378
426
|
By default the secret of a freshly minted note is the invoice's payment
|
package/dist/index.d.ts
CHANGED
|
@@ -273,6 +273,29 @@ 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 deriveCashAddressNode: (root: CashNode, host: string) => CashNode;
|
|
297
|
+
declare const cashNodeToCx1: (node: CashNode) => Cx1;
|
|
298
|
+
|
|
276
299
|
declare const PAYMENT_REQUEST_PREFIX = "lnurlcashreq1";
|
|
277
300
|
type PaymentRequestMethodDetails = {
|
|
278
301
|
mints: string[];
|
|
@@ -380,4 +403,4 @@ declare const createClient: (options?: LnurlcashOptions) => {
|
|
|
380
403
|
};
|
|
381
404
|
type LnurlcashClient = ReturnType<typeof createClient>;
|
|
382
405
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bech32, base64urlnopad } from '@scure/base';
|
|
1
|
+
import { bech32, base64urlnopad, bech32m } from '@scure/base';
|
|
2
2
|
import { hmac } from '@noble/hashes/hmac.js';
|
|
3
3
|
import { sha256, sha512 } from '@noble/hashes/sha2.js';
|
|
4
4
|
import { utf8ToBytes, bytesToHex, hexToBytes } from '@noble/hashes/utils.js';
|
|
@@ -301,6 +301,114 @@ var cashSecretSource = (root, host, start = 0) => {
|
|
|
301
301
|
source.index = () => next;
|
|
302
302
|
return source;
|
|
303
303
|
};
|
|
304
|
+
var encodeFixed = (hrp, bytes, length) => {
|
|
305
|
+
if (bytes.length !== length) {
|
|
306
|
+
throw new RangeError(`A ${hrp}1 payload is ${length} bytes, not ${bytes.length}.`);
|
|
307
|
+
}
|
|
308
|
+
return bech32m.encode(hrp, bech32m.toWords(bytes), false);
|
|
309
|
+
};
|
|
310
|
+
var decodeFixed = (hrp, value, length) => {
|
|
311
|
+
if (typeof value !== "string") return null;
|
|
312
|
+
try {
|
|
313
|
+
const decoded = bech32m.decode(value.trim(), false);
|
|
314
|
+
if (decoded.prefix !== hrp) return null;
|
|
315
|
+
const bytes = bech32m.fromWords(decoded.words);
|
|
316
|
+
return bytes.length === length ? bytes : null;
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
var encodeCp1 = (pubkeyXOnly) => encodeFixed("cp", pubkeyXOnly, 32);
|
|
322
|
+
var decodeCp1 = (value) => decodeFixed("cp", value, 32);
|
|
323
|
+
var isCp1 = (value) => decodeCp1(value) !== null;
|
|
324
|
+
var encodeCk1 = (signature) => encodeFixed("ck", signature, 65);
|
|
325
|
+
var decodeCk1 = (value) => decodeFixed("ck", value, 65);
|
|
326
|
+
var isCk1 = (value) => decodeCk1(value) !== null;
|
|
327
|
+
var encodeCs1 = (signature) => encodeFixed("cs", signature, 65);
|
|
328
|
+
var decodeCs1 = (value) => decodeFixed("cs", value, 65);
|
|
329
|
+
var isCs1 = (value) => decodeCs1(value) !== null;
|
|
330
|
+
var encodeCx1 = (pubkeyXOnly, chainCode) => {
|
|
331
|
+
if (pubkeyXOnly.length !== 32 || chainCode.length !== 32) {
|
|
332
|
+
throw new RangeError("A cx1 is a 32-byte x-only public key and a 32-byte chain code.");
|
|
333
|
+
}
|
|
334
|
+
return encodeFixed("cx", new Uint8Array([...pubkeyXOnly, ...chainCode]), 64);
|
|
335
|
+
};
|
|
336
|
+
var decodeCx1 = (value) => {
|
|
337
|
+
const bytes = decodeFixed("cx", value, 64);
|
|
338
|
+
return bytes ? { pubkeyXOnly: bytes.slice(0, 32), chainCode: bytes.slice(32) } : null;
|
|
339
|
+
};
|
|
340
|
+
var isCx1 = (value) => decodeCx1(value) !== null;
|
|
341
|
+
var CURVE_N2 = secp256k1.Point.Fn.ORDER;
|
|
342
|
+
var NOTE_DERIVE_TAG = sha256(utf8ToBytes("LNURLcash/derive"));
|
|
343
|
+
var numberOf2 = (bytes) => BigInt(`0x${bytesToHex(bytes)}`);
|
|
344
|
+
var to32Bytes2 = (value) => hexToBytes(value.toString(16).padStart(64, "0"));
|
|
345
|
+
var requireUint32 = (index) => {
|
|
346
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > 4294967295) {
|
|
347
|
+
throw new RangeError(`A note index must be a uint32, not ${index}.`);
|
|
348
|
+
}
|
|
349
|
+
return index;
|
|
350
|
+
};
|
|
351
|
+
var tweakFor = (pubkeyXOnly, chainCode, index) => {
|
|
352
|
+
if (pubkeyXOnly.length !== 32 || chainCode.length !== 32) {
|
|
353
|
+
throw new RangeError("A branch is a 32-byte x-only public key and a 32-byte chain code.");
|
|
354
|
+
}
|
|
355
|
+
const i = requireUint32(index);
|
|
356
|
+
const ser = new Uint8Array([i >>> 24 & 255, i >>> 16 & 255, i >>> 8 & 255, i & 255]);
|
|
357
|
+
const t = numberOf2(
|
|
358
|
+
sha256(new Uint8Array([...NOTE_DERIVE_TAG, ...NOTE_DERIVE_TAG, ...pubkeyXOnly, ...chainCode, ...ser]))
|
|
359
|
+
);
|
|
360
|
+
if (t >= CURVE_N2) {
|
|
361
|
+
throw new Error(`Note index ${index} is unusable on this branch. Use the next index.`);
|
|
362
|
+
}
|
|
363
|
+
return t;
|
|
364
|
+
};
|
|
365
|
+
var deriveNotePubkey = (branchPubkeyXOnly, chainCode, index) => {
|
|
366
|
+
const t = tweakFor(branchPubkeyXOnly, chainCode, index);
|
|
367
|
+
const branch = secp256k1.Point.fromBytes(new Uint8Array([2, ...branchPubkeyXOnly]));
|
|
368
|
+
const note = t === 0n ? branch : branch.add(secp256k1.Point.BASE.multiply(t));
|
|
369
|
+
if (note.is0()) {
|
|
370
|
+
throw new Error(`Note index ${index} is unusable on this branch. Use the next index.`);
|
|
371
|
+
}
|
|
372
|
+
return note.toBytes(true).slice(1);
|
|
373
|
+
};
|
|
374
|
+
var deriveNoteSecretKey = (branchPrivateKey, chainCode, index) => {
|
|
375
|
+
const p = numberOf2(branchPrivateKey);
|
|
376
|
+
if (branchPrivateKey.length !== 32 || p === 0n || p >= CURVE_N2) {
|
|
377
|
+
throw new RangeError("A branch private key is a 32-byte scalar in [1, n).");
|
|
378
|
+
}
|
|
379
|
+
const branch = secp256k1.Point.BASE.multiply(p);
|
|
380
|
+
const t = tweakFor(branch.toBytes(true).slice(1), chainCode, index);
|
|
381
|
+
const even = branch.y % 2n === 0n ? p : CURVE_N2 - p;
|
|
382
|
+
const key = (even + t) % CURVE_N2;
|
|
383
|
+
if (key === 0n) {
|
|
384
|
+
throw new Error(`Note index ${index} is unusable on this branch. Use the next index.`);
|
|
385
|
+
}
|
|
386
|
+
return to32Bytes2(key);
|
|
387
|
+
};
|
|
388
|
+
var NOTE_OWNERSHIP_DIGEST = sha256(
|
|
389
|
+
sha256(new Uint8Array([...utf8ToBytes("Lightning Signed Message:"), ...utf8ToBytes("LNURLcash")]))
|
|
390
|
+
);
|
|
391
|
+
var signNoteOwnership = (secretKey) => {
|
|
392
|
+
const signature = secp256k1.sign(NOTE_OWNERSHIP_DIGEST, secretKey, {
|
|
393
|
+
format: "recovered",
|
|
394
|
+
prehash: false
|
|
395
|
+
});
|
|
396
|
+
return new Uint8Array([...signature.subarray(1), signature[0]]);
|
|
397
|
+
};
|
|
398
|
+
var recoverNoteOwnershipPubkey = (signature) => {
|
|
399
|
+
if (!(signature instanceof Uint8Array) || signature.length !== 65) return null;
|
|
400
|
+
try {
|
|
401
|
+
const recoveryIdFirst = new Uint8Array([signature[64], ...signature.subarray(0, 64)]);
|
|
402
|
+
return secp256k1.recoverPublicKey(recoveryIdFirst, NOTE_OWNERSHIP_DIGEST, { prehash: false }).slice(1);
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var deriveCashAddressNode = (root, host) => deriveCashDomainNode(deriveCashChild(root, 1 + 2147483648), host);
|
|
408
|
+
var cashNodeToCx1 = (node) => ({
|
|
409
|
+
pubkeyXOnly: secp256k1.getPublicKey(node.privateKey, true).slice(1),
|
|
410
|
+
chainCode: node.chainCode.slice()
|
|
411
|
+
});
|
|
304
412
|
|
|
305
413
|
// src/errors.ts
|
|
306
414
|
var LnurlcashError = class extends Error {
|
|
@@ -756,22 +864,35 @@ var noteSignatureDigestForHash = (h, amountMsat) => sha256(
|
|
|
756
864
|
);
|
|
757
865
|
var NO_MATCH = { valid: false, pubkey: null };
|
|
758
866
|
var verifyNoteSignatureAgainst = (k1, amountMsat, signatureHex, mintPubkeys) => {
|
|
759
|
-
|
|
867
|
+
const h = signedNoteId(k1);
|
|
868
|
+
if (h === null) return NO_MATCH;
|
|
869
|
+
return verifyNoteSignatureHashAgainst(h, amountMsat, signatureHex, mintPubkeys);
|
|
870
|
+
};
|
|
871
|
+
var signedNoteId = (k1) => {
|
|
872
|
+
const ownership = decodeCk1(k1);
|
|
873
|
+
if (ownership) {
|
|
874
|
+
const pubkey = recoverNoteOwnershipPubkey(ownership);
|
|
875
|
+
return pubkey ? bytesToHex(pubkey) : null;
|
|
876
|
+
}
|
|
760
877
|
try {
|
|
761
|
-
|
|
878
|
+
return hashK1(k1);
|
|
762
879
|
} catch {
|
|
763
|
-
return
|
|
880
|
+
return null;
|
|
764
881
|
}
|
|
765
|
-
return verifyNoteSignatureHashAgainst(h, amountMsat, signatureHex, mintPubkeys);
|
|
766
882
|
};
|
|
767
883
|
var verifyNoteSignatureHashAgainst = (h, amountMsat, signatureHex, mintPubkeys) => {
|
|
768
884
|
const targets = (Array.isArray(mintPubkeys) ? mintPubkeys : [mintPubkeys]).filter((key) => typeof key === "string").map((key) => key.trim().toLowerCase());
|
|
769
885
|
if (targets.length === 0) return NO_MATCH;
|
|
770
886
|
let wireSig;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
887
|
+
const certificate = decodeCs1(signatureHex);
|
|
888
|
+
if (certificate) {
|
|
889
|
+
wireSig = certificate;
|
|
890
|
+
} else {
|
|
891
|
+
try {
|
|
892
|
+
wireSig = hexToBytes(signatureHex);
|
|
893
|
+
} catch {
|
|
894
|
+
return NO_MATCH;
|
|
895
|
+
}
|
|
775
896
|
}
|
|
776
897
|
if (wireSig.length !== 65) return NO_MATCH;
|
|
777
898
|
let digest;
|
|
@@ -1601,4 +1722,4 @@ var createClient = (options = {}) => ({
|
|
|
1601
1722
|
settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
|
|
1602
1723
|
});
|
|
1603
1724
|
|
|
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 };
|
|
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 };
|
package/llms.txt
CHANGED
|
@@ -48,6 +48,22 @@ 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
|
|
51
67
|
deriveNoteRoot(seedBytes) -> Uint8Array LEGACY, pre-spec. Do not mint under
|
|
52
68
|
deriveNoteSecret(root, host, index) -> k1 it; still scanned so old notes live.
|
|
53
69
|
derivedSecretSource(root, host, start) -> RandomSecret & {index()}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lnurlcash-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "LNURLcash (LUD-25) bearer note client for TypeScript - mint, rotate, split, merge, melt, and verify offline",
|
|
5
5
|
"author": "TheCryptoDonkey",
|
|
6
6
|
"license": "MIT",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/node": "^26.2.0",
|
|
63
|
-
"lnurlcash-conformance": "^0.
|
|
63
|
+
"lnurlcash-conformance": "^0.8.0",
|
|
64
64
|
"tsup": "^8.5.0",
|
|
65
65
|
"typescript": "^5.7.0",
|
|
66
66
|
"vitest": "^3.0.0"
|