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)
- 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. - In
.env:RPC_URL=https://base-sepolia.g.alchemy.com/v2/<key> # public sepolia.base.org rate-limits the indexerDEPLOYER_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=hermesHERMES_URL=https://hermes.pyth.networkHERMES_API_KEY= # optional, avoids Hermes rate limitsINDEXER_CONFIRMATIONS=2 make contracts. Writesbackend/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.jsonaddress onRPC_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 (orFRESH=1 make dev, or a new exchange address) replays fromdeployBlock(~7 min on public RPC). - Recorder runs only if
ARKIV_PRIVATE_KEYis set (.env); logs inrecorder.log. MM=0 make devskips 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)
/trade/btc→ connect wallet.- Account panel → Testnet faucet (mints 10,000 USDC) → Deposit 1000 → balance shows $1,000.00.
- Order form → Limit, price just above best ask (or Market) → Long BTC.
- Fill appears in Fills, position in Positions; settler log shows a
settledtx (cast tx <hash>or basescan). - Market close → "No open positions"; collateral drops by fees only.
- Reject path: Post-only order that crosses → toast with engine reject.
- Spot:
/spot/...swap; vault address pays 0 fee, others 0.3%.
5. Automated tests
| What | Command | Needs |
|---|---|---|
| Contracts | cd contracts && forge test | — |
| Engine | cd engine && cargo test | — |
| Engine ↔ NATS | make test-integration | docker |
| Go unit | cd 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 unit | cd client && bun run test | — |
| Client lint + build | cd client && bun run lint && bun run build | — |
| E2E (Playwright: perp, vault, spot) | cd client && bun run e2e | Base Sepolia + live backend api.maas.exchange; E2E_PRIVATE_KEY (funded Sepolia ETH) + CLIENT_RPC_URL in root .env |
| All quick ones | make test | — |
CI runs everything except E2E on every PR to main.
Troubleshooting
stream name already in use with a different configurationin integration tests: stale local nats;docker compose -f infra/docker/docker-compose.yml restart nats.- Port busy (8080): stop old
make dev;docker psfor leftovermaas-dev-*containers (docker rm -f). - Orders
MISSING_MARK(503): keeper hasn't published an index yet; wait a few seconds, checkkeeper.log. - Orders
PRICE_BAND: price > 5% from index (GATEWAY_PRICE_BAND_BPS).