# Catalog & precision

The reference-data store behind every conversion, and why the SDK speaks exact decimal strings.

Two facts shape the data model:

1. The wire protocol carries scaled integers. A price is ticks. A quantity is an integer in per-asset units. Exact and compact, but only useful if you know each asset's scale.
2. JavaScript `number` cannot represent money safely.

The SDK answers both with one design: a catalog of reference data (every scale), and a public surface of exact decimal strings (no floating point).

## The decimal-string contract

- Every price/quantity output is a decimal string, converted exactly from the wire integer. No rounding. Trailing zeros trimmed. `1500000n` at scale 6 becomes `"1.5"`.
- Service inputs use strict decimal conversion. `"0.1234567"` for a 6-decimal field throws `CatalogConversionError` from `@polyester/sdk/catalogs`; the SDK never rounds an order silently. Zero padding beyond the scale is exact, so `"1.5000000"` is accepted for that same field.
- `SpotOrderConstraints` exposes `maxPrice`, `maxQtyBase`, `maxNotionalQuote`, and `maxQuoteSlippage` as decimal strings. These are protobuf wire-format ceilings, not exchange limits. Order and trigger inputs above them throw `CatalogConversionError` before network I/O.
- `normalizePriceInput` and `normalizeQuantityInput` are different: UI helpers that truncate excess fractional digits to catalog scale. They do not round, snap to a tick grid, or validate tick, step, or minimum rules.
- Trading prices, price deltas, and slippage always use 9 decimal places (`PRICE_SCALE = 9`). Quantity scales vary per asset and come from the catalog. Public market-data volume and composite reference candle prices have their own catalog scales; see [Catalog](https://testnet.polyester.com/docs/sdk/typescript/reference/catalog#catalogmarket).

Do arithmetic with a decimal library or plain string math. The strings parse cleanly into either.

## What the catalog holds

`client.catalog` is a venue reference-data snapshot with typed readers:

| Reader           | Contents                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------ |
| `catalog.market` | Trading assets and pairs: symbols to ids, listing status, precision, price/amount converts |
| `catalog.ledger` | Ledger assets: id/symbol lookups, amount converts and display formatting                   |
| `catalog.orders` | Per-pair tick, step, minimum, and wire constraints plus order input validation             |
| `catalog.zipper` | Deposit/withdraw chains, unified assets, per-chain routes, contracts                       |

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

const pair = client.catalog.market.requirePairBySymbol("BTC-USDT");
const constraints = client.catalog.orders.getSpotOrderConstraints("BTC-USDT");
const route = client.catalog.zipper.requireAssetChain("ETH", "ethereum-sepolia");

// conversions and display helpers
client.catalog.market.normalizePriceInput("64250.5123456789", "BTC-USDT"); // "64250.512345678"
client.catalog.market.decimalPriceToTicks("64250.5123456789", "BTC-USDT"); // throws CatalogConversionError
client.catalog.ledger.formatAmount("1234.5", "USDC"); // display-rounded
```

`get*` returns `null` on a miss. `require*` throws `CatalogLookupError`.

The catalog order validator checks decimal parsing, tick, step, and minimum rules. It does not enforce the four wire ceilings.

## Lifecycle: ready, refresh, states

The catalog starts empty. It fills from two public endpoints (spot config + zipper config) the first time something needs it.

- `ensureReady()`: resolve the current snapshot, fetching if empty. Service methods await this internally. Call it yourself before direct catalog reads.
- `ready()`: wait for the current snapshot or in-flight refresh. Never starts a fetch.
- `refresh()`: force a refetch (deduped while in flight).
- `state()`: `empty`, `refreshing`, `fresh`, or `stale` (keeps serving the old snapshot after a failed refresh).

Realtime subscriptions gate event delivery behind readiness. Events that arrive before scales are known are buffered and flushed in order. The buffer is bounded, and a readiness failure ends the subscription through `onError` rather than leaving it connected and silent.

> **Unknown assets never crash**
>
> Events that reference assets the catalog does not know resolve to a sentinel unknown asset (`isUnknownLedgerAsset` / `isUnknownZipperAsset`) instead of throwing mid-stream. Orders on unknown symbols are filtered out of list responses.

## Owning the snapshot

By default each client fetches and holds its own snapshot. Three options change that:

- `catalogSnapshot`: seed with a snapshot you already have. Typical SSR path: server calls `catalog.snapshot()`, bakes it into the page, browser client starts warm.
- `catalogCell`: back the snapshot with storage you control. A reactive cell (store or signal) makes every catalog read reactive, one snapshot shared across clients and UI.
- `catalog`: inject a fully managed `ClientCatalog` (advanced; mutually exclusive with the other two).

The `@polyester/sdk/catalogs` subpath exports the builders (`createPolyesterCatalog`, `buildCatalogSnapshot`, `createCatalogSnapshotReader`) and the reader/config types for these patterns.
