Exchange & the order book
Users can trade XCH against xUSD on an order book — like a real exchange. This page explains the two sides of the market, the matching algorithm, and why trades are "off-chain".
The two players
Adds an order that rests on the book — waits for someone else. Sets the price.
Hits an existing order — takes the maker's price, and the trade happens instantly.
Every trade executes at the maker's price. The maker "made" liquidity; the taker "took" it. This is standard exchange behaviour.
The order book structure
The book is a map of price → queue of (trader, amount). Two sides:
1pub struct OrderBook {2pub bids: BTreeMap<i64, Vec<(String, i64)>>, // buys: highest price is best3pub asks: BTreeMap<i64, Vec<(String, i64)>>, // sells: lowest price is best4}
- Bids — people wanting to buy. Best bid = highest price.
- Asks — people wanting to sell. Best ask = lowest price.
- BTreeMap keeps prices sorted → the "best" level is trivial to find.
- A Vec queue per price gives FIFO: earlier orders at the same price fill first.
How matching works
When an order arrives, match_order loops: keep filling against the best opposite level while prices "cross", then whatever's left rests (or is cancelled for market orders).
1let mut remaining = incoming.amount;2let resting = incoming.side.opposite();3loop {4if remaining == 0 { break; }5let Some((price, _)) = book.best(resting) else { break };6// A bid crosses if best ask <= its limit price; an ask if best bid >= its limit7let crossed = match incoming.side {8Side::Bid => price <= incoming.price,9Side::Ask => price >= incoming.price,10};11if !crossed { break; }12let (price, takers) = book.take(resting, remaining);13for (maker, qty) in takers { /* record a Trade at [price], reduce remaining */ }14}
Limit vs market
- Limit order — you name a price. Only fills if someone is willing to trade at it; otherwise it rests.
- Market order — you take whatever is available. Implemented cleverly: bid price =
i64::MAX, ask price =0, so it always crosses. Any unfilled remainder is cancelled (market orders never rest).
The placement pipeline (funds locking)
Placing an order is more than matching — the node must make sure you can pay. Everything happens in one database transaction:
The in-memory book is rebuilt at boot from orders rows with status='open' — so the book survives restarts even though matching is in-memory.
Why "off-chain"?
Trade settlement writes directly to the balances table — it doesnot create a block or a transaction row. Compare with a wallet transfer:
Signed tx → mempool → block → confirmed. Appears on-chain. Uses nonces.
Matching + direct balance update in one DB tx. No block. Faster, but not part of the "blockchain" ledger.
Play with the engine
This is a simplified clone of match_order in the Rust node. Limit orders fill against resting orders at the maker's price; whatever doesn't fill rests on the book. Market orders cross everything.
In the real node, a bid locks xUSD = price × amount (quote) and an ask locks XCH before matching, and any unfilled remainder is refunded. Try: sell below the best bid to cross, or place a big market order to sweep the book.
The trading endpoints
GET /trade/orderbook— current depth (bids/asks as [price, size]).POST /trade/order(JWT) — place a limit or market order.GET /trade/orders(JWT) — your recent orders.DELETE /trade/order/{id}(JWT) — cancel + refund.GET /trade/trades— recent fills.
All the UI that calls these is on the Frontend → trading page, and the tables behind them are Database → orders & trades.