Checkmate-Escrow has two end-to-end test suites:
- Sandboxed lifecycle suite (
e2e-tests/) — deploys the release WASM artifacts into an in-process Soroban host and drives a complete match from creation to payout. It is fully automated, deterministic, and runs on every PR in CI. - Testnet suite (
oracle-service/tests/e2e_tests.rs) — exercises the off-chain oracle pipeline (RPC, Lichess/Chess.com fetches, persistence) against real testnet endpoints and wallets.
The unit tests in contracts/escrow and contracts/oracle register the
contracts natively (compiled into the test binary). They are fast and cover
the state machine well, but they do not prove that the deployed artifact —
the release WASM that would actually be shipped to a network — behaves
correctly, nor that the two contracts work together. The sandboxed suite
closes that gap: it deploys target/wasm32v1-none/release/{escrow,oracle}.wasm
into the sandboxed Soroban host and drives the exact flow the off-chain oracle
service drives in production.
e2e-tests/tests/lifecycle.rs runs the full flow end to end for every payout
branch:
- Deploy —
escrow.wasmandoracle.wasm(the release bytecode) are registered in the sandbox, together with a Stellar asset contract that plays the role of the stake token. - Initialize — escrow is configured with the oracle service account as its trusted oracle; the oracle contract gets the same account as admin.
- Create match —
create_matchwith a realistic Lichessgame_id("abcd1234"). The match isPendingand escrow holds nothing. - Deposit — player1 then player2 transfer their stakes in. The match
activates,
is_fundedflips totrue, and the contract balance is exactly2 × stake. - Oracle records the result — the oracle service calls
oracle.submit_result(game_id, platform, winner, response time, and confidence), which stores aResultEntryfor the audit log. - Settle — the oracle calls
escrow.submit_resultfrom the escrow-side oracle address; the match becomesCompletedand the payout vests. - Claim — each player calls
claim_vested_payout(the test config disables the vesting delay). The winner receives2 × stake, the loser receives nothing, and the contract retains a zero balance.
Assertions cover state transitions, the contract's token balance at each stage, player balances after payout, stored oracle results, and the loser's inability to claim.
| Test | Outcome | Verifies |
|---|---|---|
test_full_lifecycle_player1_wins |
Winner::Player1 |
winner ends with initial + stake, loser cannot claim |
test_full_lifecycle_player2_wins |
Winner::Player2 |
winner ends with initial + stake, loser cannot claim |
test_full_lifecycle_draw |
Winner::Draw |
each player gets exactly their stake back |
test_non_oracle_cannot_submit_result— an impostor account cannot settle a match; it staysActiveand both stakes stay in escrow.test_oracle_duplicate_submit_rejected— the oracle contract refuses a second result submission for the same match (AlreadySubmitted).test_claim_before_settlement_rejected— a player cannot claim a payout before the oracle settles the match.test_submit_result_on_unfunded_match_rejected— the oracle cannot settle a match that was never fully funded (NotFunded).
Note on events: event emission (
match/created,match/activated,match/completed,oracle/result, …) is asserted in the unit tests incontracts/*. The soroban-sdk test host does not reliably surface events published by WASM-registered contracts throughenv.events()in this SDK version, so the E2E suite validates observable effects instead — the state and balance assertions above pin the same behavior end to end.
Contracts are built for wasm32v1-none — the Soroban target that emits
core wasm 1.0 only. Do not build with wasm32-unknown-unknown for the E2E
suite: on recent Rust that target enables reference-types / multi-value
instructions, which the Soroban host validator rejects at deployment — the E2E
suite would catch exactly this.
# Full suite: build release WASM → unit tests → E2E tests
scripts/test.sh
# Or manually:
rustup target add wasm32v1-none
cargo build --target wasm32v1-none --release -p escrow -p oracle
cargo test # unit tests
cargo test -p e2e-tests # E2E tests against the release WASMThe E2E tests read the compiled artifacts from target/wasm32v1-none/release/
at runtime, so the WASM build must run first. If the artifacts are missing,
the tests fail with a clear message instead of silently testing stale
bytecode. e2e-tests is a workspace member but not a default member, so
plain cargo build / cargo test compile only the contracts — run the
harness explicitly with cargo test -p e2e-tests.
.github/workflows/ci.yml builds the release WASM in the test job and then
runs both cargo test and cargo test -p e2e-tests, so every push to main
and every PR is validated against the deployable artifacts.
- Live network behavior — fees, sequencing, and real ledger effects are
covered by the testnet suite below and by
scripts/smoke_test.sh. - Off-chain oracle HTTP fetch — the Lichess / Chess.com API calls live in
oracle-service/and are exercised by the testnet suite. - Multi-oracle consensus — the m-of-n voting path in the oracle contract
(see docs/oracle.md) is covered by unit tests in
contracts/oracle/src/tests.rs.
The E2E test suite (oracle-service/tests/e2e_tests.rs) exercises the full
oracle pipeline with no mocks:
| Test | What it validates |
|---|---|
e2e_testnet_rpc_is_reachable |
Testnet RPC endpoint responds and returns a valid ledger sequence |
e2e_soroban_client_constructs_from_testnet_config |
SorobanClient builds without error from real testnet addresses |
e2e_lichess_fetch_completed_game_result |
Lichess HTTP client fetches and parses a real completed game |
e2e_lichess_nonexistent_game_returns_not_found |
Invalid Lichess game ID returns GameNotFound, not a panic |
e2e_chess_com_fetch_completed_game_result |
Chess.com client fetches a public archived game |
e2e_provider_registry_resolves_lichess_game |
Multi-provider registry resolves a Lichess game with correct winner |
e2e_oracle_config_round_trip |
Oracle config key material round-trips through hex decode correctly |
e2e_testnet_ledger_is_progressing |
Testnet ledger advances between two polls (not stalled) |
e2e_queue_persists_and_reloads_entries |
Pending queue persists entries to disk and reloads them correctly |
e2e_dead_letter_store_round_trip |
Dead-letter store appends and reloads failed entries correctly |
e2e_health_check_returns_ok |
Health check response serializes/deserializes without error |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
rustup target add wasm32-unknown-unknowncargo install stellar-cli --features optGenerate and fund three testnet identities — one for the oracle, and two for the match players:
# Oracle signing key
stellar keys generate oracle-e2e --network testnet
stellar keys show oracle-e2e # note the G-address
stellar keys show --private oracle-e2e | xxd -p # note the hex seed
# Player wallets
stellar keys generate player1-e2e --network testnet
stellar keys generate player2-e2e --network testnet
# Fund via Friendbot (testnet only — no real XLM needed)
curl "https://friendbot.stellar.org?addr=$(stellar keys address oracle-e2e)"
curl "https://friendbot.stellar.org?addr=$(stellar keys address player1-e2e)"
curl "https://friendbot.stellar.org?addr=$(stellar keys address player2-e2e)"Follow the Deployment Guide to deploy the escrow and oracle contracts to testnet, then note the contract C-addresses:
./scripts/deploy_testnet.sh
# Note the output:
# Escrow contract: C...
# Oracle contract: C...Export the following variables before running the tests. Only
E2E_CONTRACT_ESCROW, E2E_CONTRACT_ORACLE, E2E_ORACLE_KEY_HEX, and
E2E_ORACLE_ADDRESS are required; all others have sensible defaults.
# Required
export E2E_CONTRACT_ESCROW="C<56 chars>"
export E2E_CONTRACT_ORACLE="C<56 chars>"
export E2E_ORACLE_KEY_HEX="<64 hex chars>" # 32-byte seed, hex-encoded
export E2E_ORACLE_ADDRESS="G<55 chars>" # matches E2E_ORACLE_KEY_HEX
# Optional (defaults shown)
export E2E_RPC_URL="https://soroban-testnet.stellar.org"
export E2E_NETWORK_PHRASE="Test SDF Network ; September 2015"
export E2E_LICHESS_TOKEN="" # Lichess bearer token for higher rate limits
export E2E_CHESSDOTCOM_KEY="" # Chess.com developer API key# Stellar CLI stores keys in ~/.config/stellar/identity/<name>.toml
# The raw 32-byte seed can be extracted as:
stellar keys show --private oracle-e2e
# Output looks like: SB... (Stellar secret key strkey)
# Convert to hex:
python3 -c "
import base64, sys
raw = sys.stdin.read().strip()
# Decode strkey (S...) → 32-byte seed
import struct
decoded = base64.b32decode(raw[1:] + '=' * ((8 - len(raw[1:]) % 8) % 8))
print(decoded[1:-2].hex()) # strip version byte and checksum
"
# Or use the stellar-strkey library directly in Rust if you prefer.Security note: Never commit
E2E_ORACLE_KEY_HEXto source control. Use a.envfile (listed in.gitignore) or inject it via your CI secrets manager.
# All E2E tests (sequential to avoid rate-limit collisions)
cargo test -p oracle-service --test e2e_tests -- --nocapture --test-threads=1
# Single test
cargo test -p oracle-service --test e2e_tests e2e_testnet_rpc_is_reachable -- --nocapture
# Skip tests that require env vars (CI without testnet credentials)
# — tests skip automatically when E2E_CONTRACT_ESCROW is unset —
cargo test -p oracle-service --test e2e_tests -- --nocaptureAdd the following job to your GitHub Actions workflow to run E2E tests in CI with testnet credentials stored as repository secrets:
e2e-tests:
name: E2E Testnet Tests
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
E2E_CONTRACT_ESCROW: ${{ secrets.E2E_CONTRACT_ESCROW }}
E2E_CONTRACT_ORACLE: ${{ secrets.E2E_CONTRACT_ORACLE }}
E2E_ORACLE_KEY_HEX: ${{ secrets.E2E_ORACLE_KEY_HEX }}
E2E_ORACLE_ADDRESS: ${{ secrets.E2E_ORACLE_ADDRESS }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Run E2E tests
run: |
cargo test -p oracle-service --test e2e_tests \
-- --nocapture --test-threads=1Recommended: Run E2E tests only on
mainpushes (not on every PR) to avoid exhausting Friendbot and testnet rate limits.
For a fully automated pipeline, generate fresh testnet wallets in CI and fund them via Friendbot:
- name: Generate and fund testnet wallets
run: |
stellar keys generate ci-oracle --network testnet
ORACLE_ADDR=$(stellar keys address ci-oracle)
curl -s "https://friendbot.stellar.org?addr=$ORACLE_ADDR" | jq .
echo "E2E_ORACLE_ADDRESS=$ORACLE_ADDR" >> $GITHUB_ENV
# Export the raw key for E2E_ORACLE_KEY_HEX
# (implement key extraction per your CI platform's secret handling)Each test prints a status line prefixed with [e2e]:
[e2e] testnet_rpc_is_reachable: latest ledger sequence=12345678
[e2e] lichess_fetch_completed_game_result: winner=Player1 OK
[e2e] SKIP: chess_com_fetch_completed_game_result: rate limited by chess.com; retry after 60s
OK— test passedSKIP— test was skipped due to missing credentials or unavailable network; this is not a failure- A test failure (non-zero exit code) indicates a real regression
After a match completes on testnet, verify the payout transaction using the Stellar Explorer or the CLI:
# Look up the winning player's transaction history
stellar operations list --account <PLAYER_G_ADDRESS> --network testnet
# Or use Stellar Expert:
# https://stellar.expert/explorer/testnet/account/<PLAYER_G_ADDRESS>The payout appears as a payment operation from the escrow contract address to
the winner's address for the 2 × stake_amount of the match token.
The test was skipped because one or more required env vars were absent. Set them as described in the Environment variables section and re-run.
The testnet RPC is temporarily unavailable or your network blocks outbound HTTPS. Check the Stellar status page and retry.
The E2E_CONTRACT_ESCROW or E2E_CONTRACT_ORACLE value is malformed.
Valid Soroban contract C-addresses start with C and are 56 characters long.
E2E_ORACLE_KEY_HEX was set to all zeros or was decoded to zeros. Regenerate
the key with stellar keys generate and export the correct hex seed.
The Lichess and Chess.com tests use conservative rate limits (0.5 req/s). If
you hit rate limits during development, add a short sleep between test runs
or use the E2E_LICHESS_TOKEN variable to authenticate for higher limits.
- Oracle Design — how the oracle pipeline fetches and verifies results
- Deployment Guide — deploying contracts to testnet
- Local Development Setup — running the full stack locally
- Error Codes Reference — contract error codes and recovery