@cashu/cashu-ts
Version:
cashu library for communicating with a cashu mint
952 lines (664 loc) • 43.3 kB
Markdown
# Version 4.0.0 Migration guide
⚠️ Upgrading to version 4.0.0 will come with breaking changes! Please follow the migration guide for a smooth transition to the new version.
**TIP**: If you use a coding agent, you can point them to `migration-4.0.0.SKILL.md`.
---
## The `Amount` value object — what changed and what it means for your app
Many v4 APIs that previously returned or accepted `number` now use `Amount`. This avoids silent precision loss above `Number.MAX_SAFE_INTEGER`, which matters for millisatoshi or other high-volume integer accounting.
`Amount` is immutable, bigint-backed, and non-negative. It provides:
- **Arithmetic**: `.add()`, `.subtract()`, `.multiplyBy()`, `.divideBy()`
- **Comparison**: `.lessThan()`, `.greaterThan()`, `.equals()`, etc.
- **Conversion**: `.toNumber()` (throws above `MAX_SAFE_INTEGER`), `.toBigInt()`, `.toString()`, `.toJSON()`
- **Finance**: `.scaledBy()`, `.ceilPercent()`, `.floorPercent()`, `.clamp()`, `.inRange()`
- **Construction**: `Amount.from(x)` accepts `number`, `bigint`, `string`, or another `Amount`
### Choosing your migration strategy
Before you start updating call sites, decide how deeply you want to adopt `Amount`:
**Option A — Adopt `Amount` natively (recommended for new or large-amount apps)**
Keep `Amount` flowing through your own functions and types. Use `Amount` helpers for arithmetic, and convert to `number` only at boundaries that truly require a JavaScript number. For display, prefer string-safe formatting where possible: for integer-unit currencies like SAT, avoid eager `.toNumber()` and use runtime-appropriate bigint/string formatting; for decimal or minor-unit currencies, use formatting helpers that preserve precision instead of eagerly calling `.toNumber()`.
**Option B — Convert at the boundary (simplest for existing number-typed codebases)**
Call `.toNumber()` immediately on every `Amount` the library returns, then leave all your internal types as `number`. Safe as long as your amounts stay within `Number.MAX_SAFE_INTEGER`.
Both strategies are valid. The sections below show the mechanical changes required; the key question is whether you propagate `Amount` inward or flatten it at the edge.
### Practical `Amount` rules
- `Amount` is for non-negative integer magnitudes only. Model sign separately.
- `AmountLike` is a boundary type: `number | bigint | string | Amount`.
- Normalize external input with `Amount.from(...)`, then keep `Amount` in domain logic.
- Plain JSON is acceptable for minimal migrations because `Amount.toJSON()` emits a decimal string.
- If you round-trip an `Amount` through plain JSON, rehydrate it with `Amount.from(...)`.
- Prefer `JSONInt.stringify` / `JSONInt.parse` for persisted or transported integer-bearing payloads when you want numeric/bigint fidelity after parse.
- `toNumber()` is safe-or-throw; `toNumberUnsafe()` is explicitly lossy.
- For display, prefer string-safe formatting and avoid eager `.toNumber()`.
```ts
const raw: AmountLike = getExternalAmount();
const amount = Amount.from(raw);
```
---
## ESM-only package
cashu-ts v4 ships **only ES modules**. The CommonJS build (`lib/cashu-ts.cjs`) has been removed.
Our core dependencies (`@noble/curves`, `@noble/hashes`, `@scure/bip32`) are ESM-only.
Maintaining a dual CJS build required bundling those deps into the CJS output, increasing
complexity and risk of module-duplication bugs.
- `package.json` no longer has a `"require"` condition in `exports` or a `"main"` field pointing to a `.cjs` file.
- `npm run compile` produces only the ESM bundle (`lib/cashu-ts.es.js`).
- The IIFE standalone browser build is unchanged.
### Migration
| Current setup | Migration |
| --------------------------------- | --------------------------------------------- |
| `require('@cashu/cashu-ts')` | Convert to ESM `import` or dynamic `import()` |
| Bundler configured for CJS output | Update bundler config to output ESM |
```js
// Before (CJS)
const { Wallet } = require('@cashu/cashu-ts');
// After (ESM)
import { Wallet } from '@cashu/cashu-ts';
```
If you must keep a CJS entry point, use a dynamic import wrapper:
```js
// CJS compatibility using an IIFE
(async () => {
const { Wallet } = await import('@cashu/cashu-ts');
// ...
})();
```
---
## Amount fields on mint responses now return `Amount` objects
Previously typed as `number`, the following fields now return an `Amount` value object (imported from `@cashu/cashu-ts`):
| Type | Field(s) |
| ---------------------------- | ---------------------------------------- |
| `MintQuoteBolt11Response` | `amount` |
| `MintQuoteBolt12Response` | `amount`, `amount_paid`, `amount_issued` |
| `MeltQuoteBaseResponse` | `amount` |
| `MeltQuoteBolt11Response` | `fee_reserve` |
| `MeltQuoteBolt12Response` | `fee_reserve` |
| `SerializedBlindedSignature` | `amount` |
`SwapMethod.min_amount` / `max_amount` (from `GetInfoResponse`) are now typed as `AmountLike` (`number | string | bigint | Amount`).
`expiry` fields on `MintQuoteBolt11Response` and `MintQuoteBolt12Response` now allow `null` (spec-compliant) in addition to `number`.
### Migration
```ts
// Before
const sats = meltQuote.fee_reserve + meltQuote.amount;
const n = meltQuote.amount;
// After
const sats = meltQuote.fee_reserve.add(meltQuote.amount).toNumber();
const n = meltQuote.amount.toNumber(); // throws if value > Number.MAX_SAFE_INTEGER
// Amount.toJSON() always emits a decimal string (previously number | string).
// This means JSON.stringify produces a quoted string, not a bare number:
JSON.stringify({ amount: meltQuote.amount }); // → '{"amount":"1000"}' (not '{"amount":1000}')
// Rehydrate a JSON leaf value back to Amount
const parsed = JSON.parse('{"amount":"1000"}');
const amount = Amount.from(parsed.amount);
```
---
## `SerializedBlindedMessage.amount` is now `Amount`
`SerializedBlindedMessage` is the outbound wire type sent to the mint. Its `amount` field is now typed as `Amount` (previously `number`), consistent with the rest of the v4 amount model.
This type is not typically constructed directly by application code; it is produced internally by `BlindedMessage.getSerializedBlindedMessage()`. If you build `SerializedBlindedMessage` objects manually, update the `amount` field:
```ts
// Before
const output: SerializedBlindedMessage = { amount: 1000, id: keysetId, B_: hex };
// After
const output: SerializedBlindedMessage = { amount: Amount.from(1000), id: keysetId, B_: hex };
```
### Removed
- 2024 backwards-compat shims: deprecated `paid` boolean on melt responses, deprecated array-of-arrays `contact` field normalisation, deprecated NUT-04/05/06 response shapes
---
## `sumProofs()` and `TokenMetadata.amount` now return `Amount`
`sumProofs()` (utility function) and the `amount` field on `TokenMetadata` (returned by `getTokenMetadata()`) previously returned `number`; both now return an `Amount` value object.
```ts
// Before
const n: number = sumProofs(proofs);
const m: number = getTokenMetadata(token).amount;
// After
const total: Amount = sumProofs(proofs);
const n: number = total.toNumber(); // throws if value > Number.MAX_SAFE_INTEGER
const m: Amount = getTokenMetadata(token).amount;
```
---
## `OutputData.sumOutputAmounts()` now returns `Amount`
Previously returned `number`; now returns an `Amount` value object to be consistent with the rest of the v4 API.
```ts
// Before
const total: number = OutputData.sumOutputAmounts(outputs);
// After
const total: Amount = OutputData.sumOutputAmounts(outputs);
const n: number = total.toNumber(); // throws if value > Number.MAX_SAFE_INTEGER
```
---
## `SwapPreview.amount` and `SwapPreview.fees` are now `Amount`
Both fields on the `SwapPreview` type (returned by `prepareSwapToSend()` / `prepareSwapToReceive()`) are typed as `Amount`.
If you persist or deserialize previews yourself, rehydrate before calling `Amount` methods. In arithmetic expressions, you only need to rehydrate the operand you are invoking the method on: methods like `.subtract(...)` already accept `AmountLike` for the argument.
```ts
// Before
const net = preview.amount - preview.fees;
// After — if the preview came from JSON/storage
const net = Amount.from(preview.amount).subtract(preview.fees);
const n: number = net.toNumber();
```
---
## `PaymentRequest.amount` now returns `Amount`
The `amount` field on `PaymentRequest` (and the result of `decodePaymentRequest()`) previously returned `number | undefined`; it now returns `Amount | undefined`.
```ts
// Before
const sats: number | undefined = request.amount;
// After
const sats: number | undefined = request.amount?.toNumber();
```
---
## Utility functions `splitAmount` and `getKeysetAmounts` now return `Amount[]`
These public functions in `@cashu/cashu-ts` previously returned `number[]`; they now return `Amount[]`.
```ts
// Before
const chunks: number[] = splitAmount(1000, keys);
const denominations: number[] = getKeysetAmounts(keyset);
// After
const chunks: Amount[] = splitAmount(1000, keys);
const denominations: Amount[] = getKeysetAmounts(keyset);
// Convert to numbers where needed
chunks.map((a) => a.toNumber());
```
---
## `OutputDataFactory` and `OutputDataLike`: generic removed, `amount` parameter widened
Both types previously carried a `TKeyset extends HasKeysetKeys` generic parameter and the `amount` argument on `OutputDataFactory` was typed as `number`. Both changes are now applied:
- The `<TKeyset>` generic has been removed; the keyset parameter is fixed to `HasKeysetKeys`.
- The `amount` argument on `OutputDataFactory` is now `AmountLike` (was `number`).
```ts
// Before
const factory: OutputDataFactory<MyKeyset> = (amount: number, keys: MyKeyset) => { ... };
class MyOutput implements OutputDataLike<MyKeyset> { ... }
// After
import { Amount, type AmountLike, type HasKeysetKeys } from '@cashu/cashu-ts';
const factory: OutputDataFactory = (amount: AmountLike, keys: HasKeysetKeys) => {
const a = Amount.from(amount);
// ...
};
class MyOutput implements OutputDataLike { ... }
```
---
## `SelectProofs` type: `amountToSelect` parameter is now `AmountLike`
If you implement a custom `SelectProofs` function or hold a reference typed as `SelectProofs`, update the `amountToSelect` parameter from `number` to `AmountLike`.
```ts
// Before
const mySelector: SelectProofs = (proofs, amountToSelect: number, ...) => { ... };
// After
import { type AmountLike } from '@cashu/cashu-ts';
const mySelector: SelectProofs = (proofs, amountToSelect: AmountLike, ...) => { ... };
```
---
## `MintPreview.quote` is now the full quote object
`prepareMint()` previously stored only the quote ID string in `MintPreview.quote`. It now stores the full quote object passed into `prepareMint()`, giving consumers access to informational fields (`expiry`, `request`, `amount`, `unit`) needed for NUT-19 retry flows.
```ts
// Before — quote was stored on the preview as a plain string
const previewV3: MintPreview = { ..., quote: 'q123' };
previewV3.quote; // string (quote ID only)
// After — quote is the full TQuote object when a quote object is passed
const preview = await wallet.prepareMint('bolt11', 1000, quoteResponse);
preview.quote.expiry; // number | null — accessible now
preview.quote.request; // string — Lightning invoice
// prepareMint() now expects a quote object, not a string ID
const preview2 = await wallet.prepareMint('bolt11', 1000, { quote: 'q123' });
preview2.quote; // { quote: 'q123' }
```
The type is `MintPreview<TQuote>` where `TQuote extends { quote: string }` (defaults to `MintQuoteBaseResponse`).
If you only have a bolt11 quote ID string, use `mintProofsBolt11(amount, quoteId)` rather than `prepareMint()`.
If you construct a `MintPreview` manually (e.g., after deserialization), update the `quote` field from a bare string to an object:
```ts
// Before
const preview: MintPreview = { ..., quote: 'q123' };
// After — pass the full quote object returned by createMintQuoteBolt11/12
const preview: MintPreview = { ..., quote: mintQuoteResponse };
```
Also note that preview objects are not intended for direct `JSON.stringify(...)`.
`MintPreview`, `MeltPreview`, `BatchMintPreview`, and `SwapPreview` contain values such as
`Amount`, `bigint`, `Uint8Array`, and class instances that need explicit rehydration. If you
persist previews for replay-safe recovery, serialize them into an app-defined snapshot format and
explicitly rehydrate them before passing them back to `completeMint()`, `completeMelt()`, or
`completeSwap()`.
---
## `Proof.amount` is now `Amount`
The `amount` field on the `Proof` type has changed from `number` to `Amount`. This affects any code that constructs, stores, or compares proof amounts.
```ts
// Before
const proof: Proof = { id, amount: 1000, C, secret };
const total = proofs.reduce((sum, p) => sum + p.amount, 0);
// After
const proof: Proof = { id, amount: Amount.from(1000), C, secret };
const total: Amount = proofs.reduce((sum, p) => sum.add(p.amount), Amount.zero());
// or more simply
const total: Amount = sumProofs(proofs);
// Convert or compare explicitly when needed
const display: number = proof.amount.toNumber();
const isExact = proof.amount.equals(1000);
```
If you persist proofs to JSON or a database, see the [Proof serialization](#proof-serialization) section below for the helper functions provided.
---
## `Wallet.getFeesForProofs()` and `Wallet.getFeesForKeyset()` now return `Amount`
Both methods previously returned `number`; they now return `Amount` value objects, consistent with other fee and amount fields in the v4 API.
```ts
// Before
const fee: number = wallet.getFeesForProofs(proofs);
const total = sendAmount + fee;
const ksFee: number = wallet.getFeesForKeyset(3, keysetId);
// After
const fee: Amount = wallet.getFeesForProofs(proofs);
const total = Amount.from(sendAmount).add(fee);
const n: number = fee.toNumber();
const ksFee: Amount = wallet.getFeesForKeyset(3, keysetId);
```
---
## `MessageQueue` and `MessageNode` are no longer public top-level utils
`MessageQueue` and `MessageNode` are no longer exported from `@cashu/cashu-ts` utils. `MessageNode` is no longer part of the public API, and `MessageQueue` should be treated as internal rather than migrated as a supported import.
If you were importing these classes directly:
```ts
// Before
import { MessageQueue, MessageNode } from '@cashu/cashu-ts';
// After
// Use supported WSConnection APIs instead of importing queue internals directly
```
---
## Internal utility functions removed or restricted
Several functions that were intended for internal use have been removed from the public API.
### Removed entirely (dead code)
| Function | Notes |
| --------------- | --------------------------------------------------------------------------------- |
| `checkResponse` | Superseded by the `HttpResponseError` transport refactor in 2023. Had no callers. |
| `deepEqual` | Generic deep-equality helper. Had no callers inside or outside the library. |
### Made private (no longer exported)
| Function | Notes |
| ------------------- | --------------------------------------------------------------- |
| `mergeUInt8Arrays` | Internal byte-buffer helper. |
| `hasNonHexId` | Internal guard used inside token encoding. |
| `getKeepAmounts` | Internal wallet coin-selection algorithm. Removed from `utils`. |
| `getEncodedTokenV4` | Use `getEncodedToken` instead. |
### Marked `@internal`
The following are still exported but are excluded from the trimmed type definitions and not part of the supported public API. Remove any external dependencies on them.
| Function | Notes |
| ----------------------- | ---------------------------------------------- |
| `isValidHex` | Internal helper. |
| `hexToNumber` | Crypto scalar helper (hex → bigint). |
| `numberToHexPadded64` | Crypto scalar helper (bigint → 64-char hex). |
| `isObj` | HTTP response type guard. |
| `joinUrls` | Mint URL path builder. |
| `sanitizeUrl` | Renamed to `normalizeUrl` (internal). |
| `invoiceHasAmountInHRP` | BOLT-11 HRP amount detector. |
| `bigIntStringify` | `JSON.stringify` replacer for `bigint` values. |
### `handleTokens` no longer exported
`handleTokens` should always have been an internal function, but was exported. If you used this function, prefer `getTokenMetadata` before a wallet exists, then `wallet.decodeToken(...)` after the wallet is loaded. Use `getDecodedToken(str, keysetIds)` only in advanced flows where you already manage keyset IDs yourself.
---
## Proof serialization
`ProofLike` is a new exported type: a proof-shaped object whose `amount` has not yet been normalized to `Amount` (i.e. `Omit<Proof, 'amount'> & { amount: AmountLike }`). Use it to model proofs from external storage where `amount` may be a `number`, `string`, `bigint`, or `Amount`.
Three helpers cover the common patterns for persisting and restoring proofs:
| Function | Use case |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `serializeProofs(proofs)` | Serialize `Proof \| Proof[]` to `string[]` (one JSON string per proof) without precision loss. |
| `deserializeProofs(json)` | Restore `string \| string[] \| ProofLike[]` back to `Proof[]`, with `amount` normalized to `Amount`. Pass a raw JSON string directly (no `JSON.parse` needed), a `string[]` for individual proof strings (e.g. NutZap tags), or a `ProofLike[]` for already-parsed objects. |
| `normalizeProofAmounts(raw)` | Lower-level building block: convert `ProofLike[]` to `Proof[]` by normalizing `amount` to `Amount`. Called internally by `deserializeProofs`; use directly when you already have typed `ProofLike[]` and want to skip string-detection. |
Migration rule: treat wallet/mint/API/JSON proofs as `ProofLike[]` until normalized. Normalize before app-level arithmetic, encoding, or storage-model conversion.
**Tip**: Core wallet flows now accept `ProofLike[]` directly. If you already have deserialized proof objects from JSON or storage, you can usually pass them straight into wallet APIs such as `wallet.receive(...)`, `wallet.send(...)`, `wallet.sendOffline(...)`, `wallet.prepareSwapToSend(...)`, `wallet.meltProofs...(...)`, and `wallet.signP2PKProofs(...)` without calling `normalizeProofAmounts(...)` yourself first. The same applies to `WalletOps` / builder entry points such as `wallet.ops.send(...)`, `wallet.ops.receive(...)`, and `wallet.ops.meltBolt11(...)`.
`wallet.selectProofsToSend()` and `wallet.groupProofsByState()` also accept `ProofLike[]`, so proofs loaded from storage (with `amount: number`) can be passed directly without conversion. `groupProofsByState` preserves the input type in its output — pass `MyProof[]` in, get `MyProof[]` back.
```ts
import { serializeProofs, deserializeProofs } from '@cashu/cashu-ts';
// localStorage — serializeProofs returns string[], so wrap with JSON.stringify for storage.
localStorage.setItem('proofs', JSON.stringify(serializeProofs(proofs)));
const proofs = deserializeProofs(localStorage.getItem('proofs') ?? '[]');
// NutZap proof tags — one proof string per tag
const proofTags = serializeProofs(proofs).map((s) => ['proof', s]);
const proofs = deserializeProofs(event.tags.filter((t) => t[0] === 'proof').map((t) => t[1]));
// Already-parsed objects (e.g. from a database query) — also accepted directly
const proofs = deserializeProofs(db.query('SELECT * FROM proofs'));
```
Use `getEncodedToken` when you need a full cashu token string (mint URL + unit metadata). Use `serializeProofs` when you only need to store or transmit raw proof arrays.
---
## Crypto primitive renames
The following low-level exports from `@cashu/cashu-ts` (re-exported from the crypto layer) have been renamed for clarity. The old names no longer exist.
| Old name | New name |
| ---------------------------- | ------------------------------- |
| `RawProof` | `UnblindedSignature` |
| `constructProofFromPromise` | `constructUnblindedSignature` |
| `createRandomBlindedMessage` | `createRandomRawBlindedMessage` |
| `verifyProof` | `verifyUnblindedSignature` |
These are low-level primitives not typically used by application code. If you use them directly, update your imports:
```ts
// Before
import {
RawProof,
constructProofFromPromise,
createRandomBlindedMessage,
verifyProof,
} from '@cashu/cashu-ts';
// After
import {
UnblindedSignature,
constructUnblindedSignature,
createRandomRawBlindedMessage,
verifyUnblindedSignature,
} from '@cashu/cashu-ts';
```
### Removed crypto primitives
The following exports have been removed with no replacement — they were dead code not used outside the library:
| Removed | Notes |
| -------------------- | --------------------------------------------------------------- |
| `SerializedProof` | Hex-serialised proof type; use `Proof` directly |
| `serializeProof()` | Use `Proof` values directly — no serialisation step is needed |
| `deserializeProof()` | Use `Proof` values directly — no deserialisation step is needed |
| `BlindedMessage` | Was a deprecated alias for `RawBlindedMessage`; use the latter |
### `BlindSignature.amount` field removed
`BlindSignature` (the post-blinding crypto primitive) had an `amount` field that was never used in any cryptographic computation. It has been removed. The type is now:
```ts
type BlindSignature = { C_: WeierstrassPoint<bigint>; id: string };
```
### `createBlindSignature` — `amount` parameter removed
The `amount` parameter has been dropped from `createBlindSignature`. Amount is determined at the `OutputData` layer, not the crypto layer.
```ts
// Before
createBlindSignature(B_, privateKey, amount, id);
// After
createBlindSignature(B_, privateKey, id);
```
---
## KeyChain and KeyChainCache: multi-unit support and API cleanup
### `KeyChainCache` — `unit` field removed, `savedAt` field added
The `unit` field has been removed from `KeyChainCache`. The cache now contains keysets for **all** units at the mint. Use an explicit `unit` argument when restoring from cache.
The new `savedAt?: number` field (unix ms) is set automatically when the cache is created. Use it to implement TTL / staleness checks in your app.
```ts
// Before
type KeyChainCache = { keysets: KeysetCache[]; unit: string; mintUrl: string };
// After
type KeyChainCache = { keysets: KeysetCache[]; mintUrl: string; savedAt?: number };
```
### `KeyChain.fromCache` — explicit `unit` parameter
```ts
// Before
const chain = KeyChain.fromCache(mint, cache);
// After — unit is now the second argument
const chain = KeyChain.fromCache(mint, 'sat', cache);
```
### `KeyChain.mintToCacheDTO` — `unit` parameter removed
The first argument (`unit`) has been dropped. The cache is now mint-wide (all units).
```ts
// Before
const cache = KeyChain.mintToCacheDTO(unit, mintUrl, keysets, keys);
// After
const cache = KeyChain.mintToCacheDTO(mintUrl, keysets, keys);
```
### `KeyChain` constructor — `cachedKeysets` and `cachedKeys` removed
The optional `cachedKeysets` and `cachedKeys` constructor parameters have been removed. Use `mintToCacheDTO` + `fromCache` instead:
```ts
// Before
const chain = new KeyChain(mint, unit, keysets, keys);
// After
const cache = KeyChain.mintToCacheDTO(mintUrl, keysets, keys);
const chain = KeyChain.fromCache(mint, unit, cache);
```
### `KeyChain.getCache()` — removed
The v3-deprecated `getCache()` method has been removed. Use the `cache` getter instead.
### `KeyChainCache` now contains all units
Previously, `cache.keysets` only contained keysets for the wallet's unit. It now contains keysets for every unit the mint exposes. If you read `cache.keysets` directly and assumed single-unit contents, filter by `unit` yourself.
---
## `getEncodedTokenV3` removed
V3 token encoding (`cashuA…`) is no longer supported. `getEncodedTokenV3` has been removed and the `version` option on `getEncodedToken` has been removed entirely. V3 token **decoding** is unaffected — `getDecodedToken` still handles `cashuA` tokens.
```ts
// Before
import { getEncodedTokenV3, getEncodedToken } from '@cashu/cashu-ts';
getEncodedTokenV3(token);
getEncodedToken(token, { version: 3 });
// After — encoding proofs with legacy base64 keyset IDs throws:
// "Proofs contain a legacy keyset ID and cannot be encoded. Swap them at the mint first."
getEncodedToken({ mint, proofs: proofsWithBase64KeysetIds });
```
To resolve this, swap the proofs at the mint. `wallet.receive()` now accepts proof arrays directly, including deserialized/stored `ProofLike[]`, so no token string is needed:
```ts
const freshProofs = await wallet.receive(legacyProofs);
getEncodedToken({ mint, proofs: freshProofs }); // encodes as cashuB (v4)
```
If you have a stored `cashuA` string, you can pass that instead — v3 decoding still works:
```ts
const freshProofs = await wallet.receive('cashuAeyJ0b2tlbi...');
getEncodedToken({ mint, proofs: freshProofs }); // encodes as cashuB (v4)
```
---
## `getDecodedToken` now requires `keysetIds`
Prefer `getTokenMetadata` + `wallet.decodeToken()`
`getDecodedToken` now requires a second argument: `keysetIds: readonly string[]`. This array is used to resolve v2 short keyset IDs to their full hex counterparts.
**Passing an empty array is unsafe** — it throws the moment a token contains a v2 short keyset ID.
### Recommended migration
Instead of calling `getDecodedToken` directly, use the two-step pattern:
```ts
// Before
import { getDecodedToken } from '@cashu/cashu-ts';
const token = getDecodedToken(tokenString); // TS error in v4 — second arg required
// After — Step 1: metadata before the wallet exists
import { getTokenMetadata } from '@cashu/cashu-ts';
const meta = getTokenMetadata(tokenString);
// meta.mint, meta.unit, meta.amount (Amount), meta.incompleteProofs
// After — Step 2: build and load the wallet
// Validate meta.mint before any network call, especially in server-side code.
const wallet = new Wallet(meta.mint, { unit: meta.unit });
await wallet.loadMint(); // or wallet.loadMintFromCache(mintInfo, keyChainCache)
// After — Step 3: fully hydrate the token
const token = wallet.decodeToken(tokenString); // Token with complete Proof[]
```
### When to use each API
| API | When to use |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `getTokenMetadata(str)` | Before a wallet exists — get mint URL, unit, and amount to decide which wallet to create. Treat the mint URL as untrusted until validated |
| `wallet.decodeToken(str)` | After wallet is loaded — get the complete `Token` with full `Proof[]` |
| `getDecodedToken(str, ids)` | Advanced: you manage your own keyset cache and decode outside a wallet instance |
### If you only need amount / mint / unit
```ts
const { mint, unit, amount } = getTokenMetadata(tokenString);
const sats = amount.toNumber(); // amount is Amount, not number
```
---
## Deprecated v3 APIs now removed
These APIs were already deprecated in v3. In v4 they have been removed:
- `Wallet` constructor preload options `keys`, `keysets`, and `mintInfo`; use `loadMintFromCache()` after construction.
- Deprecated wallet method alias: `wallet.swap`; use `send`.
- `Keyset` getter aliases `active`, `input_fee_ppk`, and `final_expiry`; use `isActive`, `fee`, and `expiry`. Ensure you are looking at the Cashu-TS `Keyset` domain model: raw API `MintKeyset` / `MintKeys` DTOs may still expose the old field names.
- `preferAsync` on melt option objects; set `prefer_async: true` in the melt payload or call `completeMelt(preview, privkey, { preferAsync: true })`.
- `MeltBlanks`, `wallet.on.meltBlanksCreated(cb)`, and `onChangeOutputsCreated`; use `prepareMelt()` / `completeMelt()` with `MeltPreview`.
- Deprecated utility helpers and overloads in `src/utils/core`: `bytesToNumber`, `verifyKeysetId`, the positional `deriveKeysetId(...)` signature, and the `getDecodedToken(..., HasKeysetId[])` overload; use `Bytes.toBigInt`, `Keyset.verifyKeysetId(...)`, the options-based `deriveKeysetId(...)`, and `string[]` keyset IDs.
- Deprecated convenience aliases removed elsewhere in the API: `MintInfo.supportsBolt12Description` and `WSConnection.closeSubscription()`; use `supportsNut04Description('bolt12')` and `cancelSubscription()` instead.
- Deprecated crypto/type aliases removed in the v4 cleanup, including `BlindedMessage`; use the non-deprecated names such as `RawBlindedMessage`.
---
## NUT-11 / P2PK API changes
v4 trims the public NUT-11 surface and moves callers toward two supported entry points:
- `getP2PKExpectedWitnessPubkeys(secret)` if you only need to know which pubkeys can currently sign
- `verifyP2PKSpendingConditions(proof, logger?, message?)` if you need the full lock/refund evaluation result
### Removed deprecated aliases
These older exports are gone in v4:
- `parseP2PKSecret(Uint8Array)` overload
- `WellKnownSecret`
- `signP2PKSecret`
- `verifyP2PKSecretSignature`
- `getP2PKExpectedKWitnessPubkeys`
- `verifyP2PKSig`
Use these instead:
- `parseP2PKSecret(string | Secret)`
- `SecretKind`
- `schnorrSignMessage(...)`
- `schnorrVerifyMessage(...)`
- `getP2PKExpectedWitnessPubkeys(...)`
- `isP2PKSpendAuthorised(...)` or `verifyP2PKSpendingConditions(...)`
### Removed low-level NUT-11 getters
These helpers are no longer public:
- `getP2PKWitnessPubkeys`
- `getP2PKWitnessRefundkeys`
- `getP2PKLocktime`
- `getP2PKLockState`
- `getP2PKNSigs`
- `getP2PKNSigsRefund`
If your code previously called those helpers and stitched the result together manually, migrate to `verifyP2PKSpendingConditions()` and read the returned metadata instead.
```ts
// Before
const lockState = getP2PKLockState(proof.secret);
const locktime = getP2PKLocktime(proof.secret);
const mainKeys = getP2PKWitnessPubkeys(proof.secret);
const refundKeys = getP2PKWitnessRefundkeys(proof.secret);
const required = getP2PKNSigs(proof.secret);
const refundRequired = getP2PKNSigsRefund(proof.secret);
// After
const result = verifyP2PKSpendingConditions(proof);
const { lockState, locktime } = result;
const mainKeys = result.main.pubkeys;
const refundKeys = result.refund.pubkeys;
const required = result.main.requiredSigners;
const refundRequired = result.refund.requiredSigners;
```
### `P2PKVerificationResult` shape changed
`verifyP2PKSpendingConditions()` still returns a detailed result object, but signer metadata is now grouped by path:
```ts
// Before
result.requiredSigners;
result.eligibleSigners;
result.receivedSigners;
// After
result.locktime;
result.main.requiredSigners;
result.main.pubkeys;
result.main.receivedSigners;
result.refund.requiredSigners;
result.refund.pubkeys;
result.refund.receivedSigners;
```
This makes the result unambiguous when both main and refund paths exist.
### `P2PKBuilder` now follows the same pubkey identity rules as NUT-11
`P2PKBuilder.addLockPubkey()` and `addRefundPubkey()` now normalize and deduplicate keys by x-only pubkey identity. In practice, that means `02...` and `03...` encodings of the same x-only key are treated as the same signer, and the first one added wins.
If your code relied on storing both encodings as distinct entries, update those expectations:
```ts
// Before
new P2PKBuilder().addRefundPubkey(['03' + xOnly, '02' + xOnly]).toOptions().refundKeys;
// => ['03' + xOnly, '02' + xOnly]
// After
new P2PKBuilder().addRefundPubkey(['03' + xOnly, '02' + xOnly]).toOptions().refundKeys;
// => ['03' + xOnly]
```
### `P2PKBuilder.requireLockSignatures()` and `requireRefundSignatures()` now throw for invalid input
Previously these methods silently clamped the value to at least 1 and truncated non-integers. They now throw if the argument is not a positive integer (`n < 1` or non-integer).
```ts
// Before — invalid values were silently clamped
builder.requireLockSignatures(0); // stored as 1 (clamped)
builder.requireLockSignatures(1.7); // stored as 1 (truncated)
// After — throws immediately
builder.requireLockSignatures(0); // throws: 'requiredSignatures must be a positive integer'
builder.requireLockSignatures(1.7); // throws: 'requiredSignatures must be a positive integer'
```
Ensure any value passed to these methods is a positive integer, or guard it beforehand:
```ts
const n = Math.max(1, Math.trunc(rawValue));
builder.requireLockSignatures(n);
```
---
## Generic mint/melt quote and proof methods
The v3-deprecated wallet method aliases (`createMintQuote`, `checkMintQuote`, `mintProofs`, `createMeltQuote`, `checkMeltQuote`, `meltProofs`) were removed. v4 also adds new generic methods that accept a `method` string as the first parameter, primarily to support custom payment methods (e.g., BACS, SWIFT) without requiring first-class library support.
### New generic methods on `Wallet`
| Method | Description |
| ------------------------------------------------------------------- | ------------------------------------------ |
| `createMintQuote(method, payload, options?)` | Create a mint quote for any payment method |
| `checkMintQuote(method, quote, options?)` | Check a mint quote for any payment method |
| `mintProofs(method, amount, quote, config?, outputType?)` | Mint proofs for any payment method |
| `createMeltQuote(method, payload, options?)` | Create a melt quote for any payment method |
| `checkMeltQuote(method, quote, options?)` | Check a melt quote for any payment method |
| `meltProofs(method, meltQuote, proofsToSend, config?, outputType?)` | Melt proofs for any payment method |
### New generic methods on `Mint` (low-level HTTP)
| Method | Description |
| -------------------------------------------- | ------------------------------------- |
| `createMintQuote(method, payload, options?)` | POST `/v1/mint/quote/{method}` |
| `checkMintQuote(method, quote, options?)` | GET `/v1/mint/quote/{method}/{quote}` |
| `createMeltQuote(method, payload, options?)` | POST `/v1/melt/quote/{method}` |
| `checkMeltQuote(method, quote, options?)` | GET `/v1/melt/quote/{method}/{quote}` |
The existing bolt11/bolt12 convenience methods (`createMintQuoteBolt11`, `meltProofsBolt11`, etc.) remain the recommended APIs for built-in methods. Use the generics when you need custom methods or intentionally want the lower-level method-oriented flow.
### `Mint.mint()` and `Mint.melt()` options signature change
The `options` parameter on `Mint.mint()` and `Mint.melt()` has been extended with an optional `normalize` callback. If you spread the options object or type it explicitly, update accordingly:
```ts
// Before
await mint.mint('bolt11', payload, { customRequest: myFetch });
await mint.melt('bolt11', payload, { customRequest: myFetch });
// After — unchanged for basic usage, but the options type now includes `normalize`
await mint.mint('bolt11', payload, { customRequest: myFetch });
await mint.melt('bolt11', payload, { customRequest: myFetch });
```
### Migration for v3 deprecated aliases
If you were still using the v3 deprecated aliases, update to the bolt11-specific methods or the new generics:
```ts
// Before (v3 deprecated aliases — these were bolt11 only)
const quote = await wallet.createMintQuote(64);
const checked = await wallet.checkMintQuote(quote.quote);
const proofs = await wallet.mintProofs(64, quote);
const meltQuote = await wallet.createMeltQuote(invoice);
const meltChecked = await wallet.checkMeltQuote(meltQuote.quote);
const result = await wallet.meltProofs(meltQuote, proofsToSend);
// After — option A: use the bolt11-specific methods (recommended for bolt11)
const quote = await wallet.createMintQuoteBolt11(64);
const checked = await wallet.checkMintQuoteBolt11(quote.quote);
const proofs = await wallet.mintProofsBolt11(64, quote);
const meltQuote = await wallet.createMeltQuoteBolt11(invoice);
const meltChecked = await wallet.checkMeltQuoteBolt11(meltQuote.quote);
const result = await wallet.meltProofsBolt11(meltQuote, proofsToSend);
// After — option B: use the new generics (for custom payment methods)
const quote = await wallet.createMintQuote('bacs', { amount: 5000n, ... });
const checked = await wallet.checkMintQuote('bacs', quote.quote);
const proofs = await wallet.mintProofs('bacs', 5000, quote);
```
### Normalize callback
All generic methods accept an optional `normalize` callback for coercing method-specific response fields (e.g., converting wire numbers to `Amount` objects). The callback receives the raw wire data after base normalization has already been applied:
```ts
type BacsQuoteRes = MintQuoteBaseResponse & { amount: Amount; reference: string };
const quote = await wallet.createMintQuote<BacsQuoteRes>(
'bacs',
{
amount: 5000n,
sort_code: '12-34-56',
},
{
normalize: (raw) => ({
...(raw as BacsQuoteRes),
amount: Amount.from(raw.amount as AmountLike),
}),
},
);
```
For melt quotes, base fields (`amount`, `expiry`, `change`) are always normalized automatically. For bolt11/bolt12, `fee_reserve` and `request` are also normalized. The `normalize` callback runs last, after all built-in normalization.
---
## Finance Helpers — going further with `Amount`
If you adopt `Amount` natively, it has methods to replace common float-based patterns with exact integer arithmetic:
- `ceilPercent(numerator, denominator = 100)` for rounded-up percentages
- `floorPercent(numerator, denominator = 100)` for conservative lower bounds
- `scaledBy(numerator, denominator)` for proportional rescaling
- `clamp(min, max)` for bounding into a closed range
- `inRange(min, max)` for inclusive range checks
```ts
const fee = amount.ceilPercent(2).clamp(2, amount);
const maxSpend = amount.floorPercent(98);
const adjusted = estInvAmount.scaledBy(tokenAmount, neededAmount).subtract(1);
const bounded = fee.clamp(MIN_FEE, tokenAmount);
if (msats.inRange(data.minSendable, data.maxSendable)) { ... }
```
---
## Multi-unit support: `AmountWithUnit`
If your application handles more than one currency unit (e.g. `sat` and `usd` from the same mint, or wallets across multiple mints with different units), `AmountWithUnit` is the unit-aware sibling of `Amount`. It refuses to silently mix units in arithmetic or comparison.
`AmountWithUnit` is a thin wrapper composing an `Amount` and a `unit: string`. The two type families are deliberately distinct — `AmountWithUnit.add` accepts only `AmountWithUnit`, and there is no implicit coercion from one to the other. Lifting is always explicit.
```ts
import { Amount, AmountWithUnit, AmountWithUnitError } from '@cashu/cashu-ts';
const a = AmountWithUnit.from(100, 'sat');
const b = AmountWithUnit.from(50, 'sat');
const c = AmountWithUnit.from(5, 'usd');
a.add(b); // AmountWithUnit { amount: 150, unit: 'sat' }
a.add(c); // throws AmountWithUnitError: unit mismatch
a.toAmount().add(b.toAmount()); // Amount 150 — raw arithmetic, intentional escape hatch
// Lift a unitless Amount when needed:
Amount.from(21).withUnit('sat'); // AmountWithUnit
```
Single-unit consumers (the common case — one `Wallet` per mint+unit) don't need this. It exists for downstream wallets that aggregate across units (balances, transfers, multi-currency display) without reinventing the unit-equality bookkeeping.
`AmountWithUnit` exposes the same arithmetic, comparison, and finance helpers as `Amount`, plus `static AmountWithUnit.sum(iter, unit?)` for aggregating a unit-tagged iterable.
### Implicit coercion is intentionally restricted
To keep the unit guard meaningful, `AmountWithUnit` overrides JS coercion so that the unit cannot be silently stripped:
- **String coercion** (`String(x)`, `` `${x}` ``, `.toString()`) returns a unit-bearing form like `"sat: 100"`. The unit-first ordering is deliberate: `parseInt`/`parseFloat` on the string form return `NaN` rather than silently extracting the bare number.
- **Numeric / default coercion** (`+x`, `x - 1`, `x == 5`, `Number(x)`, `x + y` between two `AmountWithUnit`) **throws** `AmountWithUnitError`. Use `.toAmount()` to get the unitless `Amount` if you genuinely need raw arithmetic.
- **JSON** is unaffected — `JSON.stringify(x)` uses `toJSON()` and emits `{"amount":"...","unit":"..."}`.
---
## New: `createEphemeralCounterSource` factory
v4 adds a public factory for the built-in in-memory `CounterSource`:
```ts
import { createEphemeralCounterSource } from '@cashu/cashu-ts';
const counters = createEphemeralCounterSource(loadCountersFromDb());
```
Previously, consumers who needed a shared `CounterSource` across multiple wallet instances had to either deep-import the internal `EphemeralCounterSource` class or reimplement the interface. The factory provides the same capability without exposing the concrete class.
This is useful when your app creates multiple short-lived wallet instances for the same seed — passing a shared `counterSource` prevents concurrent operations from reserving overlapping counter ranges. See the [deterministic counters guide](./docs-src/deterministic_counters.md) for the full pattern.
---