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 +31 -0
- package/README.md +18 -4
- package/dist/index.d.ts +7 -1
- package/dist/index.js +152 -127
- package/llms.txt +9 -0
- package/package.json +1 -1
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
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,
|
|
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 =
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1162
|
-
|
|
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]),
|
|
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
|
-
|
|
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
|
|
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