Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-src/usage/payment_requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
16 changes: 7 additions & 9 deletions etc/cashu-ts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,7 @@ export type MeltProofsConfig = {
keysetId?: string;
privkey?: string | string[];
onCountersReserved?: OnCountersReserved;
nut08Change?: boolean;
};

// @public
Expand Down Expand Up @@ -1392,7 +1393,7 @@ export function normalizeSecpPubkey(pk: string): string;
export type NUT10Option = {
kind: string;
data: string;
tags: string[][];
tags?: string[][];
};

// @public (undocumented)
Expand Down Expand Up @@ -1789,11 +1790,6 @@ export type PostRestoreResponse = {
signatures: SerializedBlindedSignature[];
};

// @public (undocumented)
export type PrepareMeltConfig = MeltProofsConfig & {
nut08Change?: boolean;
};

// @public
export type PrivKey = Uint8Array | string;

Expand Down Expand Up @@ -1843,7 +1839,7 @@ export type RawMintKeys = {
export type RawNUT10Option = {
k: string;
d: string;
t: string[][];
t?: string[][];
};

// @public (undocumented)
Expand Down Expand Up @@ -2333,7 +2329,9 @@ export class Wallet {
completeMint(mintPreview: MintPreview<Pick<MintQuoteBaseResponse, 'quote'>>): Promise<Proof[]>;
completeSwap(swapPreview: SwapPreview, privkey?: string | string[]): Promise<SendResponse>;
readonly counters: WalletCounters;
createLockedMintQuote(amount: AmountLike, pubkey: string, description?: string): Promise<MintQuoteBolt11Response>;
createLockedMintQuote(amount: AmountLike, pubkey: string, description?: string): Promise<MintQuoteBolt11Response & {
pubkey: string;
}>;
createMeltChangeProofs(outputData: OutputDataLike[], changeSigs: SerializedBlindedSignature[]): Proof[];
createMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteGenericResponse>(method: string, payload: Record<string, unknown>, options?: {
normalize?: (raw: Record<string, unknown>) => TRes;
Expand Down Expand Up @@ -2389,7 +2387,7 @@ export class Wallet {
amount: AmountLike;
quote: TQuote;
}>, config?: MintProofsConfig, outputType?: OutputType): Promise<BatchMintPreview<TQuote>>;
prepareMelt<TQuote extends Pick<MeltQuoteBaseResponse, 'amount' | 'quote'>>(method: string, meltQuote: TQuote, proofsToSend: ProofLike[], config?: PrepareMeltConfig, outputType?: OutputType): Promise<MeltPreview<TQuote>>;
prepareMelt<TQuote extends Pick<MeltQuoteBaseResponse, 'amount' | 'quote'>>(method: string, meltQuote: TQuote, proofsToSend: ProofLike[], config?: MeltProofsConfig, outputType?: OutputType): Promise<MeltPreview<TQuote>>;
prepareMint<TQuote extends Pick<MintQuoteBaseResponse, 'quote'>>(method: string, amount: AmountLike, quote: TQuote, config?: MintProofsConfig, outputType?: OutputType): Promise<MintPreview<TQuote>>;
prepareSwapToReceive(token: Token | string | ProofLike[], config?: ReceiveConfig, outputType?: OutputType): Promise<SwapPreview>;
prepareSwapToSend(amount: AmountLike, proofs: ProofLike[], config?: SendConfig, outputConfig?: OutputConfig): Promise<SwapPreview>;
Expand Down
42 changes: 42 additions & 0 deletions migration-5.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/model/PaymentRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Proof, 'amount'>), amount: Amount.from(amount) };
});
return {
...(id !== undefined && { id }),
Expand Down
8 changes: 6 additions & 2 deletions src/model/types/NUT23.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
5 changes: 2 additions & 3 deletions src/wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ import {
type ReceiveConfig,
type MintProofsConfig,
type MeltProofsConfig,
type PrepareMeltConfig,
type CompleteMeltOptions,
type SwapTransaction,
type MeltProofsResponse,
Expand Down Expand Up @@ -1990,7 +1989,7 @@ class Wallet {
amount: AmountLike,
pubkey: string,
description?: string,
): Promise<MintQuoteBolt11Response> {
): Promise<MintQuoteBolt11Response & { pubkey: string }> {
this.requireSupport('mint', 'bolt11');
this.requireMintableKeyset('createLockedMintQuote');
this.failIf(typeof pubkey !== 'string', 'A pubkey is required to lock the mint quote');
Expand Down Expand Up @@ -3188,7 +3187,7 @@ class Wallet {
method: string,
meltQuote: TQuote,
proofsToSend: ProofLike[],
config?: PrepareMeltConfig,
config?: MeltProofsConfig,
outputType?: OutputType,
): Promise<MeltPreview<TQuote>> {
this.validateMeltQuote(meltQuote);
Expand Down
7 changes: 4 additions & 3 deletions src/wallet/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
7 changes: 4 additions & 3 deletions src/wallet/types/payment-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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[][];
};
4 changes: 2 additions & 2 deletions test/wallet/WalletOps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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/);
});
Expand Down
7 changes: 4 additions & 3 deletions test/wallet/paymentRequests.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)', () => {
Expand Down
9 changes: 2 additions & 7 deletions test/wallet/wallet-quotes-mutants.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
type MintQuoteBolt12Response,
type MeltQuoteBolt11Response,
type MintQuoteBaseResponse,
type PrepareMeltConfig,
} from '../../src';

import {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading