Transactions
A transaction moves XCH from one wallet to another. It is created and signed in the browser, then sent to the node, which validates it, holds it in the mempool, and packs it into the next block.
Anatomy of a transaction
1pub struct Transaction {2pub chain_id: i64, // network id (must equal CHAIN_ID=1)3pub from: String, // sender address4pub to: String, // recipient address (0x00..00 = burn)5pub asset: String, // "XCH" or "xUSD"6pub amount: i64, // minor units (1 XCH = 1e8)7pub fee: i64, // >= MIN_FEE8pub priority_fee: i64, // tip to the proposer (extra)9pub nonce: i64, // sender's tx counter (replay protection)10pub timestamp: i64,11pub pubkey: String, // sender's public key (hex)12pub signature: String, // ed25519 signature (hex)13pub hash: String, // sha256 of everything incl. signature14}
The canonical message
Signing happens over a precise, pipe-delimited string. Both the frontend and the backend build exactly this string — if they ever disagreed, verification would fail:
1"XCH-1|1|0xfrom|0xto|XCH|100000000|1000|0|0|1722000000000"2// MAGIC |chain|from |to |asset|amount |fee |prio|nonce|timestamp
The full lifecycle
Step 1 — the browser signs
1const { nonce } = await api.nonce(address);2const unsigned = {3chain_id: CHAIN_ID, from: address, to, asset: "XCH",4amount: amountMinor, fee: feeMinor, priority_fee: 0,5nonce, timestamp: Date.now(),6};7const message = messageToSign(unsigned); // "XCH-1|..."8const signature = signBytes(secretKey, encoder.encode(message));9const tx = { ...unsigned, pubkey, signature,10hash: computeHash({ ...unsigned, pubkey, signature }) };11await api.submit(tx);
Step 2 — the node validates
tx::submit rejects the transaction if any check fails. The two most important are verify_signature and validate_state:
1pub fn verify_signature(&self) -> bool {2self.hash == self.compute_hash() // (a) hash matches fields3&& verify_signed_by(&self.pubkey, &self.signature, // (b) sig + key -> from4self.message_to_sign().as_bytes(), &self.from)5}
1fn validate_state(&self, balance, next_nonce, min_fee, chain_id) -> Result<(), ...> {2// chain_id must match · amount > 0 · fee >= min_fee3// from/to are valid 0x addresses4// nonce == next_nonce (replay protection)5// amount + fee + priority_fee <= balance (can't overdraw)6}
nonce == next_nonce means the account's stored counter must equal the tx's nonce. After a tx confirms, the nonce advances — so the same tx can never be replayed, and txs from the same wallet must be submitted one at a time in order.Step 3–5 — mempool to confirmation
The mempool is just a database status: a row with status='pending'. The producer pulls pending txs ordered by fee + priority_fee DESC (richest first), re-validates them, and packs valid ones into a block. Confirmed txs get status='confirmed' plus block_index and block_hash. Invalid ones are marked failed.
1SELECT hash, from_addr, to_addr, asset, amount, fee, priority_fee,2nonce, chain_id, timestamp, pubkey, signature, status, block_index3FROM transactions WHERE status='pending'4ORDER BY fee + priority_fee DESC LIMIT $1
Fees and the proposer
Every tx pays a base fee (≥ MIN_FEE = 1000 minor units) plus an optional priority_fee (a tip to jump the queue). In apply_block, both go to the block's proposer in the transaction's own asset. That's the incentive for producing blocks.
The faucet — server-side minting
To give testers coins, the node has a faucet: POST /faucet mints 100 XCH once per address. Interesting detail: the faucet tx is also a real signed transaction — but signed by the node, with its own faucet key (from FAUCET_SEED), from the faucet address which is pre-funded with 10,000 XCH at genesis.
1let nonce = account_nonce + count_of_pending_faucet_txs;2// the faucet signs many txs; counting its pending ones keeps nonces unique
Try it yourself
This is exactly what the frontend does before calling POST /tx/submit. The message is a canonical string; the hash is SHA-256 of everything including the signature.
0xde1a73ec76…59477— the account's next transaction counterNotice the hash includes the signature and the magic prefix XCH-1. When the node receives this, it recomputes the hash and verifies the signature against the public key — and that the public key derives to from.