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/conformanceandspartacus/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:
- Your staging wallet's base URL: the address the five wallet calls hang off (
<base>/player,<base>/balance,<base>/bet,<base>/win,<base>/rollback). - Its signing secret, in an environment variable. Never put it on the command line.
- 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_allowedinstead ofinsufficient_funds). - 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
200withstatus: "not_found", moving nothing. - A fresh launch token for the first test player.
- The currencies to test, with their decimals. Put a small-decimals currency first (for example
EUR:2beforeETH: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-amountandtwo-bets-one-roundskip themselves and are marked "not attested". - In CI, add
--report jsonor--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 withuser, optionaltokens,balancesandmax_stake, andblocked;GET /ledger: every player's balances, and how many bets and rollback markers it holds;POST /reset, andPOST /secretswith{"secrets": ["new", "old"]}to replace the signing secrets (list two to imitate a rotation).
- With only
EUR:2,big-amountis 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)
| Check | Level | Why |
|---|---|---|
sig-missing | MUST | an unsigned request must be refused: anyone could send it |
sig-wrong-secret | MUST | a signature under another secret must be refused |
sig-tampered | MUST | the signature must cover the bytes actually received |
sig-expired | MUST | a signature older than the 300 s tolerance must be refused: replays must expire |
sig-future | MUST | a signature well ahead of the 300 s tolerance must be refused: a wrong clock must not widen the window |
sig-rotated | MUST | during a rotation the header carries several v1 entries; any one that matches is enough |
unknown-field | MUST | both sides ignore JSON fields they do not know, so a new optional field is additive |
unknown-enum | MUST | enums are open: a settle reason added later must still be credited |
player-resolve | MUST | the launch token identifies the player, and an unknown token must not |
bet-win | MUST | a round that is bet and won must move exactly −stake + amount |
win-zero | MUST | a losing bet is settled with amount 0, and its retry must not move anything either |
bet-rollback | MUST | a rolled-back bet must give the stake back exactly |
rollback-player-left | MUST | a player_left rollback (the player left before the round started) must give the stake back exactly |
rollback-repeat | MUST | a retry of a rollback must replay the stored result, not restore the stake a second time |
rollback-unknown | MUST | a rollback of a bet that never arrived is answered 200 not_found, not an error |
rollback-then-bet | MUST | the rolled-back transaction_uuid must be recorded, or a late bet would debit after a refund |
rollback-other-player | MUST | a rollback signed for another player must not reverse, and must not disarm, a bet it does not own |
rollback-after-win | MUST | a settled bet must never be reversed: Telos never sends this, and it signals a bug |
bet-after-rollback | MUST | a bet that was reversed must stay reversed: a re-delivery must be refused, not replayed from the idempotency store |
win-after-rollback | MUST | a settlement for a reversed bet must be refused: crediting it pays out a round whose stake was already given back |
win-after-win | MUST | the payout of a bet is credited exactly once: a second win naming a settled bet must be refused, not paid again |
insufficient-funds | MUST | a stake above the balance must be refused, and must not overdraw the player |
malformed-body | MUST | a request this contract does not describe must be refused, not guessed at |
decimals-mismatch | MUST | the wrong scale must be refused, never applied: it would move a different amount |
decimals-exact | MUST | the 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-amount | MUST | amounts above 2^53 must survive: JSON numbers would round them |
two-bets-one-round | MUST | a player may place several bets in one round: each one must be debited |
bet-repeat | MUST | a retry of a bet must replay the stored result, not debit a second time |
bet-concurrent | MUST | ten copies of one bet in flight at once must debit exactly once |
win-repeat | MUST | a retry of a settlement must replay the stored result, not credit a second time |
conflict | MUST | one transaction_uuid with two different bodies must be refused, not applied twice |
Aggregate checks, graded over the whole run (5)
| Check | Level | Why |
|---|---|---|
balance-reconciles | MUST | the run's own record of what moved must equal what the wallet actually holds, for every currency |
moved-currency | MUST | every reply that moves funds must state the balance of the currency it moved |
schema-replies | MUST | every reply must have a status the contract declares and a body that validates |
no-5xx | MUST | a 5xx leaves the outcome unknown: every request must have a known outcome |
latency | SHOULD | the 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.