Framework-level building blocks for Lighter.xyz JavaScript clients: the client-side state store, the websocket pipeline that feeds it, the trading math, and the types that describe it all.
It is UI-free. There are no components here — only state, selectors, pure functions and types. Rendering is the host app's job.
This package ships with no API clients, no storage, no crypto, and no signers. The host application injects all of them at boot:
import { initCommonPackage } from '@elliottech/react-store'
initCommonPackage({
env: 'mainnet',
// storage — localStorage on web, AsyncStorage/MMKV on mobile
getItem, setItem,
// crypto — WebCrypto on web, a native module on mobile
sha256,
// generated REST clients from `zklighter-perps`
accountApi, orderApi, infoApi, /* ...and the rest */
// transaction signing (WASM on web, native on mobile)
signers, getApiKeyIndex, getPlatform, isRegistered,
// host-side plumbing
websocketConfigParam, captureException, showToastFromError,
})Until then the singletons in lib/ hold null! or no-op stubs and will
throw or silently do nothing. See lib/README.md for why it's
built this way.
store/useLighterStore.ts creates a single module-level
Zustand store, composed from nine slices. It exists the moment the module is
imported, so it can be read from anywhere — React components, websocket handlers,
plain functions — via useLighterStore.getState().
Because it's created at import time, it is seeded with the bundled snapshots in
fallbacks/ so the app can render markets before the first network
response arrives.
Every market has a multiplier. Prices and sizes on the wire ("real") are not
what users see ("display"):
display_size = real_size × multiplier
display_price = real_price ÷ multiplier
Mixing the two up is the single easiest way to introduce a pricing bug, so the
conversion helpers live in one place — utils/multiplier.ts
— and are documented in utils/README.md.
┌──────────────────────────────┐
REST │ hooks/ (react-query) │
(initial ─────▶ useInitTokens, useInitL1Info,│───┐
+ poll) │ useInit{Asset,OrderBook}Metas│ │
└──────────────────────────────┘ │
▼
WebSocket ┌───────────┐ ┌──────────────┐ ┌─────────────────┐
(live) ──────▶ lighter-ws├──▶ ws-sub-store ├──▶ store/ │
│ decode + │ │ buffer + │ │ useLighterStore│
│ transform│ │ throttle │ └────────┬────────┘
└───────────┘ └──────────────┘ │
▼
┌─────────────────┐
│ store/**/ │
│ selectors.ts │ ← memoized (reselect)
└────────┬────────┘
▼
┌─────────────────┐
│ formulas/ │ ← pure math
└────────┬────────┘
▼
host UI
Reads flow one way: store → selectors → formulas → UI. Writes come from the
websocket bridge, the init hooks, and actions/.
| Directory | What's in it |
|---|---|
store/ |
The Zustand store: nine slices, and the memoized selectors that read them. The largest and most intricate part of the package. |
ws-sub-store/ |
The bridge from websocket messages to store writes. Buffers high-frequency updates and flushes on an interval. |
lighter-ws/ |
The websocket client itself, plus the full type surface of every server message. |
formulas/ |
Pure trading math — PnL, margin, liquidation prices, order matching, SL/TP. No store access, no I/O. |
utils/ |
Unit conversion, decimal-safe rounding, candlestick assembly, fee tiers, input sanitizing. |
types/ |
Domain types, and the compatibility layer that narrows the generated zklighter-perps types. |
lib/ |
The dependency-injection registry — mutable singletons filled in by initCommonPackage. |
hooks/ |
The four react-query hooks that load reference data (tokens, markets, assets, L1 info) into the store. |
fallbacks/ |
Bundled JSON snapshots of market/asset/token metadata, so the store is populated at import time. |
constants/ |
Shared constants: protocol addresses, tx statuses, slippage limits, date formats, and the field lists used to trim API payloads. |
actions/ |
Imperative store writes that are too stateful for a selector. |
images/ |
CDN URL builders for token and chain icons. No bundled image assets. |
testing/ |
isTestingEnvironment() — a single flag read from process.env.VITE_PLAYWRIGHT. |
- Barrel files. Every directory has an
index.tsre-exporting its public surface, andpublic/index.tsre-exports all of them. Import from the package root; the deep paths are an implementation detail. select*is a selector, takes(state)or(state, params), and is memoized.compute*is a pure formula and takes plain values.- Enums are const objects.
MarginMode,OrderType, and friends use theas const+ lookup-type pattern rather than TSenum, so they surviveisolatedModulesand erase cleanly. - Tests are colocated as
*.test.tsand run withvitest.
yarn install # also wires the git hooks (.githooks)
yarn typecheck && yarn lint && yarn test --run
yarn build # emits dist/, the only directory that is published
yarn scan:secretsA pre-commit hook scans staged files for secrets, and yarn npm publish runs
the same scan over the whole tree (including dist/) via prepack. Both use
secretlint with the recommended
preset; exclusions go in .secretlintignore.
CI is GitHub Actions (.github/workflows): every PR and
push to main is typechecked, linted, tested and built, and main publishes
the version in package.json to npm when it is not published yet. Publishing
goes through npm trusted publishing,
so there is no npm token in the repo: publish.yml is registered as the
package's trusted publisher, and provenance is attached automatically.