diff --git a/docs-src/usage/payment_requests.md b/docs-src/usage/payment_requests.md index 2c695317e..7f5497306 100644 --- a/docs-src/usage/payment_requests.md +++ b/docs-src/usage/payment_requests.md @@ -107,7 +107,7 @@ const request = new PaymentRequest({ ### Receive the payload -Payloads arrive as raw text from the payer's wallet, so parse with `decodePayload` rather than `JSON.parse` (which silently corrupts amounts above 2^53). It validates the shape and normalizes proof amounts to `bigint`; matching the payload to your request is your job: +Payloads arrive as raw text from the payer's wallet, so parse with `decodePayload` rather than `JSON.parse` (which silently corrupts amounts above 2^53). It validates the shape and normalizes proof amounts to `Amount`, so the returned proofs are ready to use; matching the payload to your request is your job: ```typescript import { PaymentRequest, type PaymentRequestPayload } from '@cashu/cashu-ts'; diff --git a/etc/cashu-ts.api.md b/etc/cashu-ts.api.md index 31d21acb0..3ebebda1c 100644 --- a/etc/cashu-ts.api.md +++ b/etc/cashu-ts.api.md @@ -888,6 +888,7 @@ export type MeltProofsConfig = { keysetId?: string; privkey?: string | string[]; onCountersReserved?: OnCountersReserved; + nut08Change?: boolean; }; // @public @@ -1392,7 +1393,7 @@ export function normalizeSecpPubkey(pk: string): string; export type NUT10Option = { kind: string; data: string; - tags: string[][]; + tags?: string[][]; }; // @public (undocumented) @@ -1789,11 +1790,6 @@ export type PostRestoreResponse = { signatures: SerializedBlindedSignature[]; }; -// @public (undocumented) -export type PrepareMeltConfig = MeltProofsConfig & { - nut08Change?: boolean; -}; - // @public export type PrivKey = Uint8Array | string; @@ -1843,7 +1839,7 @@ export type RawMintKeys = { export type RawNUT10Option = { k: string; d: string; - t: string[][]; + t?: string[][]; }; // @public (undocumented) @@ -2333,7 +2329,9 @@ export class Wallet { completeMint(mintPreview: MintPreview>): Promise; completeSwap(swapPreview: SwapPreview, privkey?: string | string[]): Promise; readonly counters: WalletCounters; - createLockedMintQuote(amount: AmountLike, pubkey: string, description?: string): Promise; + createLockedMintQuote(amount: AmountLike, pubkey: string, description?: string): Promise; createMeltChangeProofs(outputData: OutputDataLike[], changeSigs: SerializedBlindedSignature[]): Proof[]; createMeltQuote(method: string, payload: Record, options?: { normalize?: (raw: Record) => TRes; @@ -2389,7 +2387,7 @@ export class Wallet { amount: AmountLike; quote: TQuote; }>, config?: MintProofsConfig, outputType?: OutputType): Promise>; - prepareMelt>(method: string, meltQuote: TQuote, proofsToSend: ProofLike[], config?: PrepareMeltConfig, outputType?: OutputType): Promise>; + prepareMelt>(method: string, meltQuote: TQuote, proofsToSend: ProofLike[], config?: MeltProofsConfig, outputType?: OutputType): Promise>; prepareMint>(method: string, amount: AmountLike, quote: TQuote, config?: MintProofsConfig, outputType?: OutputType): Promise>; prepareSwapToReceive(token: Token | string | ProofLike[], config?: ReceiveConfig, outputType?: OutputType): Promise; prepareSwapToSend(amount: AmountLike, proofs: ProofLike[], config?: SendConfig, outputConfig?: OutputConfig): Promise; diff --git a/migration-5.0.0.md b/migration-5.0.0.md index 6d2d99037..3bf69b369 100644 --- a/migration-5.0.0.md +++ b/migration-5.0.0.md @@ -309,6 +309,48 @@ Calls that already pass a `CompleteMeltOptions` object (or omit the third argume --- +## `PrepareMeltConfig` removed; `nut08Change` moved to `MeltProofsConfig` + +`PrepareMeltConfig` is removed. Its only distinction was `nut08Change`, which now lives on `MeltProofsConfig`, so every melt method takes the same config type. This also fixes the option being unusable through `meltProofs`, `meltProofsBolt11`, `meltProofsBolt12` and `meltProofsOnchain`: they forward their config to `prepareMelt`, so `nut08Change` worked at runtime but did not type check. In v4 the type remains as a deprecated alias of `MeltProofsConfig`. + +### Migration + +```ts +// Before +import { type PrepareMeltConfig } from '@cashu/cashu-ts'; +const config: PrepareMeltConfig = { nut08Change: false }; + +// After +import { type MeltProofsConfig } from '@cashu/cashu-ts'; +const config: MeltProofsConfig = { nut08Change: false }; +``` + +Only the type name changes; the object shape is identical, so untyped call sites need no change. + +--- + +## `NUT10Option.tags` is optional + +`NUT10Option.tags` and `RawNUT10Option.t` are now optional, matching NUT-10 and NUT-18, which both describe tags as optional. Constructing a payment request lock no longer needs a placeholder `tags: []`. Code that reads the field must handle `undefined`, which was already possible at runtime: decoding a request that omits tags produced one. + +### Migration + +```ts +// Before +if (request.nut10.tags.length > 0) { + /* … */ +} + +// After +if (request.nut10.tags?.length) { + /* … */ +} +``` + +Absent and empty are equivalent, and neither is encoded. + +--- + ## `P2PKOptions` is now a `kind` + `data` spending condition `P2PKOptions` drops `pubkey: string | string[]` and `hashlock?`. It is now the NUT-10 envelope (`kind` + `data`) plus the shared NUT-11 `LockConditions` tags: diff --git a/src/model/PaymentRequest.ts b/src/model/PaymentRequest.ts index dd3108429..5417af5c8 100644 --- a/src/model/PaymentRequest.ts +++ b/src/model/PaymentRequest.ts @@ -252,7 +252,7 @@ export class PaymentRequest { if (typeof amount !== 'number' && typeof amount !== 'bigint') { throw new CTSError(`invalid payment payload: malformed proof amount at index ${i}`); } - return { ...p, amount: Amount.from(amount).toBigInt() } as unknown as Proof; + return { ...(p as Omit), amount: Amount.from(amount) }; }); return { ...(id !== undefined && { id }), diff --git a/src/model/types/NUT23.ts b/src/model/types/NUT23.ts index f6fc95eab..bc8c81506 100644 --- a/src/model/types/NUT23.ts +++ b/src/model/types/NUT23.ts @@ -30,8 +30,12 @@ export type MintQuoteBolt11Response = MintQuoteBaseResponse & { */ amount: Amount; /** - * State of the mint quote. Deprecated in NUT-04 in favour of the accounting fields; cashu-ts - * always populates it for bolt11. + * State of the mint quote. cashu-ts always populates it for bolt11, deriving it from the + * accounting fields when the mint omits it. + * + * @deprecated Deprecated in NUT-04 in favour of `amount_paid` / `amount_issued`; the mintable + * amount is `amount_paid - amount_issued`. Retained for backwards compatibility and slated for + * removal in a future major. */ state: MintQuoteState; }; diff --git a/src/wallet/Wallet.ts b/src/wallet/Wallet.ts index ccf5bc65d..90d1d18cb 100644 --- a/src/wallet/Wallet.ts +++ b/src/wallet/Wallet.ts @@ -88,7 +88,6 @@ import { type ReceiveConfig, type MintProofsConfig, type MeltProofsConfig, - type PrepareMeltConfig, type CompleteMeltOptions, type SwapTransaction, type MeltProofsResponse, @@ -1990,7 +1989,7 @@ class Wallet { amount: AmountLike, pubkey: string, description?: string, - ): Promise { + ): Promise { this.requireSupport('mint', 'bolt11'); this.requireMintableKeyset('createLockedMintQuote'); this.failIf(typeof pubkey !== 'string', 'A pubkey is required to lock the mint quote'); @@ -3188,7 +3187,7 @@ class Wallet { method: string, meltQuote: TQuote, proofsToSend: ProofLike[], - config?: PrepareMeltConfig, + config?: MeltProofsConfig, outputType?: OutputType, ): Promise> { this.validateMeltQuote(meltQuote); diff --git a/src/wallet/types/config.ts b/src/wallet/types/config.ts index 272491043..1cbf8988e 100644 --- a/src/wallet/types/config.ts +++ b/src/wallet/types/config.ts @@ -191,9 +191,10 @@ export type MeltProofsConfig = { keysetId?: string; privkey?: string | string[]; onCountersReserved?: OnCountersReserved; -}; - -export type PrepareMeltConfig = MeltProofsConfig & { + /** + * Request NUT-08 blank outputs so the mint can return unspent fee reserve. Defaults to true. Set + * false to forfeit the change, which also permits melting on an inactive keyset. + */ nut08Change?: boolean; }; diff --git a/src/wallet/types/payment-requests.ts b/src/wallet/types/payment-requests.ts index 266b02ef0..8bdebb0b3 100644 --- a/src/wallet/types/payment-requests.ts +++ b/src/wallet/types/payment-requests.ts @@ -10,7 +10,7 @@ export type RawTransport = { export type RawNUT10Option = { k: string; // kind d: string; // data - t: string[][]; // tags + t?: string[][]; // tags (optional per NUT-18) }; export type RawSupportedMethod = { @@ -73,7 +73,8 @@ export type NUT10Option = { */ data: string; /** - * Tags associated with the spending condition for additional data. + * Optional tags associated with the spending condition for additional data. Absent and empty are + * equivalent: neither is encoded. */ - tags: string[][]; + tags?: string[][]; }; diff --git a/test/wallet/WalletOps.test.ts b/test/wallet/WalletOps.test.ts index db30d3746..be22c6940 100644 --- a/test/wallet/WalletOps.test.ts +++ b/test/wallet/WalletOps.test.ts @@ -380,7 +380,7 @@ describe('WalletOps builders', () => { const locked = new PaymentRequest({ amount: 100, unit: 'sat', - nut10: { kind: 'P2PK', data: '02'.padEnd(66, 'a'), tags: [] }, + nut10: { kind: 'P2PK', data: '02'.padEnd(66, 'a') }, }); await ops.sendToRequest(locked, proofs).run(); const outputConfig = wallet.send.mock.calls[0][3]; @@ -389,7 +389,7 @@ describe('WalletOps builders', () => { const exotic = new PaymentRequest({ amount: 100, unit: 'sat', - nut10: { kind: 'FROST', data: 'xyz', tags: [] }, + nut10: { kind: 'FROST', data: 'xyz' }, }); expect(() => ops.sendToRequest(exotic, proofs)).toThrow(/nut10 lock/); }); diff --git a/test/wallet/paymentRequests.test.ts b/test/wallet/paymentRequests.test.ts index 6a9d60a0a..4dda15a9d 100644 --- a/test/wallet/paymentRequests.test.ts +++ b/test/wallet/paymentRequests.test.ts @@ -731,7 +731,7 @@ describe('NUT-18 payment payloads', () => { expect(payload.mint).toBe(MINT); expect(payload.memo).toBe('hi'); // BigInt-safe: an amount beyond 2^53 survives exactly. - expect(payload.proofs[0].amount).toBe(9007199254740993n); + expect(payload.proofs[0].amount.toBigInt()).toBe(9007199254740993n); }); test('omits id and memo when absent and defaults the unit', () => { @@ -770,9 +770,10 @@ describe('NUT-18 payment payloads', () => { proofs: [{ id: '009a1f293253e41e', amount: 2, secret: 's', C: '02ff' }], }); - test('normalizes small JSON number amounts to bigint', () => { + test('normalizes small JSON number amounts to Amount', () => { const payload = PaymentRequest.decodePayload(JSON.stringify(valid())); - expect(payload.proofs[0].amount).toBe(2n); + expect(payload.proofs[0].amount).toBeInstanceOf(Amount); + expect(payload.proofs[0].amount.toBigInt()).toBe(2n); }); test('preserves unknown proof fields (witness, dleq)', () => { diff --git a/test/wallet/wallet-quotes-mutants.node.test.ts b/test/wallet/wallet-quotes-mutants.node.test.ts index 380e3344a..fea6f51fa 100644 --- a/test/wallet/wallet-quotes-mutants.node.test.ts +++ b/test/wallet/wallet-quotes-mutants.node.test.ts @@ -16,7 +16,6 @@ import { type MintQuoteBolt12Response, type MeltQuoteBolt11Response, type MintQuoteBaseResponse, - type PrepareMeltConfig, } from '../../src'; import { @@ -170,11 +169,7 @@ describe('_prepareInputsForMint mutants', () => { }, ] as unknown as Proof[]; - // meltProofsBolt11 declares MeltProofsConfig but forwards the config to prepareMelt, which - // is what honours nut08Change. - await wallet.meltProofsBolt11(meltQuote, proofsToSend, { - nut08Change: false, - } as PrepareMeltConfig); + await wallet.meltProofsBolt11(meltQuote, proofsToSend, { nut08Change: false }); expect(sentInputs).toHaveLength(1); expect(sentInputs[0]).not.toHaveProperty('dleq'); @@ -1069,7 +1064,7 @@ describe('createLockedMintQuote mutants', () => { await wallet.loadMint(); const quote = await wallet.createLockedMintQuote(100, PUBKEY); - expect(quote.pubkey!.toLowerCase()).toBe(PUBKEY); + expect(quote.pubkey.toLowerCase()).toBe(PUBKEY); }); test('rejects a missing pubkey with a clear error, not a TypeError', async () => {