Appearance
Testing on Solana (the Solana test profile)
The docker test stack has two chain profiles. The EVM profile (default) runs a hardhat chain container + evm-bank. The Solana profile runs the beta's real chain — the 4 Anchor programs (numero_{token,core,custody,betting}) on a solana-test-validator + solana-bank (the cage) — so the Solana fund paths (deposit / withdraw / settle / betting / the chip-claim-board playSKPK grant) run end-to-end against real programs, not stubs.
Program IDs (post
devnet-solana-deploy):numero-custodyandnumero-bettingwere rotated to fresh IDs (EXHhL2QU…/BfGbFyiT…) because their old configs+vaults were permanently tSKPK-bound singletons;numero-tokenandnumero-corekept theirs. Thetest/e2e/solana-cageharness,Anchor.toml,scripts/{init-devnet,standup-local-validator}.ts, and the on-chain tests all carry the fresh IDs — the same IDs used on devnet + (later) mainnet. See The Solana Beta Chain.
Dual-economy testing (real-play-money-split)
The cage e2e harness (test/e2e/solana-cage/) and the local standup (pnpm solana:localnet) stand up both economies since real-play-money-split: init_mints + init_playskpk (beta), an external "canonical USDC" test mint (deterministic 0x77 keypair, init-authority-held — standing in for Circle's devnet faucet), init_real_backing binding it, and one custody init per economy (the default custody binds the DEPLOY chip; beta also inits the REAL custody at GiKdXReu…).
Two build artifacts are required before any cage-e2e run:
bash
cd onchain && anchor build --no-idl && bash scripts/build-real-custody.sh(numero_custody_real.so is the byte-identical real-economy custody under its fresh declare_id — the harness refuses to start without it.)
Funding helpers changed with the backing: fundAndWrap now mints external USDC → wraps (the SKUSDC faucet has no path into the chip); TestUser.realTskpk is the REAL-chip ATA (equal to tskpk on tskpk-profile runs). The dual-economy rows live in test/e2e/solana-cage/real-split.test.ts (run with CHIP_TOKEN=playskpk); skpk_enabled rows are written AT THE RUN's environment shard (h.environment) — never 'all' (shared dev DB).
Why the validator runs on the host (not a container)
EVM's hardhat chain runs in a container. Solana's does not — a deliberate, platform-driven exception:
- The only Anza validator image (
anzaxyz/agave) is linux/amd64-only. On Apple Silicon it runs under emulation, and the emulatedsolana-test-validatorfails to become RPC-ready in 120s+ (a native one boots in ~5s). A consensus node under emulation is too slow and flaky for a test stack people run repeatedly. - Anza does not reliably publish native
aarch64-linuxreleases. - The host already has a native arm64 validator, and the dev flow (
pnpm solana:localnet) + thetest/e2e/solana-cageharness already run it there.
So the Solana profile runs the validator on the host and points the containerized cage + game-server at host.docker.internal:8899. Everything else (the cage, game-server, auth-server, postgres) stays containerized, exactly like the EVM profile.
Trade-off: the host validator is a singleton on 127.0.0.1:8899, so the Solana profile is one-stack-at-a-time (not per-worktree-isolated like the EVM hardhat container) until the validator's --rpc-port is parameterized by TEST_PREFIX (a planned follow-up). The EVM profile stays fully per-worktree. Because it's a shared singleton, test:up:solana refuses to start if another worktree's solana-bank is already running (both would silently share + corrupt the one chain) — bring the other down first, or SOLANA_SHARE_OK=1 to override.
The EVM → Solana mapping
| EVM profile | Solana profile |
|---|---|
hardhat container (local chain) | host solana-test-validator (native arm64, 4 programs loaded) |
chain-deploy one-shot (deploy diamond → DB) | the same host step — pnpm solana:localnet inits playSKPK/custody/betting + writes the cage keypairs |
evm-bank + game-server CHAIN=evm | solana-bank (CHIP_TOKEN=playskpk) + game-server CHAIN=solana |
Chain + deploy: pnpm solana:localnet
scripts/standup-local-validator.ts (pnpm solana:localnet) is the Solana analog of EVM's chain container and its deploy one-shot in one host step. With CHIP_TOKEN=playskpk (beta mode) it:
- Spawns
solana-test-validator --reset --quietwith all 4 programs loaded (--bpf-program <id> <so>fromonchain/target/deploy/). - Inits playSKPK (grant authority = the cage) + custody (cage/arbiter/admin) + betting — the real-money
init_mints/wrap path is absent. - Writes the fixed-seed authority keypairs (
cage/faucet/init) to.solana-localnet/(gitignored), which the cage mounts. - Stays alive (Ctrl-C to stop).
Prerequisite: build the programs once — export PATH="$HOME/.local/share/solana/install/active_release/bin:$HOME/.cargo/bin:$PATH" && cd onchain && anchor build --no-idl → the 4 .so (gitignored build artifacts).
Import-time env baseline: the standup imports the cage's chain/ builders, and solana-bank/src/env.ts asserts its full env contract at module load — so the standup must run with the cage's required vars present (they're not dereferenced; mirrors test/e2e/solana-cage/_env-placeholders.ts). The test:up:solana switch provides these.
The cage: solana-bank (test fragment)
apps/solana-bank/docker-compose.test.yml runs the cage in the stack, mirroring evm-bank's fragment with three Solana-specific twists:
profiles: ["solana"]— opt-in. The defaultpnpm test:up(EVM) skips it entirely; only--profile solanabrings it up, so the EVM suites are unaffected.extra_hosts: ["host.docker.internal:host-gateway"]— so the container reaches the host validator athost.docker.internal:8899.volumes: [./.solana-localnet:/app/.solana-localnet:ro]— the cage mounts the authority keypairs the standup wrote.
apps/solana-bank/.env.test (committed, non-secret) points the cage at the host validator and matches the shared stack values (SERVICE_KEY, DATABASE_URL, AUTH_*, GAME_SERVER_INTERNAL_URL) from evm-bank/.env.test:
CHIP_TOKEN=playskpk
SOLANA_RPC_URL=http://host.docker.internal:8899
CAGE_KEYPAIR_PATH=/app/.solana-localnet/cage.json # + faucet.json, init.json
NODE_ENV=test # REQUIRED with ENABLE_TEST_AUTHGotcha:
ENABLE_TEST_AUTH=truerequiresNODE_ENV(development|test) — the fail-closed test-auth gate. Without it the cage crash-loops at boot (assertTestAuthSafety). Every test-profile service that enables test auth must setNODE_ENV=test.
A booted cage answers a service-key grant end-to-end against the real chain:
POST /api/grant { recipient, amount: 500, claimId } x-service-key
→ { ok: true, idempotent: false, txHash: "…" } # minted 500 playSKPK
# retry with the same claimId:
→ { ok: true, idempotent: true, txHash: null } # no double-mint (on-chain grant_marker)Running the Solana profile
bash
# 1. Start the host validator (separate terminal — stays alive, Ctrl-C to stop):
CHIP_TOKEN=playskpk pnpm solana:localnet
# 2. Bring up the stack on the Solana profile:
pnpm test:up:solana # = TEST_CHAIN=solana bash scripts/test-stack.sh up
pnpm test:down:solanaTEST_CHAIN=solana makes scripts/test-stack.sh:
- layer in
docker-compose.test.solana.yml(game-serverenvironment: { CHAIN: solana, SOLANA_BANK_INTERNAL_URL: http://solana-bank:3668 }, which wins over theCHAIN=evmenv_file) +--profile solana(brings upsolana-bank), - fail fast if the host validator isn't ready — it checks readiness + ownership, not just liveness: the validator must answer and have
numero_coreloaded (a baresolana-test-validatorwith no programs is rejected, so you don't discover it at the first grant), and no other worktree'ssolana-bankmay be running (see the shared-singleton note above), - otherwise behave exactly like
pnpm test:up(per-worktree name, port allocation — but see the one-stack-at-a-time caveat above).
The default pnpm test:up is unchanged — TEST_CHAIN defaults to evm, so the EVM stack is byte-for-byte what it was (game-server CHAIN=evm, no solana-bank).
One CHAIN per game-server: a single game-server serves one chain, so the EVM and Solana profiles can't share a game-server — they're separate profiles selected by TEST_CHAIN. (The beta is Solana-only; CHAIN=evm is the legacy path.)
First consumer
The Solana profile is proven ready (validator + cage + game-server on CHAIN=solana, all healthy, with a real cage grant end-to-end). The first game-server-orchestrated fund-path proof is chip-claim-board Phase 3 (the playSKPK claim grant: game-server → cage → chain), which runs its A2/A6/A8/A11/A15 on this profile.