Skip to main content
This guide spins up a fully self-contained dev environment: one Quantum Chain node sealing blocks every 5 seconds, a local Qustody API talking to it, and a pre-funded address you can transact with. By the end you will have:
  • A dev chain at http://127.0.0.1:8545
  • A Qustody API at http://127.0.0.1:8080
  • A pre-funded wallet registered with Qustody

Prerequisites

  • geth and keygenerator from make all — see Installation
  • qc-custody binary, also built by make all
  • PostgreSQL 16 running locally (or via Docker)

Step 1. Generate a sealer keypair

mkdir -p ~/devnet/keystore
cd ~/devnet
/path/to/build/bin/keygenerator --passphrase "devpass" > sealer.txt
echo devpass > password.txt
mv UTC--*.json keystore/
SEALER_ADDR=$(grep -oE '0x[0-9a-fA-F]{40}' sealer.txt | head -1)
SEALER_PUBKEY=$(grep -oE 'Public key: 0x[0-9a-fA-F]+' sealer.txt | awk '{print $3}')
echo "Sealer address: $SEALER_ADDR"

Step 2. Build the genesis file

extraData encodes the sealer set: 32 vanity bytes + each sealer’s 20-byte address + 65 zero bytes for the signature placeholder.
SEALER_NO_PREFIX=${SEALER_ADDR#0x}
EXTRA="0x$(printf '0%.0s' $(seq 1 64))${SEALER_NO_PREFIX}$(printf '0%.0s' $(seq 1 130))"

cat > genesis.json <<EOF
{
  "config": {
    "chainId": 20803,
    "homesteadBlock": 0,
    "eip150Block": 0,
    "eip155Block": 0,
    "eip158Block": 0,
    "byzantiumBlock": 0,
    "constantinopleBlock": 0,
    "petersburgBlock": 0,
    "istanbulBlock": 0,
    "berlinBlock": 0,
    "londonBlock": 0,
    "quantumVerificationBlock": 0,
    "clique": { "period": 5, "epoch": 30000 }
  },
  "publicKey": "${SEALER_PUBKEY}",
  "difficulty": "1",
  "gasLimit": "30000000",
  "extraData": "${EXTRA}",
  "alloc": {
    "${SEALER_ADDR}": { "balance": "1000000000000000000000000" }
  }
}
EOF

Step 3. Initialize and start the node

geth --datadir ~/devnet init genesis.json

geth \
  --datadir ~/devnet \
  --networkid 20803 \
  --port 30303 \
  --nodiscover \
  --mine \
  --miner.etherbase "$SEALER_ADDR" \
  --unlock "$SEALER_ADDR" \
  --password ~/devnet/password.txt \
  --allow-insecure-unlock \
  --http --http.addr 127.0.0.1 --http.port 8545 \
  --http.api eth,net,web3,debug,personal,clique,txpool \
  --http.corsdomain "*" \
  --ws --ws.addr 127.0.0.1 --ws.port 8546 \
  --ws.api eth,net,web3 \
  --verbosity 3
Confirm blocks are advancing:
curl -s -X POST http://127.0.0.1:8545 \
  -H 'Content-Type: application/json' \
  --data '{"jsonrpc":"2.0","method":"eth_blockNumber","id":1,"params":[]}'
The result hex should increment every 5 seconds.

Step 4. Start PostgreSQL

docker run -d --name qustody-pg \
  -p 5432:5432 \
  -e POSTGRES_PASSWORD=devpass \
  -e POSTGRES_DB=qc_custody \
  -e POSTGRES_USER=custody \
  postgres:16

Step 5. Start a local signer (callback mode)

For development you can run a minimal callback-mode signer. Create a file local-signer.go that reads pending transactions from Qustody, signs with your local keystore, and POSTs back. See External signing for the payload contract. For end-to-end tests, the simpler path is to use callback mode with a script that polls every 2 seconds. Or use the reference signer service qustody-signer if you have it built locally.

Step 6. Start the Qustody API

export CUSTODY_DB_HOST=127.0.0.1
export CUSTODY_DB_PORT=5432
export CUSTODY_DB_USER=custody
export CUSTODY_DB_PASSWORD=devpass
export CUSTODY_DB_NAME=qc_custody
export CUSTODY_DB_SSLMODE=disable
export CUSTODY_DB_MIGRATE_ON_START=true

export CUSTODY_NODE_RPC_URL=http://127.0.0.1:8545
export CUSTODY_NODE_WS_URL=ws://127.0.0.1:8546
export CUSTODY_CHAIN_ID=20803
export CUSTODY_CONFIRMATION_DEPTH=2

export CUSTODY_AUTH_ENABLED=false      # dev only — never in production
export CUSTODY_RBAC_ENABLED=false
export CUSTODY_SIGNER_TYPE=callback
export CUSTODY_ENV=development

/path/to/build/bin/qc-custody
Once /readyz returns 200:
curl http://127.0.0.1:8080/readyz

Step 7. Register a wallet and transact

BASE_URL=http://127.0.0.1:8080

# Vault
VAULT_ID=$(curl -s -X POST $BASE_URL/v1/vaults \
  -H 'Content-Type: application/json' \
  -d '{"name":"dev"}' | jq -r .id)

# Wallet pointing to the sealer (which has the dev funds)
WALLET_ID=$(curl -s -X POST $BASE_URL/v1/wallets \
  -H 'Content-Type: application/json' \
  -d "{
    \"vault_id\": \"$VAULT_ID\",
    \"public_key\": \"$SEALER_PUBKEY\",
    \"address\": \"$SEALER_ADDR\"
  }" | jq -r .id)

# Send a transaction
TX_ID=$(curl -s -X POST $BASE_URL/v1/transactions \
  -H 'Content-Type: application/json' \
  -d "{
    \"wallet_id\": \"$WALLET_ID\",
    \"to_address\": \"0x000000000000000000000000000000000000dEaD\",
    \"value\": \"1\",
    \"asset\": \"QRC\"
  }" | jq -r .id)

# Watch state
watch -n 1 "curl -s $BASE_URL/v1/transactions/$TX_ID | jq .status"
You will see SUBMITTED → APPROVED → PENDING_SIGNATURE. Your local signer fulfils the signing payload, then the transaction transitions to BROADCAST → CONFIRMED.

Tear down

docker rm -f qustody-pg
pkill -f 'geth --datadir ~/devnet'
pkill qc-custody
rm -rf ~/devnet

Where to go next