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
sqlx::migrate! applies schema changes at boot.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
AppStateand one Postgres pool — no separate service to deploy. - The block producer is a background
tokio::spawnloop; the API keeps working while it runs.
Boot sequence (from main.rs)
What happens when a request arrives
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
| Area | Public | Requires 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:
1pub enum AppError {2BadRequest(String), // 4003Unauthorized(String), // 4014NotFound(String), // 4045Conflict(String), // 4096Internal(String), // 500 (message logged, body says "internal error")7}
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:
| Var | Default | Meaning |
|---|---|---|
| PORT | 8080 | listen port |
| DATABASE_URL | — | Postgres connection (required) |
| JWT_SECRET | dev-secret… | signs access/refresh tokens |
| CHAIN_ID | 1 | network id in every signed message |
| BLOCK_INTERVAL_MS | 3000 | block time |
| EPOCH_BLOCKS | 100 | blocks per staking epoch |
| EPOCH_REWARD | 50000000 | 0.5 XCH per epoch, split pro-rata |
| MIN_FEE | 1000 | minimum tx fee (minor units) |
| MIN_STAKE | 1e9 | 10 XCH to become a validator |
| FAUCET_AMOUNT | 1e10 | 100 XCH per faucet claim |
| RATE_LIMIT_PER_MIN | 240 | per-IP requests/minute |
| ALLOWED_ORIGIN | localhost:3000 | CORS 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 pure —validate_block and apply_block take state in and return state out. That design choice makes them easy to test.
Deep dives
Sign-in-with-wallet, JWTs, middleware.
Blocks, hashes, proposer selection, block producer.
Sign → submit → mempool → block → confirmed.
Matching engine, market vs limit.
Balances, validators, rewards, multisig.
Every endpoint in plain language.