Skip to main content

Testing guide

Order: chain → contracts → backend → client → test. Each make target runs in its own terminal; Ctrl-C stops it.

0. Setup (once)​

Needs docker (running), foundry (forge/cast; Go integration tests spawn their own anvil), go 1.25+, rust/cargo, bun, jq.

git submodule update --init --recursive
cd client && bun install && bunx playwright install chromium && cd ..
make env # creates root .env from .env.example (if missing) + generates client/.env.local, landing/.env.local

One env file: the root .env. Backend scripts (make dev, make contracts) read it directly; a var set in the shell wins over .env, missing required keys fail with the key name (infra/dev/env.sh). The apps can't read the root, so make env generates their .env.local (chain/RPC/addresses from client/src/shared/config/perp.json, gateway from GATEWAY_PORT, HERMES_*). Re-run make env after editing .env or redeploying.

PRIVY_APP_ID in .env (-> NEXT_PUBLIC_PRIVY_APP_ID) enables the Privy wallet modal on Base Sepolia; e2e (NEXT_PUBLIC_E2E=1 in playwright.config.ts) uses plain wagmi + the "E2E wallet" (E2E_PRIVATE_KEY).

1. Chain + contracts​

Base Sepolia (chain id 84532)​

  1. Make 4 wallets (cast wallet new ×4): deployer, operator (settler), keeper, mm-bot. Fund from a Base Sepolia faucet (Coinbase CDP / Alchemy / QuickNode): deployer ~0.2 ETH, others ~0.05 ETH.
  2. In .env:
    RPC_URL=https://base-sepolia.g.alchemy.com/v2/<key> # public sepolia.base.org rate-limits the indexer
    DEPLOYER_PRIVATE_KEY=0x...
    OPERATOR_PRIVATE_KEY=0x...
    KEEPER_PRIVATE_KEY=0x...
    MM_PRIVATE_KEY=0x...
    PYTH=0xA2aa501b19aff244D90cc15a4Cf739D2725B5729 # Pyth on Base Sepolia (verified onchain)
    ORACLE_MODE=hermes
    HERMES_URL=https://hermes.pyth.network
    HERMES_API_KEY= # optional, avoids Hermes rate limits
    INDEXER_CONFIRMATIONS=2
  3. make contracts. Writes backend/config/{spot,perp}.json + client/src/shared/config/*.json. Tracked configs = the Base Sepolia deploy (deployments/base-sepolia/).

Current default: Base Sepolia is already deployed (addresses + roles in deployments/base-sepolia/README.md). Skip step 1: make env → make dev → client.

Spot deploys its own Uniswap v4 PoolManager + mintable test USDC (the app's faucet button mints it). Perp reuses that USDC, so spot always deploys first (the script does both, in order).

2. Backend​

make dev # nats + pg (docker :4322/:5442), engine, indexer, keeper, settler, gateway :8080, mm-bot
  • Refuses to start if no PerpExchange at perp.json address on RPC_URL ("deploy first: make contracts").
  • Prints a logs dir (engine.log, gateway.log, settler.log, …). State (pg, nats, engine snapshot) persists across restarts; first start (or FRESH=1 make dev, or a new exchange address) replays from deployBlock (~7 min on public RPC).
  • Recorder runs only if ARKIV_PRIVATE_KEY is set (.env); logs in recorder.log.
  • MM=0 make dev skips the market maker (empty books). GATEWAY_PORT=… to move the gateway.
  • Sanity: curl localhost:8080/markets, curl 'localhost:8080/book/0?depth=5' (mm-bot quotes appear within a few seconds).

3. Client​

make env # after `make contracts`: picks up chain + addresses
cd client && bun run dev # http://localhost:3000

Base Sepolia works the same: once client/src/shared/config/perp.json is the Base Sepolia deploy (chainId 84532) and RPC_URL points at it, make env writes the NEXT_PUBLIC_BASE_SEPOLIA_* vars.

Wallet: MetaMask/injected (or Privy) with a Base Sepolia account; the app's faucet mints test tokens.

4. Manual test (happy path)​

  1. /trade/btc → connect wallet.
  2. Account panel → Testnet faucet (mints 10,000 USDC) → Deposit 1000 → balance shows $1,000.00.
  3. Order form → Limit, price just above best ask (or Market) → Long BTC.
  4. Fill appears in Fills, position in Positions; settler log shows a settled tx (cast tx <hash> or basescan).
  5. Market close → "No open positions"; collateral drops by fees only.
  6. Reject path: Post-only order that crosses → toast with engine reject.
  7. Spot: /spot/... swap; vault address pays 0 fee, others 0.3%.

5. Automated tests​

WhatCommandNeeds
Contractscd contracts && forge test—
Enginecd engine && cargo test—
Engine ↔ NATSmake test-integrationdocker
Go unitcd backend && go test ./...—
Go integration (indexer, keeper, settler, gateway, mm-bot)make infra then cd backend && go test -tags integration -p 1 ./...docker nats/pg, forge, built engine; env as in .github/workflows/ci.yml job backend-integration
Client unitcd client && bun run test—
Client lint + buildcd client && bun run lint && bun run build—
E2E (Playwright: perp, vault, spot)cd client && bun run e2eBase Sepolia + live backend api.maas.exchange; E2E_PRIVATE_KEY (funded Sepolia ETH) + CLIENT_RPC_URL in root .env
All quick onesmake test—

CI runs everything except E2E on every PR to main.

Troubleshooting​

  • stream name already in use with a different configuration in integration tests: stale local nats; docker compose -f infra/docker/docker-compose.yml restart nats.
  • Port busy (8080): stop old make dev; docker ps for leftover maas-dev-* containers (docker rm -f).
  • Orders MISSING_MARK (503): keeper hasn't published an index yet; wait a few seconds, check keeper.log.
  • Orders PRICE_BAND: price > 5% from index (GATEWAY_PRICE_BAND_BPS).