Olympus

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-dev

The Vite dev server proxies /api and /ws requests to http://localhost:3000, so the backend must be running.

Production Build

make web-build

Outputs to apps/web/dist/.

Architecture

Pages

Path-based routing in main.tsx (no router library):

PathPageDescription
/ExchangeTrading interface with order book, chart, trade form
/walletWalletUnified portfolio view with bridge deposit/withdraw
/adminAdminAdmin 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:

StorePurpose
accountStoreConnected wallet address (synced from wagmi)
marketStoreActive symbol, instrument list from /api/v1/exchangeInfo
orderBookStoreBids, asks, best bid/ask, lastUpdateId — initialized from REST, updated from WS diffs
tradeStoreLast price + direction (from aggTrade), recent trades for display
klineStoreOHLCV candles — initialized from REST, updated from WS kline + aggTrade
balanceStorePer-asset balances, open orders, order history — REST + WS driven

Data Flow

  1. On mount, App fetches instruments via GET /api/v1/exchangeInfo
  2. useWebSocket opens a WebSocket to /ws and subscribes to depthUpdate, aggTrade, and kline for the active symbol, plus allMids
  3. The hook fetches an initial order book snapshot via GET /api/v1/depth, then applies incremental WS diffs on top
  4. Historical candles load from GET /api/v1/klines. The WS kline feed provides the in-progress candle (handles mid-candle page loads). aggTrade interpolates the close price between kline snapshots for smooth TradingView-style movement.
  5. When the user switches instruments, the hook unsubscribes from the old symbol, subscribes to the new one, and re-fetches the snapshot
  6. When a wallet is connected, the WebSocket authenticates via a challenge-response flow (EIP-191 personal_sign) and subscribes to accountUpdate for real-time balance, open order, and order event updates. A session token is issued and persisted so subsequent page loads skip the wallet prompt
  7. useBalances fetches initial balances, open orders, and order history from REST on mount; subsequent updates arrive via the WebSocket balanceUpdate channel
  8. Order submission calls POST /api/v1/order with 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 depthUpdate first, then fetches a REST snapshot. Diffs arriving during the fetch are buffered and drained after the snapshot loads, ensuring no updates are lost

Components

  • 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/order with 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/allOrders on 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 balanceOf on-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/deposit with EIP-712 signature. Engine locks core balance, bridge mints ERC-20.
  • EVM → Core (Withdraw): two EVM transactions via wagmi — approve() then CoreWriter.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-response

On this page