lnurlcash-kit 0.2.0 → 0.3.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 +29 -0
- package/README.md +41 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +135 -47
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,35 @@
|
|
|
3
3
|
Semantic versioning. While the LUD-25 draft is unmerged, `0.x` minor bumps
|
|
4
4
|
may carry breaking changes; pin an exact version.
|
|
5
5
|
|
|
6
|
+
## 0.3.0 - 2026-08-24
|
|
7
|
+
|
|
8
|
+
- Additive bound-mint receipt parsing and validation for sealed signers:
|
|
9
|
+
`InvoiceResult.mint`, `VerifyResult.mint`, `requireBoundMintQuote` and
|
|
10
|
+
`validateBoundMintReceipt`. The quote must commit the requested `h` and
|
|
11
|
+
exact net amount before payment; the settled LUD-21 response must match the
|
|
12
|
+
invoice and commitment and carry a valid ordinary LUD-25 note signature.
|
|
13
|
+
- `noteSignatureMessageForHash`, `noteSignatureDigestForHash`,
|
|
14
|
+
`verifyNoteSignatureHash` and `verifyNoteSignatureHashAgainst` expose the
|
|
15
|
+
existing signature construction when a signer deliberately retains `k1`.
|
|
16
|
+
Existing secret-based helpers delegate to them unchanged.
|
|
17
|
+
|
|
18
|
+
## 0.2.1 - 2026-08-22
|
|
19
|
+
|
|
20
|
+
- `WithdrawRequestInfo` carries `payLink`, the way home a SERVICE may
|
|
21
|
+
publish on a note's informational GET. It is the reverse of the
|
|
22
|
+
`withdrawLink` a payRequest advertises, and it is the only route a
|
|
23
|
+
bearer-note wallet has to a mint's discovery document: the document lives
|
|
24
|
+
under a username the note never mentions and cannot be guessed from the
|
|
25
|
+
callback. Without it, a WALLET that has only ever received notes cannot
|
|
26
|
+
read the mint's `previousPubkeys`, so an announced key rotation is
|
|
27
|
+
indistinguishable from a substituted key and gets refused.
|
|
28
|
+
- A `payLink` on any origin but the note's own is dropped rather than
|
|
29
|
+
passed on, so a caller can treat its presence as the fact it looks like.
|
|
30
|
+
Whoever controls the host controls the pin anyway, which is TOFU's own
|
|
31
|
+
argument, but that argument does not stretch to letting a SERVICE
|
|
32
|
+
nominate a THIRD party to vouch for its key history, and refusing costs
|
|
33
|
+
nothing.
|
|
34
|
+
|
|
6
35
|
## 0.2.0 - 2026-08-22
|
|
7
36
|
|
|
8
37
|
### Deterministic note secrets and restore from a seed
|
package/README.md
CHANGED
|
@@ -358,6 +358,47 @@ is yours from the moment it exists. The claim GET does show the secret to the
|
|
|
358
358
|
mint it is a claim on, which is a different thing from showing it to whoever
|
|
359
359
|
scanned the QR, and you can still rotate if you want the offline signature.
|
|
360
360
|
|
|
361
|
+
### A sealed signer: confirm without exporting the secret
|
|
362
|
+
|
|
363
|
+
A hardware vault cannot use the claim GET above without handing `k1` to its
|
|
364
|
+
companion. A receipt-capable mint can instead commit the quote to the requested
|
|
365
|
+
`h` and exact net amount, then place the ordinary LUD-25 note signature on its
|
|
366
|
+
settled LUD-21 response. The extension is optional; absence means use the
|
|
367
|
+
unchanged preimage-import-and-rotate flow before showing an invoice.
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import {
|
|
371
|
+
requestInvoice, fetchInvoiceVerification,
|
|
372
|
+
requireBoundMintQuote, validateBoundMintReceipt
|
|
373
|
+
} from 'lnurlcash-kit'
|
|
374
|
+
|
|
375
|
+
const staged = await vault.newSecret() // {id, h}; k1 stays in the vault
|
|
376
|
+
const expectedNetMsat = 21_000
|
|
377
|
+
const quote = await requestInvoice(pay.callback, 21_000, {h: staged.h})
|
|
378
|
+
|
|
379
|
+
// Do this before displaying or paying quote.pr.
|
|
380
|
+
requireBoundMintQuote(quote, staged.h, expectedNetMsat)
|
|
381
|
+
if (!quote.verify) throw new Error('No settlement receipt offered')
|
|
382
|
+
|
|
383
|
+
// After payment, poll quote.verify until settled.
|
|
384
|
+
const verification = await fetchInvoiceVerification(quote.verify)
|
|
385
|
+
const receipt = validateBoundMintReceipt(
|
|
386
|
+
quote,
|
|
387
|
+
verification,
|
|
388
|
+
staged.h,
|
|
389
|
+
expectedNetMsat,
|
|
390
|
+
pinnedMintPubkeys
|
|
391
|
+
)
|
|
392
|
+
await vault.confirm(staged.id, receipt.amountMsat, mintHost, receipt.signature)
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`quote.mint` is `{h, amountMsat}` in the typed API (`amount` on the wire).
|
|
396
|
+
The settled response must repeat that commitment and add `signature` (`sig`
|
|
397
|
+
on the wire). Validation matches the invoice, output and amount, refuses a
|
|
398
|
+
pre-settlement signature, and recovers the signer against the pinned current
|
|
399
|
+
or previous mint keys. The payment preimage remains proof of payment; it never
|
|
400
|
+
replaces the vault's staged secret.
|
|
401
|
+
|
|
361
402
|
## Asking to be paid
|
|
362
403
|
|
|
363
404
|
"Send me 500 sat" today means handing over a Lightning Address, which is a
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ type WithdrawRequestInfo = {
|
|
|
39
39
|
maxWithdrawable: number;
|
|
40
40
|
defaultDescription?: string;
|
|
41
41
|
mintPubkey?: string;
|
|
42
|
+
payLink?: string;
|
|
42
43
|
};
|
|
43
44
|
declare const fetchNoteInfo: (url: string, options?: LnurlcashOptions) => Promise<WithdrawRequestInfo>;
|
|
44
45
|
declare const probeBurnedNote: (url: string, options?: LnurlcashOptions) => Promise<"live" | "gone" | "unknown">;
|
|
@@ -132,6 +133,15 @@ type InvoiceResult = {
|
|
|
132
133
|
verify?: string;
|
|
133
134
|
disposable: boolean;
|
|
134
135
|
mintToHash: boolean;
|
|
136
|
+
mint?: BoundMintCommitment;
|
|
137
|
+
};
|
|
138
|
+
type BoundMintCommitment = {
|
|
139
|
+
h: string;
|
|
140
|
+
amountMsat: number;
|
|
141
|
+
signature?: string;
|
|
142
|
+
};
|
|
143
|
+
type ValidatedBoundMintReceipt = Required<BoundMintCommitment> & {
|
|
144
|
+
pubkey: string;
|
|
135
145
|
};
|
|
136
146
|
type InvoiceRequestOptions = LnurlcashOptions & {
|
|
137
147
|
h?: string;
|
|
@@ -141,8 +151,11 @@ type VerifyResult = {
|
|
|
141
151
|
settled: boolean;
|
|
142
152
|
preimage: string | null;
|
|
143
153
|
pr: string;
|
|
154
|
+
mint?: BoundMintCommitment;
|
|
144
155
|
};
|
|
145
156
|
declare const fetchInvoiceVerification: (verifyUrl: string, options?: LnurlcashOptions) => Promise<VerifyResult>;
|
|
157
|
+
declare const requireBoundMintQuote: (invoice: InvoiceResult, expectedH: string, expectedAmountMsat: number) => BoundMintCommitment;
|
|
158
|
+
declare const validateBoundMintReceipt: (invoice: InvoiceResult, verification: VerifyResult, expectedH: string, expectedAmountMsat: number, mintPubkeys: string | string[]) => ValidatedBoundMintReceipt;
|
|
146
159
|
type MintClaim = {
|
|
147
160
|
state: 'minted' | 'unminted' | 'pending' | 'spent';
|
|
148
161
|
k1: string;
|
|
@@ -225,7 +238,9 @@ declare const decodePaymentRequest: (value: string, { now }?: DecodeOptions) =>
|
|
|
225
238
|
declare const isPaymentRequest: (value: string) => boolean;
|
|
226
239
|
|
|
227
240
|
declare const noteSignatureMessage: (k1: string, amountMsat: number) => string;
|
|
241
|
+
declare const noteSignatureMessageForHash: (h: string, amountMsat: number) => string;
|
|
228
242
|
declare const noteSignatureDigest: (k1: string, amountMsat: number) => Uint8Array;
|
|
243
|
+
declare const noteSignatureDigestForHash: (h: string, amountMsat: number) => Uint8Array;
|
|
229
244
|
type SignatureCheck = {
|
|
230
245
|
valid: true;
|
|
231
246
|
pubkey: string;
|
|
@@ -234,7 +249,9 @@ type SignatureCheck = {
|
|
|
234
249
|
pubkey: null;
|
|
235
250
|
};
|
|
236
251
|
declare const verifyNoteSignatureAgainst: (k1: string, amountMsat: number, signatureHex: string, mintPubkeys: string | string[]) => SignatureCheck;
|
|
252
|
+
declare const verifyNoteSignatureHashAgainst: (h: string, amountMsat: number, signatureHex: string, mintPubkeys: string | string[]) => SignatureCheck;
|
|
237
253
|
declare const verifyNoteSignature: (k1: string, amountMsat: number, signatureHex: string, mintPubkeys: string | string[]) => boolean;
|
|
254
|
+
declare const verifyNoteSignatureHash: (h: string, amountMsat: number, signatureHex: string, mintPubkeys: string | string[]) => boolean;
|
|
238
255
|
|
|
239
256
|
declare const isBolt11Invoice: (value: string) => boolean;
|
|
240
257
|
declare const sameInvoice: (a: string, b: string) => boolean;
|
|
@@ -296,4 +313,4 @@ declare const createClient: (options?: LnurlcashOptions) => {
|
|
|
296
313
|
};
|
|
297
314
|
type LnurlcashClient = ReturnType<typeof createClient>;
|
|
298
315
|
|
|
299
|
-
export { AmbiguousMintError, AmbiguousMutationError, type DecodeOptions, 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, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, type PayRequestInfo, type PaymentRequest, type PaymentRequestMethodDetails, PendingNoteError, ProtocolError, type RandomSecret, RequestRefusedError, type RestoreOptions, type RestoreResult, type RestoredNote, type RotateResult, ServiceRejectedError, type SettleForValueOptions, type SettledForValue, type SettledNote, type SignatureCheck, type SplitResult, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureMessage, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, verifyNoteSignature, verifyNoteSignatureAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
|
316
|
+
export { AmbiguousMintError, AmbiguousMutationError, type BoundMintCommitment, type DecodeOptions, 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, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, type PayRequestInfo, type PaymentRequest, type PaymentRequestMethodDetails, PendingNoteError, ProtocolError, type RandomSecret, RequestRefusedError, type RestoreOptions, type RestoreResult, type RestoredNote, type RotateResult, ServiceRejectedError, type SettleForValueOptions, type SettledForValue, type SettledNote, type SignatureCheck, type SplitResult, type ValidatedBoundMintReceipt, type VerifyResult, type WithdrawRequestInfo, type WithdrawSuccessResponse, applyMintFee, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
package/dist/index.js
CHANGED
|
@@ -627,6 +627,62 @@ var describeMintFee = (fee) => [
|
|
|
627
627
|
fee.baseFeeMsat > 0 ? `${Math.round(fee.baseFeeMsat / 1e3)} sat flat` : null,
|
|
628
628
|
fee.feePpm > 0 ? `${formatFeePercent(fee.feePpm)}% of the amount paid` : null
|
|
629
629
|
].filter(Boolean).join(" + ");
|
|
630
|
+
var LIGHTNING_SIGNED_MESSAGE_PREFIX = utf8ToBytes("Lightning Signed Message:");
|
|
631
|
+
var noteSignatureMessage = (k1, amountMsat) => noteSignatureMessageForHash(hashK1(k1), amountMsat);
|
|
632
|
+
var noteSignatureMessageForHash = (h, amountMsat) => `LNURLcash:${amountMsat}:${h.trim().toLowerCase()}`;
|
|
633
|
+
var noteSignatureDigest = (k1, amountMsat) => noteSignatureDigestForHash(hashK1(k1), amountMsat);
|
|
634
|
+
var noteSignatureDigestForHash = (h, amountMsat) => sha256(
|
|
635
|
+
sha256(
|
|
636
|
+
new Uint8Array([
|
|
637
|
+
...LIGHTNING_SIGNED_MESSAGE_PREFIX,
|
|
638
|
+
...utf8ToBytes(noteSignatureMessageForHash(h, amountMsat))
|
|
639
|
+
])
|
|
640
|
+
)
|
|
641
|
+
);
|
|
642
|
+
var NO_MATCH = { valid: false, pubkey: null };
|
|
643
|
+
var verifyNoteSignatureAgainst = (k1, amountMsat, signatureHex, mintPubkeys) => {
|
|
644
|
+
let h;
|
|
645
|
+
try {
|
|
646
|
+
h = hashK1(k1);
|
|
647
|
+
} catch {
|
|
648
|
+
return NO_MATCH;
|
|
649
|
+
}
|
|
650
|
+
return verifyNoteSignatureHashAgainst(h, amountMsat, signatureHex, mintPubkeys);
|
|
651
|
+
};
|
|
652
|
+
var verifyNoteSignatureHashAgainst = (h, amountMsat, signatureHex, mintPubkeys) => {
|
|
653
|
+
const targets = (Array.isArray(mintPubkeys) ? mintPubkeys : [mintPubkeys]).filter((key) => typeof key === "string").map((key) => key.trim().toLowerCase());
|
|
654
|
+
if (targets.length === 0) return NO_MATCH;
|
|
655
|
+
let wireSig;
|
|
656
|
+
try {
|
|
657
|
+
wireSig = hexToBytes(signatureHex);
|
|
658
|
+
} catch {
|
|
659
|
+
return NO_MATCH;
|
|
660
|
+
}
|
|
661
|
+
if (wireSig.length !== 65) return NO_MATCH;
|
|
662
|
+
let digest;
|
|
663
|
+
try {
|
|
664
|
+
if (!/^[0-9a-fA-F]{64}$/.test(h.trim())) return NO_MATCH;
|
|
665
|
+
digest = noteSignatureDigestForHash(h, amountMsat);
|
|
666
|
+
} catch {
|
|
667
|
+
return NO_MATCH;
|
|
668
|
+
}
|
|
669
|
+
const recoveryIdFirst = new Uint8Array([
|
|
670
|
+
wireSig[64],
|
|
671
|
+
...wireSig.subarray(0, 64)
|
|
672
|
+
]);
|
|
673
|
+
for (const candidate of [recoveryIdFirst, wireSig]) {
|
|
674
|
+
try {
|
|
675
|
+
const recovered = bytesToHex(
|
|
676
|
+
secp256k1.recoverPublicKey(candidate, digest, { prehash: false })
|
|
677
|
+
);
|
|
678
|
+
if (targets.includes(recovered)) return { valid: true, pubkey: recovered };
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return NO_MATCH;
|
|
683
|
+
};
|
|
684
|
+
var verifyNoteSignature = (k1, amountMsat, signatureHex, mintPubkeys) => verifyNoteSignatureAgainst(k1, amountMsat, signatureHex, mintPubkeys).valid;
|
|
685
|
+
var verifyNoteSignatureHash = (h, amountMsat, signatureHex, mintPubkeys) => verifyNoteSignatureHashAgainst(h, amountMsat, signatureHex, mintPubkeys).valid;
|
|
630
686
|
|
|
631
687
|
// src/client.ts
|
|
632
688
|
var fetchNoteInfo = async (url, options = {}) => {
|
|
@@ -649,7 +705,22 @@ var fetchNoteInfo = async (url, options = {}) => {
|
|
|
649
705
|
"The service echoed back a different k1 than was queried - the note may have been redeemed elsewhere, or the service isn't spec-compliant."
|
|
650
706
|
);
|
|
651
707
|
}
|
|
652
|
-
|
|
708
|
+
const info = body;
|
|
709
|
+
const payLink = sameOriginPayLink(body.payLink, reqUrl);
|
|
710
|
+
if (payLink === void 0) delete info.payLink;
|
|
711
|
+
else info.payLink = payLink;
|
|
712
|
+
return info;
|
|
713
|
+
};
|
|
714
|
+
var sameOriginPayLink = (value, noteUrl) => {
|
|
715
|
+
if (typeof value !== "string" || value.length === 0) return void 0;
|
|
716
|
+
let candidate;
|
|
717
|
+
try {
|
|
718
|
+
candidate = new URL(value, noteUrl);
|
|
719
|
+
} catch {
|
|
720
|
+
return void 0;
|
|
721
|
+
}
|
|
722
|
+
if (candidate.origin !== noteUrl.origin) return void 0;
|
|
723
|
+
return candidate.toString();
|
|
653
724
|
};
|
|
654
725
|
var probeBurnedNote = async (url, options = {}) => {
|
|
655
726
|
try {
|
|
@@ -890,6 +961,18 @@ var fetchPayRequest = async (url, options = {}) => {
|
|
|
890
961
|
mintToHash: asBoolean(body.mintToHash)
|
|
891
962
|
};
|
|
892
963
|
};
|
|
964
|
+
var asBoundMintCommitment = (value) => {
|
|
965
|
+
if (!value || typeof value !== "object") return void 0;
|
|
966
|
+
const raw = value;
|
|
967
|
+
if (typeof raw.h !== "string" || !/^[0-9a-fA-F]{64}$/.test(raw.h) || typeof raw.amount !== "number" || !Number.isSafeInteger(raw.amount) || raw.amount <= 0 || raw.sig !== void 0 && typeof raw.sig !== "string") {
|
|
968
|
+
return void 0;
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
h: raw.h.toLowerCase(),
|
|
972
|
+
amountMsat: raw.amount,
|
|
973
|
+
...typeof raw.sig === "string" ? { signature: raw.sig } : {}
|
|
974
|
+
};
|
|
975
|
+
};
|
|
893
976
|
var requestInvoice = async (payCallback, amountMsat, options = {}) => {
|
|
894
977
|
const cbUrl = new URL(payCallback);
|
|
895
978
|
cbUrl.searchParams.set("amount", String(amountMsat));
|
|
@@ -915,7 +998,8 @@ var requestInvoice = async (payCallback, amountMsat, options = {}) => {
|
|
|
915
998
|
pr: body.pr,
|
|
916
999
|
verify: typeof body.verify === "string" ? body.verify : void 0,
|
|
917
1000
|
disposable: body.disposable !== false,
|
|
918
|
-
mintToHash: body.mintToHash === true
|
|
1001
|
+
mintToHash: body.mintToHash === true,
|
|
1002
|
+
mint: asBoundMintCommitment(body.mint)
|
|
919
1003
|
};
|
|
920
1004
|
};
|
|
921
1005
|
var fetchInvoiceVerification = async (verifyUrl, options = {}) => {
|
|
@@ -926,9 +1010,56 @@ var fetchInvoiceVerification = async (verifyUrl, options = {}) => {
|
|
|
926
1010
|
return {
|
|
927
1011
|
settled: body.settled,
|
|
928
1012
|
preimage: typeof body.preimage === "string" ? body.preimage : null,
|
|
929
|
-
pr: body.pr
|
|
1013
|
+
pr: body.pr,
|
|
1014
|
+
mint: asBoundMintCommitment(body.mint)
|
|
930
1015
|
};
|
|
931
1016
|
};
|
|
1017
|
+
var requireBoundMintQuote = (invoice, expectedH, expectedAmountMsat) => {
|
|
1018
|
+
const h = expectedH.trim().toLowerCase();
|
|
1019
|
+
if (!isPreimage(h)) throw new ProtocolError("The expected mint output hash is malformed.");
|
|
1020
|
+
if (!Number.isSafeInteger(expectedAmountMsat) || expectedAmountMsat <= 0) {
|
|
1021
|
+
throw new ProtocolError("The expected mint amount must be positive integer millisatoshis.");
|
|
1022
|
+
}
|
|
1023
|
+
if (!invoice.mintToHash || !invoice.mint) {
|
|
1024
|
+
throw new ProtocolError("The service did not commit this quote to a bound mint output.");
|
|
1025
|
+
}
|
|
1026
|
+
if (invoice.mint.h !== h) {
|
|
1027
|
+
throw new ProtocolError("The service committed the quote to a different mint output.");
|
|
1028
|
+
}
|
|
1029
|
+
if (invoice.mint.amountMsat !== expectedAmountMsat) {
|
|
1030
|
+
throw new ProtocolError("The service committed the quote to a different mint amount.");
|
|
1031
|
+
}
|
|
1032
|
+
if (invoice.mint.signature !== void 0) {
|
|
1033
|
+
throw new ProtocolError("The service signed a mint output before the invoice settled.");
|
|
1034
|
+
}
|
|
1035
|
+
return invoice.mint;
|
|
1036
|
+
};
|
|
1037
|
+
var validateBoundMintReceipt = (invoice, verification, expectedH, expectedAmountMsat, mintPubkeys) => {
|
|
1038
|
+
const quote = requireBoundMintQuote(invoice, expectedH, expectedAmountMsat);
|
|
1039
|
+
if (!verification.settled) {
|
|
1040
|
+
throw new ProtocolError("The invoice has not settled.");
|
|
1041
|
+
}
|
|
1042
|
+
if (!sameInvoice(invoice.pr, verification.pr)) {
|
|
1043
|
+
throw new ProtocolError("The settlement receipt names a different invoice.");
|
|
1044
|
+
}
|
|
1045
|
+
const receipt = verification.mint;
|
|
1046
|
+
if (!receipt || receipt.h !== quote.h || receipt.amountMsat !== quote.amountMsat) {
|
|
1047
|
+
throw new ProtocolError("The settlement receipt does not match the mint quote.");
|
|
1048
|
+
}
|
|
1049
|
+
if (!receipt.signature) {
|
|
1050
|
+
throw new ProtocolError("The settled mint receipt has no note signature.");
|
|
1051
|
+
}
|
|
1052
|
+
const checked = verifyNoteSignatureHashAgainst(
|
|
1053
|
+
receipt.h,
|
|
1054
|
+
receipt.amountMsat,
|
|
1055
|
+
receipt.signature,
|
|
1056
|
+
mintPubkeys
|
|
1057
|
+
);
|
|
1058
|
+
if (!checked.valid) {
|
|
1059
|
+
throw new ProtocolError("The settled mint receipt has an invalid note signature.");
|
|
1060
|
+
}
|
|
1061
|
+
return { ...receipt, signature: receipt.signature, pubkey: checked.pubkey };
|
|
1062
|
+
};
|
|
932
1063
|
var claimMintedNote = async (withdrawLink, k1, options = {}) => {
|
|
933
1064
|
const secret = k1.trim().toLowerCase();
|
|
934
1065
|
if (!isPreimage(secret)) {
|
|
@@ -956,49 +1087,6 @@ var claimMintedNote = async (withdrawLink, k1, options = {}) => {
|
|
|
956
1087
|
throw err;
|
|
957
1088
|
}
|
|
958
1089
|
};
|
|
959
|
-
var LIGHTNING_SIGNED_MESSAGE_PREFIX = utf8ToBytes("Lightning Signed Message:");
|
|
960
|
-
var noteSignatureMessage = (k1, amountMsat) => `LNURLcash:${amountMsat}:${hashK1(k1)}`;
|
|
961
|
-
var noteSignatureDigest = (k1, amountMsat) => sha256(
|
|
962
|
-
sha256(
|
|
963
|
-
new Uint8Array([
|
|
964
|
-
...LIGHTNING_SIGNED_MESSAGE_PREFIX,
|
|
965
|
-
...utf8ToBytes(noteSignatureMessage(k1, amountMsat))
|
|
966
|
-
])
|
|
967
|
-
)
|
|
968
|
-
);
|
|
969
|
-
var NO_MATCH = { valid: false, pubkey: null };
|
|
970
|
-
var verifyNoteSignatureAgainst = (k1, amountMsat, signatureHex, mintPubkeys) => {
|
|
971
|
-
const targets = (Array.isArray(mintPubkeys) ? mintPubkeys : [mintPubkeys]).filter((key) => typeof key === "string").map((key) => key.trim().toLowerCase());
|
|
972
|
-
if (targets.length === 0) return NO_MATCH;
|
|
973
|
-
let wireSig;
|
|
974
|
-
try {
|
|
975
|
-
wireSig = hexToBytes(signatureHex);
|
|
976
|
-
} catch {
|
|
977
|
-
return NO_MATCH;
|
|
978
|
-
}
|
|
979
|
-
if (wireSig.length !== 65) return NO_MATCH;
|
|
980
|
-
let digest;
|
|
981
|
-
try {
|
|
982
|
-
digest = noteSignatureDigest(k1, amountMsat);
|
|
983
|
-
} catch {
|
|
984
|
-
return NO_MATCH;
|
|
985
|
-
}
|
|
986
|
-
const recoveryIdFirst = new Uint8Array([
|
|
987
|
-
wireSig[64],
|
|
988
|
-
...wireSig.subarray(0, 64)
|
|
989
|
-
]);
|
|
990
|
-
for (const candidate of [recoveryIdFirst, wireSig]) {
|
|
991
|
-
try {
|
|
992
|
-
const recovered = bytesToHex(
|
|
993
|
-
secp256k1.recoverPublicKey(candidate, digest, { prehash: false })
|
|
994
|
-
);
|
|
995
|
-
if (targets.includes(recovered)) return { valid: true, pubkey: recovered };
|
|
996
|
-
} catch {
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return NO_MATCH;
|
|
1000
|
-
};
|
|
1001
|
-
var verifyNoteSignature = (k1, amountMsat, signatureHex, mintPubkeys) => verifyNoteSignatureAgainst(k1, amountMsat, signatureHex, mintPubkeys).valid;
|
|
1002
1090
|
|
|
1003
1091
|
// src/settle.ts
|
|
1004
1092
|
var normaliseHost = (value) => serverOf(value.trim().replace(/^@/, "")).toLowerCase();
|
|
@@ -1104,4 +1192,4 @@ var createClient = (options = {}) => ({
|
|
|
1104
1192
|
settleNoteForValue: (noteUrl, terms) => settleNoteForValue(noteUrl, terms, options)
|
|
1105
1193
|
});
|
|
1106
1194
|
|
|
1107
|
-
export { AmbiguousMintError, AmbiguousMutationError, InsufficientValueError, LnurlcashError, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, PendingNoteError, ProtocolError, RequestRefusedError, ServiceRejectedError, applyMintFee, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureMessage, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, verifyNoteSignature, verifyNoteSignatureAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
|
1195
|
+
export { AmbiguousMintError, AmbiguousMutationError, InsufficientValueError, LnurlcashError, NoteSpentError, NoteUnknownError, PAYMENT_REQUEST_PREFIX, PendingNoteError, ProtocolError, RequestRefusedError, ServiceRejectedError, applyMintFee, buildNoteUrl, claimMintedNote, classifyNoteError, createClient, decodeBolt11AmountMsat, decodePaymentRequest, defaultRandomSecret, deriveNoteRoot, deriveNoteSecret, derivedSecretSource, describeMintFee, encodePaymentRequest, fetchInvoiceVerification, fetchMintAddress, fetchNoteInfo, fetchPayRequest, formatFeePercent, fromBech32Lnurl, fromLud17, grossUpForMintFee, hashK1, isAllowedServiceUrl, isBech32Lnurl, isBolt11Invoice, isLightningAddress, isPaymentRequest, isPreimage, isValidNoteInput, lightningAddressUsername, meltNote, mergeNotes, mergeNotesWithHash, mintAddressUrl, mintFeeBand, newSecretsOf, noteDeclaredAmount, noteK1, noteSignature, noteSignatureDigest, noteSignatureDigestForHash, noteSignatureMessage, noteSignatureMessageForHash, parseMintFee, paymentRequestAmountMsat, probeBurnedNote, requestInvoice, requireBoundMintQuote, requireNoteK1, resolveLnurlInput, resolveMintInput, resolveNoteInput, restoreNotes, rotateNote, rotateNoteWithHash, sameInvoice, serverOf, settleNote, settleNoteForValue, splitNote, splitNoteWithHash, toBech32Lnurl, toLud17w, validateBoundMintReceipt, verifyNoteSignature, verifyNoteSignatureAgainst, verifyNoteSignatureHash, verifyNoteSignatureHashAgainst, withNewK1, withinMintFeeBand, withoutK1 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lnurlcash-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.3.0",
|
|
64
64
|
"tsup": "^8.5.0",
|
|
65
65
|
"typescript": "^5.7.0",
|
|
66
66
|
"vitest": "^3.0.0"
|