Skip to content
6 changes: 6 additions & 0 deletions docs-src/usage/melt_token.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ requiring first-class library support.

The mint must advertise the method at `/v1/melt/quote/{method}`.

The NUT-05 base fields (`quote`, `request`, `amount`, `unit`, `method`, `state`, `expiry`, optional
`fee_reserve`) are normalized and validated automatically for every method — `state` always uses
the standard `UNPAID`/`PENDING`/`PAID` values. The optional `normalize` callback is only needed for
method-specific fields. Without a type parameter, the generic methods return
`MeltQuoteGenericResponse`, which exposes method-specific fields as `unknown`.

```ts
import { Wallet, Amount, type MeltQuoteBaseResponse, type AmountLike } from '@cashu/cashu-ts';

Expand Down
17 changes: 7 additions & 10 deletions docs-src/usage/mint_token.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,14 @@ The generic `createMintQuote()` / `mintProofs()` methods support arbitrary payme

The mint must advertise the method at `/v1/mint/quote/{method}`.

The NUT-04 base fields (`quote`, `request`, `unit`, `method`, `amount_paid`, `amount_issued`, `updated_at`, `expiry`) are normalized and validated automatically for every method — the optional `normalize` callback is only needed for method-specific fields. Without a type parameter, the generic methods return `MintQuoteGenericResponse`, which exposes method-specific fields as `unknown`.

```ts
import {
Wallet,
Amount,
MintQuoteState,
type MintQuoteBaseResponse,
type AmountLike,
} from '@cashu/cashu-ts';
import { Wallet, Amount, type MintQuoteBaseResponse, type AmountLike } from '@cashu/cashu-ts';

// Define your custom quote response type
type BacsMintQuoteResponse = MintQuoteBaseResponse & {
amount: Amount;
state: MintQuoteState;
reference: string; // bank transfer reference
};

Expand Down Expand Up @@ -132,8 +127,10 @@ const updated = await wallet.checkMintQuote<BacsMintQuoteResponse>('bacs', mintQ
}),
});

// Mint once the bank transfer is confirmed
if (updated.state === MintQuoteState.PAID) {
// Mint once the bank transfer is confirmed. The accounting fields are the
// method-independent way to read quote progress: paid minus issued is mintable.
const available = updated.amount_paid.subtract(updated.amount_issued);
if (available.greaterThanOrEqual(5000)) {
const proofs = await wallet.mintProofs('bacs', 5000, updated);
}
```
42 changes: 25 additions & 17 deletions etc/cashu-ts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,12 +845,16 @@ export type MeltProofsResponse<TQuote extends Pick<MeltQuoteBaseResponse, 'quote
export type MeltQuoteBaseRequest = {
unit: string;
request: string;
amount?: AmountLike;
};

// @public
export type MeltQuoteBaseResponse = {
quote: string;
request: string;
amount: Amount;
method: string;
fee_reserve?: Amount;
unit: string;
state: MeltQuoteState;
expiry: number;
Expand All @@ -871,7 +875,6 @@ export type MeltQuoteBolt11Request = MeltQuoteBaseRequest & {

// @public
export type MeltQuoteBolt11Response = MeltQuoteBaseResponse & {
request: string;
fee_reserve: Amount;
payment_preimage: string | null;
};
Expand All @@ -888,6 +891,9 @@ export type MeltQuoteBolt12Request = MeltQuoteBaseRequest & {
// @public
export type MeltQuoteBolt12Response = MeltQuoteBolt11Response;

// @public
export type MeltQuoteGenericResponse = MeltQuoteBaseResponse & Record<string, unknown>;

// @public
export type MeltQuoteOnchainFeeOption = {
fee_index: number;
Expand All @@ -902,7 +908,6 @@ export type MeltQuoteOnchainRequest = MeltQuoteBaseRequest & {

// @public
export type MeltQuoteOnchainResponse = MeltQuoteBaseResponse & {
request: string;
fee_options: MeltQuoteOnchainFeeOption[];
selected_fee_index: number | null;
outpoint: string | null;
Expand Down Expand Up @@ -935,29 +940,29 @@ export class Mint {
logger?: Logger;
});
check(checkPayload: CheckStatePayload, customRequest?: RequestFn): Promise<CheckStateResponse>;
checkMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteBaseResponse>(method: string, quote: string, options?: {
checkMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteGenericResponse>(method: string, quote: string, options?: {
customRequest?: RequestFn;
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
checkMeltQuoteBolt11(quote: string, customRequest?: RequestFn): Promise<MeltQuoteBolt11Response>;
checkMeltQuoteBolt12(quote: string, customRequest?: RequestFn): Promise<MeltQuoteBolt12Response>;
checkMeltQuoteOnchain(quote: string, customRequest?: RequestFn): Promise<MeltQuoteOnchainResponse>;
checkMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteBaseResponse>(method: string, quote: string, options?: {
checkMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteGenericResponse>(method: string, quote: string, options?: {
customRequest?: RequestFn;
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
checkMintQuoteBolt11(quote: string, customRequest?: RequestFn): Promise<MintQuoteBolt11Response>;
checkMintQuoteBolt12(quote: string, customRequest?: RequestFn): Promise<MintQuoteBolt12Response>;
checkMintQuoteOnchain(quote: string, customRequest?: RequestFn): Promise<MintQuoteOnchainResponse>;
connectWebSocket(): Promise<void>;
createMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteBaseResponse>(method: string, payload: Record<string, unknown>, options?: {
createMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteGenericResponse>(method: string, payload: Record<string, unknown>, options?: {
customRequest?: RequestFn;
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
createMeltQuoteBolt11(meltQuotePayload: MeltQuoteBolt11Request, customRequest?: RequestFn): Promise<MeltQuoteBolt11Response>;
createMeltQuoteBolt12(meltQuotePayload: MeltQuoteBolt12Request, customRequest?: RequestFn): Promise<MeltQuoteBolt12Response>;
createMeltQuoteOnchain(meltQuotePayload: MeltQuoteOnchainRequest, customRequest?: RequestFn): Promise<MeltQuoteOnchainResponse>;
createMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteBaseResponse>(method: string, payload: Record<string, unknown>, options?: {
createMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteGenericResponse>(method: string, payload: Record<string, unknown>, options?: {
customRequest?: RequestFn;
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
Expand Down Expand Up @@ -1203,6 +1208,8 @@ export type MintProofsConfig = {
// @public
export type MintQuoteBaseRequest = {
unit: string;
amount?: AmountLike;
description?: string;
pubkey?: string;
};

Expand All @@ -1211,6 +1218,11 @@ export type MintQuoteBaseResponse = {
quote: string;
request: string;
unit: string;
method: string;
amount_paid: Amount;
amount_issued: Amount;
updated_at: number | null;
expiry: number | null;
pubkey?: string;
};

Expand All @@ -1224,7 +1236,6 @@ export type MintQuoteBolt11Request = MintQuoteBaseRequest & {
export type MintQuoteBolt11Response = MintQuoteBaseResponse & {
amount: Amount;
state: MintQuoteState;
expiry: number | null;
};

// @public
Expand All @@ -1236,26 +1247,23 @@ export type MintQuoteBolt12Request = MintQuoteBaseRequest & {
// @public
export type MintQuoteBolt12Response = MintQuoteBaseResponse & {
amount: Amount | null;
expiry: number | null;
pubkey: string;
amount_paid: Amount;
amount_issued: Amount;
};

// @public (undocumented)
export type MintQuoteFor<M extends MintMethod> = M extends 'bolt11' ? string | MintQuoteBolt11Response : M extends 'bolt12' ? MintQuoteBolt12Response : MintQuoteOnchainResponse;

// @public
export type MintQuoteGenericResponse = MintQuoteBaseResponse & Record<string, unknown>;

// @public
export type MintQuoteOnchainRequest = MintQuoteBaseRequest & {
pubkey: string;
};

// @public
export type MintQuoteOnchainResponse = MintQuoteBaseResponse & {
expiry: number | null;
pubkey: string;
amount_paid: Amount;
amount_issued: Amount;
};

// @public (undocumented)
Expand Down Expand Up @@ -2144,13 +2152,13 @@ export class Wallet {
lastCounterWithSignature?: number;
}>;
bindKeyset(id: string): void;
checkMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteBaseResponse>(method: string, quote: string | Pick<TRes, 'quote'>, options?: {
checkMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteGenericResponse>(method: string, quote: string | Pick<TRes, 'quote'>, options?: {
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
checkMeltQuoteBolt11(quote: string | MeltQuoteBolt11Response): Promise<MeltQuoteBolt11Response>;
checkMeltQuoteBolt12(quote: string): Promise<MeltQuoteBolt12Response>;
checkMeltQuoteOnchain(quote: string): Promise<MeltQuoteOnchainResponse>;
checkMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteBaseResponse>(method: string, quote: string | Pick<TRes, 'quote'>, options?: {
checkMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteGenericResponse>(method: string, quote: string | Pick<TRes, 'quote'>, options?: {
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
checkMintQuoteBolt11(quote: string | MintQuoteBolt11Response): Promise<MintQuoteBolt11Response>;
Expand All @@ -2164,13 +2172,13 @@ export class Wallet {
readonly counters: WalletCounters;
createLockedMintQuote(amount: AmountLike, pubkey: string, description?: string): Promise<MintQuoteBolt11Response>;
createMeltChangeProofs(outputData: OutputDataLike[], changeSigs: SerializedBlindedSignature[]): Proof[];
createMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteBaseResponse>(method: string, payload: Record<string, unknown>, options?: {
createMeltQuote<TRes extends MeltQuoteBaseResponse = MeltQuoteGenericResponse>(method: string, payload: Record<string, unknown>, options?: {
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
createMeltQuoteBolt11(invoice: string, amountMsat?: AmountLike): Promise<MeltQuoteBolt11Response>;
createMeltQuoteBolt12(offer: string, amountMsat?: AmountLike): Promise<MeltQuoteBolt12Response>;
createMeltQuoteOnchain(address: string, amount: AmountLike): Promise<MeltQuoteOnchainResponse>;
createMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteBaseResponse>(method: string, payload: Record<string, unknown>, options?: {
createMintQuote<TRes extends MintQuoteBaseResponse = MintQuoteGenericResponse>(method: string, payload: Record<string, unknown>, options?: {
normalize?: (raw: Record<string, unknown>) => TRes;
}): Promise<TRes>;
createMintQuoteBolt11(amount: AmountLike, description?: string): Promise<MintQuoteBolt11Response>;
Expand Down
71 changes: 71 additions & 0 deletions migration-5.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,74 @@ await wallet.completeMelt(meltPreview, privkey, { preferAsync: true });
```

Calls that already pass a `CompleteMeltOptions` object (or omit the third argument) need no change.

---

## Mint quote responses now carry NUT-04 accounting fields

`MintQuoteBaseResponse` (and every method-specific mint quote response) gains five required fields:

- `amount_paid: Amount` — total paid to the mint for this quote
- `amount_issued: Amount` — total ecash issued for this quote
- `updated_at: number | null` — Unix timestamp of the last quote update (`null` when the mint does not report it)
- `expiry: number | null` — moved here from the method-specific response types
- `method: string` — the payment method, populated from the request endpoint when the mint omits it; a reported method that disagrees with the endpoint throws `Invalid response from mint`

The difference `amount_paid − amount_issued` is the amount available to mint. The single-use `state` field is deprecated in NUT-04 in favour of the accounting fields, but cashu-ts always populates it on `MintQuoteBolt11Response` (derived from the accounting fields when the mint omits it).

Quotes returned by `Wallet`/`Mint` methods need no change: normalization fills the fields in, deriving them from the legacy `state` and `amount` for mints that predate quote accounting.

### Migration

Code that constructs mint quote objects (test fixtures, hydrating stored quotes) must supply the new fields:

```ts
// Before
const quote: MintQuoteBolt11Response = {
quote: 'q1',
request: 'lnbc…',
unit: 'sat',
amount: Amount.from(21),
state: 'UNPAID',
expiry: null,
};

// After
const quote: MintQuoteBolt11Response = {
quote: 'q1',
request: 'lnbc…',
unit: 'sat',
amount: Amount.from(21),
state: 'UNPAID',
expiry: null,
method: 'bolt11',
amount_paid: Amount.from(0),
amount_issued: Amount.from(0),
updated_at: null,
};
```

Generic mint quote responses (custom payment methods) are now base-validated like melt quotes always were: a response missing `quote`, `request` or `unit`, or whose accounting fields are absent and underivable, throws `Invalid response from mint` instead of passing through silently.

---

## Melt quote responses require `request` and carry `method`

`request` (the method-specific payment routing instructions) moved from the bolt11/onchain response types into `MeltQuoteBaseResponse`, and the base type gains an optional `fee_reserve`. Melt quote responses from any method — including custom ones — that lack a `request` string now throw `Invalid response from mint`.

`MeltQuoteBaseResponse` also gains a required `method: string` with the same semantics as on mint quotes: populated from the request endpoint when the mint omits it, throwing on a mismatch.

bolt11, bolt12 and onchain flows are unaffected: those responses already required `request`. Code constructing a plain `MeltQuoteBaseResponse` must include `request` and `method`.

---

## Mintable amount is enforced for every payment method

`prepareMint`/`mintProofs` previously rejected requests above `amount_paid − amount_issued` only for bolt12 and onchain quotes. v5 applies the check to any quote object that carries accounting fields, regardless of method.

Two escape hatches keep stored-quote flows working:

- Quote objects without accounting fields (e.g. minimal `{ quote: '…' }` references) skip the check, as before.
- Quotes reporting `0/0` defer to the mint — a zero snapshot may simply have been fetched before the payment was made, so the create → pay externally → mint flow is unaffected.

The practical change from v4: attempting to re-mint a quote object whose snapshot shows it fully issued (`amount_paid === amount_issued > 0`) now fails fast client-side instead of round-tripping to the mint for a rejection.
Loading
Loading