Skip to content
Open
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
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