SDK wallet test kit

The test kit of the SDK checks your wallet before go-live: it signs and sends every wallet operation and its edge cases to your wallet and reports pass or fail per case.

Run the command

The package installs the asteriks-wallet-test-kit command, which runs on Bun. Give it your wallet’s base URL, a test player and its currency, and set the API key and secret the aggregator signs your wallet calls with in two environment variables. Each case takes back the money it moved, so a run leaves the test player’s balance as it found it; the player needs a balance of at least WALLET_TEST_AMOUNT, 100 minor units.

ASTERIKS_WALLET_API_KEY=your-api-key ASTERIKS_WALLET_SECRET=your-api-secret \
  bunx asteriks-wallet-test-kit --url https://wallet.example.com/wallet --player player-1001 --currency EUR

The command prints one line per case and a summary, and exits 0 when every case passed, 1 when one failed and 2 on a usage error. A failed case is followed by its reason: the step and what the wallet answered.

asteriks wallet test kit: 15 cases against https://wallet.example.com/wallet
PASS  balance_answer  A balance call is answered in the player's currency.
FAIL  stale_timestamp_refused  A call whose timestamp is more than 30 seconds old is refused.
      balance with a stale timestamp: expected the refusal UNAUTHORIZED, got an answer with balance 10000 EUR
...
14 passed, 1 failed

--list prints the cases without sending anything. --provider (default drakon), --game-id and --session-id set the fields every call carries, and --timeout-ms the deadline of every call but a bet, 10,000 ms by default; a bet always has 3,000 ms. Every call names its case in the unsigned header x-asteriks-test-case, so your wallet’s logs show which case sent it.

What the cases check

Each case judges one rule of the wallet contract; the wallet webhooks page describes each operation, and the Bet and Rollback reference pages list every field.

  • balance_answer: a balance call is answered in the player’s currency.
  • bet_debits, win_credits, rollback_reverses_bet and round_finished_answered: a bet debits its amount, a win credits its amount, also in a round with no bet, a rollback gives a bet’s amount back and a round close is answered.
  • repeat_returns_first_answer and mismatch_is_duplicate: a repeated transaction gets its first answer and moves no money, and a transaction_id used again with another amount is refused with DUPLICATE_TRANSACTION. The idempotency page explains the rule.
  • unknown_rollback_acknowledged and late_bet_after_tombstone_refused: a rollback of an unknown transaction is acknowledged with the current balance, and the bet that arrives after it is refused with DUPLICATE_TRANSACTION.
  • unknown_player_refused and insufficient_funds_refused: a call for a player the wallet does not hold is refused with PLAYER_NOT_FOUND, and a bet larger than the balance with INSUFFICIENT_FUNDS or PLAYER_LIMIT_REACHED.
  • bad_signature_refused, stale_timestamp_refused and reused_nonce_refused: a call signed with another secret, a call whose timestamp is 60 seconds old and a call that reuses a nonce are each refused with UNAUTHORIZED. The authentication page describes the signature and its time window.
  • bet_within_deadline: a bet is answered within 3,000 ms, WALLET_BET_DEADLINE_MS.

Add a case of your own

The cases are one exported list, WALLET_TEST_CASES. runTestKitCli is the command itself: give it your own arguments, environment and output, and the shared cases with yours after them. A case calls the wallet through its context and returns { pass: true } or { pass: false, reason }; the outcome of a call is answered with the balance, refused with a canonical code, or unknown.

check-wallet.ts

import { runTestKitCli, WALLET_TEST_CASES, type WalletTestCase } from '@asteriks-aggregator/sdk/test-kit'

/** A case of your own beside the shared ones: the test player has money to bet with. */
const balanceIsPositive: WalletTestCase = {
  id: 'balance_is_positive',
  operation: 'balance',
  title: 'The test player has money to bet with.',
  run: async (context) => {
    const { outcome } = await context.call('balance', {
      player_id: context.player,
      session_id: context.sessionId,
      provider: context.provider,
      game_id: context.gameId,
      currency: context.currency,
    })
    return outcome.kind === 'answered' && outcome.balance > 0
      ? { pass: true }
      : { pass: false, reason: `balance: ${outcome.kind}` }
  },
}

/** Runs the shared cases and yours against the wallet at `url`; resolves with the exit status (0, 1 or 2). */
export function checkWallet(url: string, apiKey: string, secret: string, out: (line: string) => void): Promise<number> {
  return runTestKitCli({
    argv: ['--url', url, '--player', 'player-1001', '--currency', 'EUR'],
    env: { ASTERIKS_WALLET_API_KEY: apiKey, ASTERIKS_WALLET_SECRET: secret },
    out,
    cases: [...WALLET_TEST_CASES, balanceIsPositive],
  })
}

Run the cases from code

runWalletTestKit runs a list of cases in order and returns a report per case, so a test suite of your own can fail on a wallet change before it is deployed. Give it a WalletCaller per case, with your wallet’s base URL and credential, and a source of fresh ids: every id the kit sends is a UUID v7, so crypto.randomUUID, which makes a v4, will not do.

failed-cases.ts

import {
  runWalletTestKit,
  TEST_CASE_HEADER,
  WALLET_TEST_CASES,
  WalletCaller,
  type WalletCredential,
  type WalletTestReport,
} from '@asteriks-aggregator/sdk/test-kit'

/** The shared cases the wallet at `baseUrl` fails; `ids` gives a fresh UUID v7 per call. */
export async function failedCases(
  baseUrl: string,
  credential: WalletCredential,
  ids: () => string,
): Promise<WalletTestReport[]> {
  const reports = await runWalletTestKit({
    cases: WALLET_TEST_CASES,
    caller: (caseId) => new WalletCaller({ baseUrl, credential, headers: () => ({ [TEST_CASE_HEADER]: caseId }) }),
    target: { player: 'player-1001', currency: 'EUR', provider: 'drakon', gameId: ids(), sessionId: ids(), ids },
  })
  return reports.filter((report) => !report.pass)
}

A case that throws fails with the error’s message and the run goes on. WalletCaller never follows a redirect, so a signed body cannot leave your wallet’s host, and it judges every answer as the aggregator does: a timeout, an INTERNAL_ERROR or an answer outside the contract is unknown, never a refusal. The SDK wallet kit guide shows a wallet that passes every case.

Was this page helpful?