Architecture
The adapter boundary between the interface and execution, and how contracts replace the simulation.
The interface is written against one interface, ExchangeAdapter. Market data, pre-trade estimates, order placement, cancellation and balances all pass through it, so the execution venue is a swappable implementation detail rather than something threaded through the components.
The adapter boundary
No component imports the simulation or the matching engine directly for anything that would eventually be onchain. That single rule is what makes the diagram above true rather than aspirational.
export interface ExchangeAdapter {
readonly id: "simulation" | "onchain";
readonly isLive: boolean;
// market data
getTicker(symbol: string, at: number): Ticker;
getCandles(symbol: string, timeframe: Timeframe, count: number, at: number): readonly Candle[];
getOrderBook(query: OrderBookQuery): OrderBookSnapshot;
getRecentTrades(symbol: string, at: number, count: number): readonly PublicTrade[];
// account reads
getBalances(): readonly Balance[];
getOpenOrders(symbol?: string): readonly Order[];
// pre-trade
estimate(symbol, side, amount, at, limitPrice?): ExecutionEstimate | null;
validate(draft: OrderDraft, at: number): ValidationResult;
// writes
placeOrder(draft: OrderDraft, at: number): Promise<PlaceOrderResult>;
cancelOrder(orderId: string, at: number): Promise<boolean>;
}Layers
| Layer | Location | Responsibility |
|---|---|---|
| Interface | src/app, src/components | Routes, terminal, order ticket, portfolio, docs |
| Adapter | src/lib/exchange/adapter.ts | The only contract the interface depends on |
| Simulation | src/lib/exchange/mockAdapter.ts | Active implementation: market data plus local matching |
| Onchain | src/lib/exchange/contractAdapter.ts | Stub for deployed contracts; every method throws today |
| Market data | src/lib/exchange/marketData.ts | Deterministic prices, candles, depth and prints |
| Engine | src/lib/exchange/orderEngine.ts | Validation, book walking, fills, resting orders, fees |
| State | src/state | Balances, orders, trades, activity, UI preferences |
| Config | src/config | Brand, chain and market registries |
Replacing the simulation
contractAdapter exists and is deliberately unimplemented: every method throws instead of returning plausible-looking data, which keeps the codebase honest about what is connected. Wiring a real deployment means filling in that one file and setting the addresses in chain configuration.
- Map a
Marketto the sorted token pair and the active book epoch. - Convert display prices to the engine's tick representation and sizes to raw units.
- Submit orders with the flags implied by the selected time in force, and read resting quantity back from the book.
- Derive order status from book presence and remaining quantity, as an indexer would.
- Read balances from chain state instead of local storage.
// src/lib/exchange/index.ts
export const exchange: ExchangeAdapter = hasDeployment ? contractAdapter : mockAdapter;Rendering and data flow
- Market data is read through hooks with per-surface refresh rates: the book updates several times a second, a market list once a second.
- Time comes from one shared clock, so every panel renders the same instant and the market never appears to disagree with itself.
- Persisted stores hydrate explicitly, so the first client render matches the server render.
- The terminal mounts one layout at a time — desktop grid or mobile tabs — rather than rendering both and hiding one.