Skip to content

e2e (shellnet)

e2e (shellnet) #76

Workflow file for this run

name: e2e (shellnet)
# Real end-to-end tests against the live shellnet: deploy a market, stake,
# split, place/cancel orders, buyFullSet. Slow (~8-10 min), hit the network,
# and spend test NACKL. Separate from pr-tests.yml (its `tests` job skips
# these — nextest does not run `#[ignore]` tests by default).
on:
# Called by pr-tests.yml after its unit/integration jobs pass, so e2e gates
# a PR only once everything else is green. Also runs standalone nightly and
# on demand.
workflow_call:
workflow_dispatch:
schedule:
- cron: "0 3 * * *" # nightly ~03:00 UTC
# Never interrupt a run mid-deploy — a half-applied shellnet deploy leaves
# orphan contracts. One e2e run at a time.
concurrency:
group: e2e-shellnet
cancel-in-progress: false
permissions:
contents: read
env:
CARGO_PROFILE_DEV_DEBUG: "0"
CARGO_PROFILE_TEST_DEBUG: "0"
CARGO_INCREMENTAL: "0"
CARGO_TERM_COLOR: always
jobs:
e2e:
name: e2e against shellnet
runs-on: ubuntu-latest
timeout-minutes: 120
env:
# Same shape as the PR `tests` job — local read-model the e2e helpers
# upsert the deployed market into.
TEST_DATABASE_URL: postgres://dodex:dodex@localhost:5432/dodex_test
E2E_NETWORK_ENDPOINT: https://shellnet.ackinacki.org
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: dodex
POSTGRES_PASSWORD: dodex
POSTGRES_DB: dodex_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U dodex -d dodex_test"
--health-interval 5s
--health-retries 10
steps:
- uses: actions/checkout@v4
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
shared-key: pr-tests
cache-on-failure: true
- name: Install nextest
uses: taiki-e/install-action@nextest
# Self-provision fresh throwaway deployer notes per run, minted from the
# public shellnet giver (`mint_pn_pool` → `<out>.seed_notes.json`, the exact
# format TestPnPool::load() reads). Replaces the old S3 fetch: once a run
# drains + withdraws a note (the hasWithdrawn latch) that note is dead for
# deployPMP (ERR_INVALID_STATE), so any long-lived hosted note goes stale.
# Fresh notes per run sidestep cross-run poisoning and need no secret.
# They are drained in the teardown step below.
# TWO NOTES, AND THE CURRENCY IS NOT INTERCHANGEABLE. The trader path
# deploys NACKL markets and stakes out of `_balance[NACKL]`; an inference
# buy is paid out of `_balance[CURRENCIES_ID_SHELL]`. Only the constructor
# writes that ledger (`_balance[tokenType] = value`), so the deposit
# currency at deploy time decides which of the two a note can do — for
# good. Topping a NACKL note up with SHELL does not help: the gas voucher
# credits PHYSICAL ECC on the account, and the escrow stopped coming from
# there. Each pool is single-currency by construction (the minter refuses
# to append a different token type), so they are minted separately and the
# two seed_notes arrays concatenated; `--profile` is what tells the loader
# which rows belong to which suite.
- name: Mint fresh e2e seed notes
working-directory: sdk
run: |
cargo run --release --bin mint_pn_pool -- \
--count 1 --nominal N10000 --token-type nackl --profile PN-API \
--endpoint "$E2E_NETWORK_ENDPOINT" \
--output "$RUNNER_TEMP/pn_pool_api.json"
cargo run --release --bin mint_pn_pool -- \
--count 1 --nominal N10000 --token-type shell --profile PN-INF \
--endpoint "$E2E_NETWORK_ENDPOINT" \
--output "$RUNNER_TEMP/pn_pool_inf.json"
jq -s 'add' \
"$RUNNER_TEMP/pn_pool_api.seed_notes.json" \
"$RUNNER_TEMP/pn_pool_inf.seed_notes.json" \
> "$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json"
test "$(jq 'length' "$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json")" = 2
jq -r '.[] | "minted \(.profile) tokenType=\(.tokenType) \(.pn_address)"' \
"$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json"
# Single-threaded on purpose: the PN-per-slot pool removes the per-PN
# `_busy` contention, but every deploy still hits the shellnet-global
# RootOracle (deployOracle) and OracleEventList (addEvent), which
# serialise on-chain — parallel deploys race to exit_code 52/101.
# `--run-ignored only` runs exactly the `#[ignore]`-marked e2e tests.
# No `-E` filter: there is nothing left to exclude. A
# `not binary(e2e_inference_dispute)` used to sit here to skip a suite that
# waited out a ~1200s acceptance window, and it outlived the suite — a
# `binary()` predicate matching nothing is an error in nextest, not a
# no-op, so the step died on the filter before running a single test.
# Anything excluded here must name a binary that exists.
- name: cargo nextest run (e2e, single-threaded)
run: cargo nextest run -p dodex-api --run-ignored only --test-threads 1 --no-fail-fast
# Teardown: drain the throwaway note to a random (uninit) address so the
# plaintext key surfaced in the mint step / fixture controls no funds after
# the run — and the note ends in the hasWithdrawn state, unusable. Runs even
# if the suite failed; best-effort (the next run mints a fresh note anyway),
# so a drain failure never fails the job.
- name: Drain e2e seed notes (teardown)
if: always()
working-directory: sdk
run: |
seed="$GITHUB_WORKSPACE/tests/fixtures/seed_notes.json"
[ -s "$seed" ] || { echo "no seed note to drain — skip"; exit 0; }
# Every row, not just the first: the file holds one note per profile
# and each one's key is in the clear. Draining only row 0 would leave
# the other spendable by anyone who read the log.
for i in $(jq -r 'to_entries[].key' "$seed"); do
jq --argjson i "$i" \
'{pn_address: .[$i].pn_address, owner_public_key_hex: .[$i].pn_pubkey_hex, owner_secret_key_hex: .[$i].pn_seckey_hex}' \
"$seed" > "$RUNNER_TEMP/pn_state.json"
dest="0:$(openssl rand -hex 32)"
echo "draining seed note $i ($(jq -r ".[$i].profile // \"<unprofiled>\"" "$seed")) to random dest $dest"
cargo run --release --bin dexdo -- withdraw \
--pn-state-file "$RUNNER_TEMP/pn_state.json" \
--dest "$dest" \
--endpoint "$E2E_NETWORK_ENDPOINT" || echo "teardown withdraw failed for note $i (non-fatal)"
done