# Catalog

The reference-data store behind every symbol lookup and decimal conversion, with typed readers for markets, ledger assets, order constraints, and zipper routes.

`client.catalog` is a snapshot of venue reference data (assets, pairs, scales, routes) with typed readers on top. Every service consults it to turn symbols into ids and wire integers into exact decimal strings. This page documents the reader surface; for the design and the decimal-string contract, see [Catalog & precision](https://testnet.polyester.com/docs/sdk/typescript/concepts/catalog-and-precision).

The four readers all hang off the catalog:

| Reader           | Covers                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| `catalog.market` | Trading assets and pairs: symbol and id lookups, listing status, price and quantity conversions. |
| `catalog.ledger` | Ledger assets: id and symbol lookups, amount conversion and display formatting.                  |
| `catalog.orders` | Per-pair order constraints (tick, step, minimums) and order input validation.                    |
| `catalog.zipper` | Deposit and withdraw chains, unified assets, per-chain routes, and contracts.                    |

Across every reader, `get*` and `lookup*` methods return `null` on a miss, and `require*` methods throw a `CatalogLookupError` from `@polyester/sdk/catalogs`.

## Lifecycle

The catalog starts empty and fills itself from two public endpoints (spot config and zipper config) the first time something needs it. Service methods await readiness internally, so you only manage the lifecycle yourself before direct reader reads.

```ts
await client.catalog.ensureReady(); // fetch if empty, then resolve the snapshot
```

| Method           | Behavior                                                                        |
| ---------------- | ------------------------------------------------------------------------------- |
| `ensureReady()`  | Resolve the current snapshot, fetching if the catalog is empty.                 |
| `ready()`        | Passive: resolve the current snapshot or an in-flight refresh, never start one. |
| `refresh()`      | Force a refetch (deduplicated while one is in flight).                          |
| `state()`        | `{ status }` where status is `empty`, `refreshing`, `fresh`, or `stale`.        |
| `snapshot()`     | The current `CatalogSnapshot`, or throw `CatalogNotReadyError` if empty.        |
| `setSnapshot(s)` | Replace the snapshot with one you already have.                                 |

`stale` means a refresh failed and the previous snapshot is still being served rather than dropped.

> **You rarely call ensureReady before service methods**
>
> Any service method that needs reference data awaits readiness for you. Call `ensureReady()` yourself only before reading a reader directly, as in the examples below.

## `catalog.market`

Assets and trading pairs, plus exact price and quantity conversions. Enriched pairs retain `baseQuantityScale` and `quoteQuantityScale` from the spot config. These fields can be absent in older snapshots; freshly populated catalogs preserve them.

Assets also carry an optional `marketDataVolumeScale`, used to decode candle volume and market-overview 24h base volume. Pairs carry an optional `referencePriceScale`, used to decode composite reference candle prices. Snapshots without these fields fall back to the asset quantity scale and the 9-decimal trading price scale.

```ts
await client.catalog.ensureReady();

const symbolId = client.catalog.market.requireSymbolIdByPairSymbol("BTC-USDT");
const pair = client.catalog.market.requirePairBySymbol("BTC-USDT");
const listed = client.catalog.market.listPairs({ listed: true });
```

Lookups (each has a `get*` null-returning and a `require*` throwing form):

- Assets: `listAssets()`, `getAsset(key)` / `requireAsset(key)`, `getAssetBySymbol(sym)` / `requireAssetBySymbol(sym)`, `getAssetByLedgerId(id)` / `requireAssetByLedgerId(id)`.
- Pairs: `listPairs(filter?)`, `getPair(key)` / `requirePair(key)`, `getPairBySymbol(sym)` / `requirePairBySymbol(sym)`, `getPairBySymbolId(id)` / `requirePairBySymbolId(id)`.
- Id and symbol crosswalk: `getSymbolIdByPairSymbol(sym)` / `requireSymbolIdByPairSymbol(sym)`, `getPairSymbolBySymbolId(id)` / `requirePairSymbolBySymbolId(id)`.

Conversions and formatting (all exact, all keyed by a pair):

```ts
// Normalize UI input to the pair's precision by truncating excess fractional digits
client.catalog.market.normalizePriceInput("64250.512345", "BTC-USDT");
client.catalog.market.normalizeQuantityInput("0.2500", "BTC-USDT");

// Strict decimal string <-> scaled integer conversion
client.catalog.market.decimalPriceToTicks("64250.5", "BTC-USDT");
client.catalog.market.decimalQuantityToScaled("0.25", "BTC-USDT");
client.catalog.market.priceTicksToDecimalString(64250500000000n, "BTC-USDT"); // "64250.5"

// Display-rounded strings for UI
client.catalog.market.formatPrice("64250.5", "BTC-USDT");
client.catalog.market.formatQuantity("0.25", "BTC-USDT");
```

Keys accept a pair symbol (`"BTC-USDT"`), a numeric `symbolId`, or `{ symbol }` / `{ symbolId }`.

`normalizePriceInput` and `normalizeQuantityInput` are for partial form input: they truncate to catalog scale but do not round, snap to a tick grid, or check order constraints. The strict converters reject excess precision with `CatalogConversionError`, exported from `@polyester/sdk/catalogs`.

## `catalog.ledger`

Ledger assets, addressed by symbol or numeric ledger id, with amount conversion and display formatting.

```ts
const usdc = client.catalog.ledger.requireAssetBySymbol("USDC");
const display = client.catalog.ledger.formatAmount("1234.5", "USDC"); // display-rounded
```

## `catalog.orders`

Per-pair order constraints and order-input validation. Use these to build order forms and to pre-validate before calling `client.orders.create`.

```ts
const constraints = client.catalog.orders.getSpotOrderConstraints("BTC-USDT");
// tick size, step size, minimums, and protobuf wire ceilings

const result = client.catalog.orders.validateSpotOrderDecimalInput({
	pair: "BTC-USDT",
	price: "64250.5",
	quantity: "0.25",
});
if (!result.valid) console.log(result.errors);
```

- `getSpotOrderConstraints(pair)` returns tick, step, and minimum values, plus the decimal-string wire ceilings `maxPrice` for price, `maxQtyBase` for base quantity, `maxNotionalQuote` for quote amount, and `maxQuoteSlippage` for quote-denominated slippage.
- `validateSpotOrderDecimalInput(input)` returns `{ valid, errors }`, a structured list you can render inline in a form.
- `assertSpotOrderDecimalInput(input)` throws a `CatalogValidationFailedError` on the first violation instead of returning errors.

Use these validators when you need pre-submit feedback. `orders.create` and `orders.modify` still perform strict shape and decimal-scale conversion, but do not automatically validate current tick, step, or minimum rules. The validator checks parsing, tick, step, and minimum rules. It does not enforce the four maxima. Those maxima are protobuf wire-format ceilings, not exchange limits. Order and trigger methods throw `CatalogConversionError` before network I/O when a decimal exceeds the corresponding ceiling. The backend is authoritative because catalog and balance state can change.

## `catalog.zipper`

Use `environment.chain.id` for the Polyester chain ID. Zipper configuration and catalog snapshots do not expose `polyesterChainId`.

Deposit and withdraw chains, unified assets, and the per-chain routes that back [deposits and withdrawals](https://testnet.polyester.com/docs/sdk/typescript/guides/deposits-and-withdrawals).

```ts
const chains = client.catalog.zipper.listChains();
const eth = client.catalog.zipper.requireAssetBySymbol("ETH");
const route = client.catalog.zipper.requireAssetChain("ETH", "ethereum-sepolia");
```

Lookups: `listChains()`, `requireChain(key)`, `requireChainByCode(code)`, `requireChainById(id)`, `requireChainIdByCode(code)`, `requireAssetBySymbol(sym)`, `requireAssetChain(asset, chain)`, and `requireAssetChainByZippedAssetId(id)`.

## Unknown assets never crash

Events that reference an asset the catalog does not know resolve to a sentinel "unknown asset" rather than throwing mid-stream. Detect them with the `isUnknownLedgerAsset` and `isUnknownZipperAsset` guards exported from `@polyester/sdk/catalogs`.

## Owning the snapshot

By default each client fetches and holds its own snapshot. Three config options change that, all covered in [Client configuration](https://testnet.polyester.com/docs/sdk/typescript/reference/client-configuration):

- `catalogSnapshot` seeds a client with a snapshot you already have (typically from SSR: the server calls `catalog.snapshot()`, bakes it into the page, and the browser client starts warm).
- `catalogCell` backs the snapshot with external, optionally reactive storage you control.
- `catalog` injects a fully managed `ClientCatalog` instance (mutually exclusive with the other two).

The `@polyester/sdk/catalogs` subpath exports the builders and reader types for these patterns: `createPolyesterCatalog`, `buildCatalogSnapshot`, `createCatalogSnapshotReader`, `patchZipperCatalogSupply`, the unknown-asset guards, and the reader and config types.

## Related

- [Catalog & precision](https://testnet.polyester.com/docs/sdk/typescript/concepts/catalog-and-precision) for the design.
- [Orders](https://testnet.polyester.com/docs/sdk/typescript/reference/orders) and the [Trading guide](https://testnet.polyester.com/docs/sdk/typescript/guides/trading) for order validation in context.
- [Errors](https://testnet.polyester.com/docs/sdk/typescript/reference/errors) for the catalog error classes.
