Trading Frontend
React-based trading interface with real-time WebSocket data, order book visualization, and order placement.
Overview
The trading frontend (apps/web) is a React + TypeScript single-page application with three pages: Exchange (trading), Wallet (portfolio + bridge), and an Admin panel. It connects to the exchange via REST and WebSocket, and to the EVM via wagmi/viem RPC calls.
Stack: Vite 6, React 19, Zustand 5, CSS Modules, TypeScript 5.7, wagmi/viem, @reown/appkit (WalletConnect)
Design: Hyperliquid-inspired dark theme with blue accent (#4A90E2), muted green/red trading colors, JetBrains Mono typography.
Running the Frontend
Prerequisites
- Bun package manager
- Backend running on port 3000 (
OLYMPUS_ENV=dev cargo run)
Development
# Install dependencies
make web-install
# Start dev server (port 5173, proxies to backend on 3000)
make web-devThe Vite dev server proxies /api and /ws requests to http://localhost:3000, so the backend must be running.
Production Build
make web-buildOutputs to apps/web/dist/.
Architecture
Pages
Path-based routing in main.tsx (no router library):
| Path | Page | Description |
|---|---|---|
/ | Exchange | Trading interface with order book, chart, trade form |
/wallet | Wallet | Unified portfolio view with bridge deposit/withdraw |
/admin | Admin | Admin panel for credits, instruments, bridge, OLP distribution |
Navigation via a burger menu in the top-left of the shared header.
Exchange Layout
Three-column grid with a fixed header and bottom panel:
+-----------------------------------------------------+
| [≡] Olympus | [Instrument v] | * connected | [●] |
+----------+----------------------+-------------------+
| | | |
| Order | Candlestick Chart | Trade Form |
| Book | | (Buy/Sell) |
| | | |
| 300px | flex | 320px |
+----------+----------------------+-------------------+
| [Balances] [Open Orders] [Order History] |
+-----------------------------------------------------+The header shows a burger menu (≡), logo, instrument selector, connection status dot, and a deterministic gradient avatar for the connected wallet.
State Management
Zustand stores manage application state:
| Store | Purpose |
|---|---|
accountStore | Connected wallet address (synced from wagmi) |
marketStore | Active symbol, instrument list from /api/v1/exchangeInfo |
orderBookStore | Bids, asks, best bid/ask, lastUpdateId — initialized from REST, updated from WS diffs |
tradeStore | Last price + direction (from aggTrade), recent trades for display |
klineStore | OHLCV candles — initialized from REST, updated from WS kline + aggTrade |
balanceStore | Per-asset balances, open orders, order history — REST + WS driven |
Data Flow
- On mount,
Appfetches instruments viaGET /api/v1/exchangeInfo useWebSocketopens a WebSocket to/wsand subscribes todepthUpdate,aggTrade, andklinefor the active symbol, plusallMids- The hook fetches an initial order book snapshot via
GET /api/v1/depth, then applies incremental WS diffs on top - Historical candles load from
GET /api/v1/klines. The WSklinefeed provides the in-progress candle (handles mid-candle page loads).aggTradeinterpolates the close price between kline snapshots for smooth TradingView-style movement. - When the user switches instruments, the hook unsubscribes from the old symbol, subscribes to the new one, and re-fetches the snapshot
- When a wallet is connected, the WebSocket authenticates via a challenge-response flow (EIP-191
personal_sign) and subscribes toaccountUpdatefor real-time balance, open order, and order event updates. A session token is issued and persisted so subsequent page loads skip the wallet prompt useBalancesfetches initial balances, open orders, and order history from REST on mount; subsequent updates arrive via the WebSocketbalanceUpdatechannel- Order submission calls
POST /api/v1/orderwith an EIP-712 signature. Market and limit orders are supported
WebSocket Connection
The useWebSocket hook manages the connection lifecycle:
- Auto-reconnect with exponential backoff (1s initial, 30s max)
- Connection status displayed as a colored dot in the header (green = connected, cyan = connecting, red = disconnected)
- Symbol resubscription — changing instruments triggers unsubscribe + subscribe without reconnecting
- Order book sync — on connect or symbol change, subscribes to
depthUpdatefirst, then fetches a REST snapshot. Diffs arriving during the fetch are buffered and drained after the snapshot loads, ensuring no updates are lost
Components
Header
- Burger menu — opens dropdown to navigate between Exchange, Wallet, and Bridge pages
- InstrumentSelector — dropdown populated from
exchangeInfo, triggers WebSocket resubscription on change - AccountWidget — WalletConnect / MetaMask connect button via @reown/appkit modal. When connected, shows a deterministic gradient avatar (unique per address). Click to open account modal.
- ConnectionStatus — green/cyan/red dot indicating WebSocket state
OrderBook
- 20 levels per side
- Asks displayed in reverse (lowest at bottom, nearest the spread)
- Depth bar fill proportional to cumulative size
- Click a price level to populate the trade form
- Spread displayed between asks and bids (absolute + percentage)
TradeForm
Binance-style order form with interactive controls:
- Buy/Sell toggle with color-coded active state (green/red)
- Order types: Market (default), Limit (GTC), IOC
- Available balance display for the relevant asset (quote for buys, base for sells)
- Price input with asset suffix (hidden for market orders, shows best bid/ask instead)
- Size input with asset suffix and lot size snapping
- Percentage slider with draggable track and 0/25/50/75/100% dot markers — auto-calculates size from available balance and price
- Total estimate (price × size) shown below the size input
- Balance validation — submit button disabled when order exceeds available balance, inline error message shown
- EVM balance hint — when available=0 but funds exist on EVM, shows clickable "X USD on EVM — withdraw to trade" linking to
/wallet - Submits to
POST /api/v1/orderwith EIP-712 signature - Per-order error checking with toast notifications for rejections
BottomPanel
- Balances tab — shows available, reserved, locked, and total per asset (real-time via WS)
- Open Orders tab — displays resting orders with cancel buttons (real-time via WS)
- Order History tab — shows order events (filled, partially filled, cancelled, rejected) with timestamps, status colors, and rejection reasons. Loaded from
GET /api/v1/allOrderson mount, updated in real-time via WS. Existing entries update in place on status changes (e.g., cancel)
Notifications
Toast notifications (top-right, auto-dismiss 6s) for:
- Order fills and partial fills (info)
- Order rejections with reason (error)
- Market orders cancelled due to no liquidity (warning)
- Cancel failures (error)
- WebSocket auth failures (warning/error)
Wallet Page (/wallet)
Unified portfolio view showing all assets across both the Core matching engine and the EVM layer.
Asset List
Columns: Asset | Total | Available | On EVM | Titled
- Available — tradeable balance on the Core exchange
- On EVM — ERC-20 tokens held in the user's EVM wallet (read via
balanceOfon-chain) - Titled — total amount bridged in the user's name (core
locked). Represents regulatory ownership for voting rights and dividends, even if the corresponding ERC-20 tokens were sold on the EVM layer- Shows "bridging..." (blue) when deposit is in transit
- Shows "X transferred" (grey) when some tokens left the user's EVM wallet
- EVM-only tokens — assets received via DeFi (not originated from core) appear with an "EVM only" tag
Deposit / Withdraw
Click any asset row to expand an inline transfer panel:
- Core → EVM (Deposit): calls
POST /api/v1/bridge/depositwith EIP-712 signature. Engine locks core balance, bridge mints ERC-20. - EVM → Core (Withdraw): two EVM transactions via wagmi —
approve()thenCoreWriter.unlockAsset(). Block watcher picks up the event, engine unlocks core balance. - Balance validation — amount input turns red and submit disables when exceeding available balance
- MAX button — fills in the maximum transferable amount
Transaction History
Two history tabs:
- Core History — order events, bridge deposits/withdrawals, and gift credits from Postgres (
GET /api/v1/allOrders). Event types: MARKET, LIMIT, IOC, DEPOSIT, WITHDRAW, GIFT. - EVM History — on-chain transactions from Blockscout API (
explorer.olympxs.com/api/v2). Shows OLP native transfers, token transfers with symbol, contract interactions. Each row links to the block explorer.
Token Address Resolution
EVM token addresses are resolved on-chain via the OlympusTokenFactory.getToken(symbol) view function at 0x0D00 — no backend endpoint needed. Token symbol convention: {ticker}o (e.g., "AAPLo"). Balances read via balanceOf(userAddress) with 18-decimal formatting.
Welcome Faucet
On first wallet authentication (signature-based, not token reuse), new users automatically receive:
- 10,000 USD credited to their Core balance (for trading)
- 1 OLP native gas token sent to their EVM address (for on-chain transactions)
Tracked in-memory per address — each wallet can only claim once per server lifetime. Shows as a GIFT event in Core History.
OLP Native Token
OLP is the native gas token of the Olympus EVM chain (equivalent to ETH on Ethereum). Genesis allocates 21,000,000 OLP to the bridge signer. OLP is distributed via the admin panel through native EVM value transfers — not a core-layer asset.
File Structure
apps/web/
+-- index.html
+-- package.json
+-- vite.config.ts
+-- tsconfig.json
+-- src/
+-- main.tsx # Path-based routing: /, /wallet, /admin
+-- App.tsx # Exchange page
+-- WalletApp.tsx # Wallet page
+-- AdminApp.tsx # Admin panel
+-- api/
| +-- types.ts # TypeScript interfaces for all API shapes
| +-- client.ts # fetchApi() with session token + address headers
| +-- market.ts # depth, exchangeInfo, bookTicker
| +-- trading.ts # placeOrder, cancelOrder (EIP-712 signed)
| +-- bridge.ts # deposit (Core→EVM, EIP-712 signed)
| +-- admin.ts # admin operations including OLP distribution
+-- config/
| +-- wagmi.ts # Olympus chain (600613), WagmiAdapter, AppKit
+-- stores/
| +-- accountStore.ts # connected wallet address (synced from wagmi)
| +-- marketStore.ts # instruments, activeSymbol
| +-- orderBookStore.ts # bids, asks, bestBid, bestAsk, lastUpdateId
| +-- balanceStore.ts # balances, open orders, order history
| +-- notificationStore.ts # toast notifications with auto-dismiss
+-- hooks/
| +-- useWebSocket.ts # WS connection, challenge-response auth
| +-- useBalances.ts # initial REST fetch + WS-driven updates
| +-- useEvmBalances.ts # on-chain token balances via factory + balanceOf
| +-- useEvmHistory.ts # EVM tx history from Blockscout API
| +-- useExchangeInfo.ts
+-- components/
| +-- Header/ # Burger menu, avatar, instrument selector
| +-- OrderBook/
| +-- TradeForm/ # Binance-style form with slider + balance
| +-- CandlestickChart/
| +-- BottomPanel/ # Balances, Open Orders, Order History
| +-- Wallet/ # Wallet page: asset list, transfer panel, history
| +-- Notifications/ # Toast notification overlay
| +-- Admin/ # Admin cards: credit, OLP distribute, instruments
+-- styles/
| +-- variables.css # Hyperliquid-inspired dark theme tokens
| +-- global.css
+-- utils/
+-- format.ts
+-- signing.ts # EIP-712 signing, WS challenge-responseWebSocket API
Real-time market data streaming and authenticated private data via WebSocket — order book diffs, trades, klines, mid prices, and account balance updates.
Account balances GET
Returns the authenticated user's account balances. The account is determined solely by the authentication headers — there is no `user` parameter. **Dev mode:** Requires `X-Account-Address` header. **Production:** Requires `X-Account-Address`, `X-Signature`, and `X-Timestamp` headers.