Telos operator docs

The conformance kit: prove your wallet against the contract

The conformance kit sends real, signed wallet calls to your staging wallet and checks that every answer follows the operator contract (openapi.yaml): signatures, idempotency, money, rollbacks and the error codes. It is the deeper version of the back office's Test wallet (and of the Test suite, when that is offered): run it before your first real player, and in your own CI whenever your wallet changes.

The kit has two parts:

  • conformance, the checker: a command-line tool, shipped as a Docker image so you need no Node.js.
  • wallet-sim, the reference wallet Telos tests against. Run the checker against it to see what a passing wallet looks like, and compare its answers with yours.

Getting the images. The images are not on a public registry yet. Until they are, Telos runs the kit with you, or sends you the images on request: ask your Telos contact. The commands below use the image names spartacus/conformance and spartacus/wallet-sim.

#Before you run it

The kit moves real balances on the wallet you point it at. Use your staging wallet only, never production.

You need:

  1. Your staging wallet's base URL: the address the five wallet calls hang off (<base>/player, <base>/balance, <base>/bet, <base>/win, <base>/rollback).
  2. Its signing secret, in an environment variable. Never put it on the command line.
  3. A test player holding at least 1,000 minor units in each currency you test, whose own stake limits allow its balance + 1 (otherwise the "not enough money" check gets bet_not_allowed instead of insufficient_funds).
  4. A second test player on the same wallet, holding the same currencies. Nothing of theirs moves: the kit uses them to send a rollback for a bet they do not own, which you must answer 200 with status: "not_found", moving nothing.
  5. A fresh launch token for the first test player.
  6. The currencies to test, with their decimals. Put a small-decimals currency first (for example EUR:2 before ETH:18).

#Run it against your wallet

export SP_WEBHOOK_SECRET='…'      # your staging signing secret; never echoed, never logged
docker run --rm -e SP_WEBHOOK_SECRET spartacus/conformance \
  --target https://wallet-staging.example.com/telos \
  --secret-env SP_WEBHOOK_SECRET \
  --test-player player-test-1 \
  --second-player player-test-2 \
  --launch-token "$LAUNCH_TOKEN" \
  --currency EUR:2 --currency ETH:18 \
  --bets-per-round 2 \
  --yes-this-is-a-test-wallet

Read the first line it prints: it names the target and both players. Stop it if any of them is wrong.

#Read the result

Exit code Meaning
0 every MUST check passed
1 at least one MUST check failed
2 the command line is wrong (the usage is printed)
3 the kit refused to run: a required option or the secret variable is missing, the two players are the same, or the test player cannot afford the run
  • The report lists every check in run order, with the five aggregate checks last.
  • A check that was skipped says why.
  • A skipped MUST is not a pass. To attest every check, include an 18-decimal currency and run with --bets-per-round 2. Without them, big-amount and two-bets-one-round skip themselves and are marked "not attested".
  • In CI, add --report json or --report junit: the report goes to stdout and the progress to stderr.

Each failure is either a real defect in your wallet, or a real disagreement about the contract. Send the second kind to your Telos contact rather than working around it.

Clean-up: the run leaves some bets open on purpose, so it can prove a debit without a settlement (unknown-field, decimals-exact for each currency, two-bets-one-round, bet-repeat, bet-concurrent, conflict). Their stakes stay debited, and the balance-reconciles check already counts them. After the run, void or settle the test player's open bets yourself. Rotate the launch token afterwards if your tokens are reusable.

#See a passing wallet: run it against wallet-sim

docker network create telos-kit
docker run -d --name wallet-sim --network telos-kit spartacus/wallet-sim --secret=demo-secret
# seed the two test players on wallet-sim's admin port (4021)
docker run --rm --network telos-kit curlimages/curl -s -X POST http://wallet-sim:4021/players \
  -H 'Content-Type: application/json' \
  -d '{"user":"player-test-1","tokens":["launch-token-1"],"balances":{"EUR":"100000000"}}'
docker run --rm --network telos-kit curlimages/curl -s -X POST http://wallet-sim:4021/players \
  -H 'Content-Type: application/json' \
  -d '{"user":"player-test-2","balances":{"EUR":"100000000"}}'
docker run --rm --network telos-kit -e SP_WEBHOOK_SECRET=demo-secret spartacus/conformance \
  --target http://wallet-sim:4020 --secret-env SP_WEBHOOK_SECRET \
  --test-player player-test-1 --second-player player-test-2 --launch-token launch-token-1 \
  --currency EUR:2 --bets-per-round 2 --yes-this-is-a-test-wallet
docker rm -f wallet-sim && docker network rm telos-kit
  • wallet-sim serves the five wallet calls on port 4020 and an admin API on port 4021:
    • POST /players: seed a player with user, optional tokens, balances and max_stake, and blocked;
    • GET /ledger: every player's balances, and how many bets and rollback markers it holds;
    • POST /reset, and POST /secrets with {"secrets": ["new", "old"]} to replace the signing secrets (list two to imitate a rotation).
  • With only EUR:2, big-amount is skipped. Add an 18-decimal currency, and seed its balance, to attest it.

#The checks

The list below is generated from the kit's own source at the version this site was built from.

Per-request checks (31)

CheckLevelWhy
sig-missingMUSTan unsigned request must be refused: anyone could send it
sig-wrong-secretMUSTa signature under another secret must be refused
sig-tamperedMUSTthe signature must cover the bytes actually received
sig-expiredMUSTa signature older than the 300 s tolerance must be refused: replays must expire
sig-futureMUSTa signature well ahead of the 300 s tolerance must be refused: a wrong clock must not widen the window
sig-rotatedMUSTduring a rotation the header carries several v1 entries; any one that matches is enough
unknown-fieldMUSTboth sides ignore JSON fields they do not know, so a new optional field is additive
unknown-enumMUSTenums are open: a settle reason added later must still be credited
player-resolveMUSTthe launch token identifies the player, and an unknown token must not
bet-winMUSTa round that is bet and won must move exactly −stake + amount
win-zeroMUSTa losing bet is settled with amount 0, and its retry must not move anything either
bet-rollbackMUSTa rolled-back bet must give the stake back exactly
rollback-player-leftMUSTa player_left rollback (the player left before the round started) must give the stake back exactly
rollback-repeatMUSTa retry of a rollback must replay the stored result, not restore the stake a second time
rollback-unknownMUSTa rollback of a bet that never arrived is answered 200 not_found, not an error
rollback-then-betMUSTthe rolled-back transaction_uuid must be recorded, or a late bet would debit after a refund
rollback-other-playerMUSTa rollback signed for another player must not reverse, and must not disarm, a bet it does not own
rollback-after-winMUSTa settled bet must never be reversed: Telos never sends this, and it signals a bug
bet-after-rollbackMUSTa bet that was reversed must stay reversed: a re-delivery must be refused, not replayed from the idempotency store
win-after-rollbackMUSTa settlement for a reversed bet must be refused: crediting it pays out a round whose stake was already given back
win-after-winMUSTthe payout of a bet is credited exactly once: a second win naming a settled bet must be refused, not paid again
insufficient-fundsMUSTa stake above the balance must be refused, and must not overdraw the player
malformed-bodyMUSTa request this contract does not describe must be refused, not guessed at
decimals-mismatchMUSTthe wrong scale must be refused, never applied: it would move a different amount
decimals-exactMUSTthe balance in a reply must be the balance the next read reports, digit for digit, and the move must be exactly the stake — for every currency, not only the primary one
big-amountMUSTamounts above 2^53 must survive: JSON numbers would round them
two-bets-one-roundMUSTa player may place several bets in one round: each one must be debited
bet-repeatMUSTa retry of a bet must replay the stored result, not debit a second time
bet-concurrentMUSTten copies of one bet in flight at once must debit exactly once
win-repeatMUSTa retry of a settlement must replay the stored result, not credit a second time
conflictMUSTone transaction_uuid with two different bodies must be refused, not applied twice

Aggregate checks, graded over the whole run (5)

CheckLevelWhy
balance-reconcilesMUSTthe run's own record of what moved must equal what the wallet actually holds, for every currency
moved-currencyMUSTevery reply that moves funds must state the balance of the currency it moved
schema-repliesMUSTevery reply must have a status the contract declares and a body that validates
no-5xxMUSTa 5xx leaves the outcome unknown: every request must have a known outcome
latencySHOULDthe contract's webhook timeout is 2 s; slower replies risk a rollback

Command line

spartacus-conformance --target <operator webhook base URL> --secret-env <VAR> \
  --test-player <user> --second-player <user> --launch-token <token> \
  --currency <code>:<decimals> [--currency …] \
  [--game-code <code>] [--bets-per-round <n>] --yes-this-is-a-test-wallet \
  [--report json|junit|text]

The suite moves real balances on the target. Point it at the operator's staging wallet, never at
production, with two test players and a fresh launch token.

--second-player is a second test player on the same wallet, holding the same currencies. Only
--test-player's balances move; --second-player exists so the suite can send a rollback signed for
one player naming the other player's bet, which must reverse nothing and disarm nothing.