X-Change Guide

Backend overview

The backend is one Rust program (xchange-node) that is both an HTTP server and a blockchain node. This page gives you the mental model; the sub-pages go deep into each area.

The stack, and why

Rust— memory-safe, fast, and its type system catches bugs at compile time. Great for "correctness matters" systems code.
Axum— the HTTP web framework. Builds the router, defines handlers, runs middleware.
Tokio— the async runtime. Lets the server handle many requests while the block producer ticks every 3 seconds.
SQLx— database driver. Compile-time-checked SQL, async Postgres pool, and sqlx::migrate! applies schema changes at boot.
ed25519-dalek— the signature library behind wallets, transactions and the faucet.

One binary, two jobs

The same process serves HTTP and produces blocks. That's unusual and worth being able to justify:

  • It's a demo — a real network would have nodes that only validate, nodes that only serve APIs, etc.
  • Keeping them together means the API handlers and the block producer share one AppState and one Postgres pool — no separate service to deploy.
  • The block producer is a background tokio::spawn loop; the API keeps working while it runs.

Boot sequence (from main.rs)

1 · Load .env + configPORT, DATABASE_URL, JWT_SECRET, chain params
2 · Connect to PostgresPgPool with up to 10 connections
3 · Run migrationssqlx::migrate! creates all tables
4 · Build faucet accountfrom FAUCET_SEED → its address
5 · Create AppStatedb + cfg + node_address + orderbooks + rate + faucet
6 · Bootstrap genesisblock 0 if chain empty; seed faucet balance
7 · Rebuild order booksreload open orders from DB
8 · Spawn block producertokio::spawn(block_producer::run)
9 · Serve HTTPaxum on 0.0.0.0:PORT

What happens when a request arrives

Browser
fetch / JSON
HTTP
Rate limit
240 req/min per IP
CORS
allowlist check
Route
matches path
Handler
does the work
Postgres
reads/writes
Middleware (runs for every request)Your codeStorage

Protected routes also run the JWT check (middleware) before the handler. If the token is missing or invalid you get a 401 before any business logic runs. Backend deep dive: auth

The routes at a glance

AreaPublicRequires JWT
Health/health
Auth/auth/challenge /auth/login /auth/refresh/auth/me
Transactions/tx/submit /tx/{hash} /txs /faucet
Wallets/wallet/{addr} /wallet/{addr}/nonce
Chain/chain/blocks /chain/blocks/{sel} /chain/height /chain/validators /chain/search
Trading/trade/orderbook /trade/trades/trade/order /trade/orders DELETE /trade/order/{id}
Staking/staking/validators/staking/register /staking/stake /staking/unstake /staking/positions
Multisig/multisig/wallet/{addr}/multisig/wallet /multisig/tx /multisig/tx/{id}/sign /multisig/tx/{id}/execute
Admin/admin/metrics /admin/validators

Full detail on every endpoint — request, response, errors — is on the Backend API reference page.

How errors are returned

Every handler returns Result<T, AppError>. The IntoResponse impl turns it into a JSON body with a proper HTTP status:

apps/node/src/api/middleware.rs
1pub enum AppError {
2 BadRequest(String), // 400
3 Unauthorized(String), // 401
4 NotFound(String), // 404
5 Conflict(String), // 409
6 Internal(String), // 500 (message logged, body says "internal error")
7}
Why map_err helpers?
Rust's map_err needs a plain function to pass in, so the code uses tiny helpers like err_bad(e), err_internal(e) etc. instead of methods. Small but handy implementation detail to mention.

Configuration via environment variables

Almost nothing is hardcoded — the chain parameters are read from env at boot into a Config struct:

VarDefaultMeaning
PORT8080listen port
DATABASE_URLPostgres connection (required)
JWT_SECRETdev-secret…signs access/refresh tokens
CHAIN_ID1network id in every signed message
BLOCK_INTERVAL_MS3000block time
EPOCH_BLOCKS100blocks per staking epoch
EPOCH_REWARD500000000.5 XCH per epoch, split pro-rata
MIN_FEE1000minimum tx fee (minor units)
MIN_STAKE1e910 XCH to become a validator
FAUCET_AMOUNT1e10100 XCH per faucet claim
RATE_LIMIT_PER_MIN240per-IP requests/minute
ALLOWED_ORIGINlocalhost:3000CORS allowlist (comma-separated)

Tests — 16, and all DB-free

  • 10 unit tests — signature round-trip, block hash/index/merkle validation, proposer determinism + weighting, epoch math, FIFO matching, market orders.
  • 6 integration tests — tampered signatures rejected, double-spend (same nonce) rejected, insufficient balance rejected, apply_block accounting, nonce ordering.

The tests run without Postgres because the core logic is kept purevalidate_block and apply_block take state in and return state out. That design choice makes them easy to test.

Deep dives

Backend Auth & Wallet

Sign-in-with-wallet, JWTs, middleware.

Backend Blockchain & Consensus

Blocks, hashes, proposer selection, block producer.

Backend Transactions

Sign → submit → mempool → block → confirmed.

Backend Exchange & Order Book

Matching engine, market vs limit.

Backend Staking & Ledger

Balances, validators, rewards, multisig.

Backend API Reference

Every endpoint in plain language.