Skip to content

Changelog

Track Casino SDK releases, breaking changes, and unreleased work. Check what changed before you move your game to another version.

Versions follow semver from 0.5.0, and each heading carries the release date. In 0.x a minor bump is breaking and a patch is not. Releases before 0.5.0 used date versions, YYYY.MM.DD-N.

Released in lockstep with @chain-protocol/pvp-sdk and @chain-protocol/cli 0.7.0.

  • Optional logical guest windows via openWindow, callable only after the guest advertises the exact target. Public exports include WINDOW_TARGETS, supportsWindowTarget, WindowTarget, WindowCapabilities and WindowOpenResult. Legacy openPanel behavior is unchanged.

  • SteeringDeployer creates a graph title’s Steering contract at a predictable address. deploySteering(initCode) runs CREATE2 with a zero salt and emits SteeringDeployed(steering, initCodeHash); steeringAddress(initCodeHash) predicts it. The salt is not bound to the sender, so a build can name the address before it knows who sends the deploy, and titles with the same init code share one Steering contract.

  • steeringRefusal({ maxIndependentDraws, configurationChunks, nodeChunks }) returns why derive or GraphTitleDeployer would refuse a packed title under a Steering contract (a ladder, or a table bundle), or null. deployGraphGame throws the same reason as before.

  • A graph deployment’s steering takes constructorArgs, with the same {{diamond}} and {{token}} placeholders as a contract deployment. defineDeployment defaults it to [], and the local node passes it to the Steering contract’s constructor.

  • defineDeployment takes an optional frontend for every kind: directory, framework (vite, nextjs or tanstack-start), installCommand, buildCommand and outputDirectory. It tells the platform where the frontend lives and how it builds; without it the platform detects the framework.

  • Slot and graph titles name only chunks their own deployer created. SlotTitleDeployer and GraphTitleDeployer record every pointer deployChunk creates in isChunk, and refuse any other pointer with SlotTitleDeployer__NotAChunk(pointer) or GraphTitleDeployer__NotAChunk(pointer). A contract that only looks like a chunk could otherwise change its code after the deployer derived the title’s RTP from it. The deploy helpers already upload through the deployer they create the title with. The fix ships as a new version of both deployers in slotGameDeployerArtifact and graphGameDeployerArtifact.
  • Graph titles: a chance cannot reach a stake it never escrowed. Derive now refuses a chance any of whose outcomes holds a stake other than the chance’s own, with GraphDerive__ChanceStakeMismatch(id), as the compiler always has. A decision holds its action 0’s stake. A forfeit at a decision pays the default value less any part of that stake the session has not escrowed, as a settlement does. Engines and titles are immutable, so the fix ships as a new GraphTitleDeployer: graphGameDeployerArtifact carries it, and titles from the previous deployer keep the previous engine. Inline derive costs about 2% more gas and batched derive under 0.5% more; the inline-scale graph still derives in one transaction.
  • GraphNode’s chance kind takes an optional stake, the stake a packed chance records. unpackGraph sets it only where it differs from the stake the chance’s first outcome holds, which the compiler never packs, and deriveGraph refuses such a chance. graphGameAbi lists the new error.
  • Breaking: the player’s contract is called the Smart Vault again. HostSnapshotV1 reports it as wallet.smartVaultAddress and balances.smartVaultBalance, replacing smartWalletAddress and smartWalletBalance. The docs, the simulator and the examples use the new names.
  • Breaking: every creation call names its game developer. SlotTitleDeployer.deployTitle and deployThresholdTitle, GraphTitleDeployer.deployTitle and deployBatchedTitle and GameBytecodeDeployer.createGame take address developer before authorshipId, and the registry’s registerEngineTitle and registerBytecodeGame take it after creator. Anyone may name any developer, since the name only directs the game dev fee. A zero developer reverts with GameRegistryFacet__ZeroDeveloper (LocalGameRegistry__ZeroDeveloper on the simulator), and with an authorship the named developer must be its writer, else Authorship__DeveloperMismatch(authorshipId, developer). underivedTitle carries developer after creator. The SDK’s deployTableGame, deployThresholdGame, deploySlotGame, deployGraphGame and createBatchedGraphGame take an optional developer and name the wallet client’s account without one. Against a deployer from before this change the wrappers simulate the new call first and, when the deployer has no function for it, send the call without a developer, whose registry makes the sender the developer; that fallback goes once every registered deployer is redeployed.
  • Breaking: casino randomness settles whenever it lands. A word delivered after the deadline reaches onRandomness like any other, instead of refunding the stake. The randomness timeout is now 45 seconds and cannot be set lower, so cancelStuckRandomness refunds only after that. LocalCasinoHost.RANDOMNESS_TIMEOUT_SECONDS is 45. See ADR 0026.
  • The Title names listed under 0.6.0-canary.3 are no longer deprecated. They stay as aliases of the Game names, and the deployed results’ title stays beside game.

0.6.0 publishes the 0.6.0 canaries under the latest dist-tag, together with the changes of 0.5.2. Coming from 0.5.x, read the three canary entries below: they list every breaking change and every rename.

  • Engines first. Getting Started and the overview tell game devs to write a game for the slot engine or the graph engine whenever it fits: the chain checks the math when it deploys, the review covers the frontend only, and a game at or below the approval ceiling is whitelisted when it deploys. An own ICasinoGameV2 contract is for games neither engine can express.
  • A Graph Engine guide: writing a title, checking its figures, playing it locally, submitting it, the frontend, fair ladders, table bundles, Steering contracts and deploy costs. Its example, Climb, is a tower and a scratch card with a full frontend: bun run start:graph in a checkout plays it on the SDK simulator.
  • The Slot Engine page covers the Game names, the browser-safe slot-engine/threshold subpath and how to deploy a threshold Game.
  • A browser-safe quote of a plain graph configuration. quoteConfiguration(table, configuration, wager) from @chain-protocol/casino-sdk/graph-engine returns, bit for bit, what the engine’s quoteCaps, quoteRiskParams and onSessionStart reserve give encodePlainGameData(configuration): maxEscrowStake, maxReservedProfit, openingReservedProfit, maxPayout, probabilityWad, expectedPayout and bodyVarianceScaled (the type GraphQuote). graphQuoteTable(compiled, { steered }) builds its GraphQuoteTable from a compiled Game; store its figures and quote in the browser without deriving there. A Game deployed with a Steering contract is steered and reserves its whole cap when a session opens. Out-of-range configurations throw a RangeError.
  • @chain-protocol/casino-sdk/slot-engine/threshold, a browser-safe subpath with no Node imports: thresholdGameData, decodeThresholdGameData, rollResult, rollThreshold, decodeThresholdGameState, quoteThreshold, thresholdRtpWadRange, thresholdGameInputsHash, ThresholdTargetOutOfRangeError and the types. quoteThreshold now also returns maxEscrowStake, maxReservedProfit and openingReservedProfit, so it answers what quoteCaps, quoteRiskParams and the opening reserve give.
  • @chain-protocol/casino-sdk/local-host, for host-path tests on EDR or any dev chain: deployLocalCasinoHost, whitelistOnLocalHost, decodeCasinoSession, and the ABIs and bytecode of LocalCasinoHost, LocalTestToken and TestRandomnessRouter, whose fulfill(requestId, word) delivers the word a test picks.
  • Slot threshold Games from a TS module. deployment.ts takes { kind: 'threshold', name, title }, where title names a module whose default export lists the Game’s ThresholdConfigurations by index. chain dev casino deploys it through the simulator’s SlotTitleDeployer.
  • Game IDs (ADR 0025, proposed). The simulator’s registry gives each registered game the next Game ID from 1, with gameIdOf, gameOfId, gameCount and a GameIdAssigned event, and /__local-contracts.json lists every game with its gameId. The production diamond gains the same once the security council cuts the GameRegistryFacet.
  • runCasinoProperties and runCasinoConformance (./conformance) take { address } of a Game already on the chain as well as runtime code, so they run an engine clone whose implementation is there.
  • quoteBundle returns openingReservedProfit too, the whole cap, and its result is a GraphQuote.
  • thresholdGameData throws a RangeError (still an Error) for an index or target out of range.
  • Smaller guests. The engine and deployer ABIs are marked pure and tree-shake away: a guest that walks and encodes a graph Game drops from 46.3 KB to 18.8 KB minified (12.5 KB to 6.2 KB gzip).
  • Faster derive. A Game’s max net is derived once, not per configuration: Dice’s 9,799 configurations compile in 0.2 s instead of 2.6 s, with the same figures.
  • chain dev casino watches every module a Game module imports, workspace packages outside the project included, and redeploys when one changes. Installed packages under node_modules stay unwatched.
  • chain dev casino leaves its reference CoinflipGame out of the game picker when the project’s Game registers under that name.
  • LocalCasinoHost’s bytecode changes for the Game IDs. Read it from ./local-host rather than a copy.
  • Title is now Game in the TypeScript API (original-games spec U2). Every old name below still works and is the same value, and goes in 0.7.0. Deployed results carry the address as game beside the deprecated title. The Solidity contracts, their functions, events and errors, the deployment.ts field title and the simulator’s contract keys (slotTitleDeployer, graphTitleDeployer) keep their names.

    SubpathOld nameNew name
    graph-enginecompileGraphTitlecompileGraphGame
    graph-engineGraphTitleDefinitionGraphGameDefinition
    graph-engineCompiledGraphTitleCompiledGraphGame
    graph-enginedeployableTitledeployableGame
    graph-enginedeployGraphTitledeployGraphGame
    graph-engineDeployGraphTitleParamsDeployGraphGameParams
    graph-engineDeployedGraphTitleDeployedGraphGame
    graph-enginecreateBatchedGraphTitlecreateBatchedGraphGame
    graph-enginederiveGraphTitlederiveGraphGame
    graph-enginegraphTitleInputsHashgraphGameInputsHash
    graph-enginegraphTitleAbigraphGameAbi
    graph-enginegraphTitleDeployerAbigraphGameDeployerAbi
    graph-enginegraphTitleDeployerArtifactgraphGameDeployerArtifact
    graph-engineMAX_TITLE_CHUNKSMAX_GAME_CHUNKS
    slot-engineslotTitleAbislotGameAbi
    slot-engineslotTitleDeployerAbislotGameDeployerAbi
    slot-engineslotTitleDeployerArtifactslotGameDeployerArtifact
    slot-enginereadTitlereadTableGame
    slot-enginereadThresholdTitlereadThresholdGame
    slot-enginedeployTitledeployTableGame
    slot-engineDeployTitleParamsDeployTableGameParams
    slot-engineDeployedTitleDeployedSlotGame
    slot-enginedeployThresholdTitledeployThresholdGame
    slot-engineDeployThresholdTitleParamsDeployThresholdGameParams
    slot-enginedeploySlotTitledeploySlotGame
    slot-engineDeploySlotTitleParamsDeploySlotGameParams
    slot-enginecompileSlotTitleFilecompileSlotGameFile
    slot-enginecompileTitleFilecompileTableGameFile
    slot-engineTitleDefinitionSlotGameDefinition
    slot-engineCompiledTitleCompiledTableGame
    slot-engineThresholdTitleThresholdGame
    slot-engineSlotTitleSlotGame
    both slot subpathsthresholdTitleInputsHashthresholdGameInputsHash
  • Nothing on chain changes for Games: engine titles deployed with canary.1 or canary.2 keep working, and these quotes match them.
  • Code that compares a whole quoteThreshold or quoteBundle result, or spreads it into an array, sees the added fields.
  • The old Title names compile with a deprecation warning. Move to the new names before 0.7.0.
  • Committed ladder rounds (ADR 0023). Game data 0x03 ‖ uint16 configuration ‖ uint8 roundCount ‖ roundCount × uint8 option ‖ tail opens a session at a fair-ladder root that plays every listed option in one randomness callback and settles. Auto-bet can now drive a ladder such as Hilo. @chain-protocol/casino-sdk/graph-engine adds encodeCommittedLadderGameData, decodeCommittedLadderGameData, checkCommittedLadder, walkCommittedLadder, committedLadderReserve, InvalidCommittedRoundsError and the type CommittedLadderRounds. A configuration takes at most its committedRounds options, which derive sets from the callback gas and the ladder’s widest option: 93 for Hilo, 16 for options of 256 outcomes. More rounds, none, or a root that is not a ladder revert GraphEngine__InvalidCommittedRounds.
  • raw.actions on session items: every actionData submitted to the session, in step order, as SubmittedAction { step, actionData, transactionHash? }. step is the step the action produced, so a randomness request that step made has it as its nonce. The list includes the action that settled the session and is empty before the first action. A multi-step game rebuilds a session from randomnessRequests and actions after a reload or on another device. A host that does not report actions omits the field, so treat undefined as unknown.
  • LocalCasinoHost, and the production casino facet, emit CasinoSessionActionSubmitted(uint256 indexed sessionId, uint32 indexed step, bytes actionData) for every player action, before the step’s CasinoSessionAdvanced or CasinoSessionSettled. The simulator reports raw.actions from it.
  • Breaking: graph titles need the new GraphTitleDeployer. ConfigurationFigures, which configurationFigures and derive return, ends with uint8 committedRounds, and DerivedGraph’s configurations carry it. Titles of the canary.1 engine reject tag 0x03 with GraphEngine__InvalidGameData. Redeploy a game, and regenerate any artefact that stores derived figures.
  • Breaking: SlotTitleDeployer and GraphTitleDeployer take the diamond’s game registry (IGameRegistry) as their only constructor argument and register every title on it with the creator, an authorship id, the title inputs hash and the lowest and highest RTP of its bet configurations. deployTitle, deployThresholdTitle and deployBatchedTitle take an authorshipId, zero for none, and the SDK’s deploy functions an optional authorshipId. The deployers’ RTP band, setRtpBand and owner are gone: they still refuse an RTP of 100% or more, and the diamond checks the RTP band. titleAuthor is now titleCreator, and GraphTitleDeployer.pendingTitleAuthor is now underivedTitle, and GraphTitleDeployer__NotPending is now GraphTitleDeployer__NotDeriving.

  • Breaking: casino deadlines are block timestamps. CasinoSession.deadlineBlock is now deadline, a session has 30 seconds to receive its randomness and 86,400 seconds (24 hours) to act, and LocalCasinoHost exposes ACTION_TIMEOUT_SECONDS and RANDOMNESS_TIMEOUT_SECONDS. The conformance suite expires a session with advanceTime.

  • Breaking: randomness delivered after its deadline no longer reaches onRandomness. The host cancels the session and refunds the stake, as cancelStuckRandomness does, and reports a zero randomness in CasinoSessionSettled.

  • Breaking: a game gets one random value per request. A step reached from onRandomness may not move to WAITING_RANDOMNESS. The casino facet reverts the callback with CasinoGameFacet__RandomnessRequestedFromCallback, and once the randomness deadline passes cancelStuckRandomness refunds the session’s whole remaining escrow. Derive further values from the one you received.

  • openSession and submitAction are payable. The call whose step requests randomness carries its fee, and a call whose step requests none gets its ETH back.

  • The simulator pays for randomness the same way. LocalCasinoHost starts with an empty router balance, the simulator page attaches the host’s requiredPayment to every open and action, and a step reached from onRandomness that requests randomness fails delivery with LocalCasinoHost__RandomnessRequestedFromCallback.

  • Optional Host capability discovery on guest connections and playAccess opt-in for requestPlayAccess(). Casino gains the access intent; PvP keeps its existing optional method and legacy callers. An acknowledgment means the existing Host UI was accepted, never that the player is authenticated or ready. Pending duplicate requests coalesce, and connection destruction invalidates pending/queued requests. Simulator requests record explicit fixture intent without automatically changing readiness. See “Requesting play access” in the README.
  • Breaking: casino deadlines are block timestamps. CasinoSession.deadlineBlock is now deadline, a session has 30 seconds to receive its randomness and 86,400 seconds (24 hours) to act, and LocalCasinoHost exposes ACTION_TIMEOUT_SECONDS and RANDOMNESS_TIMEOUT_SECONDS. The conformance suite expires a session with advanceTime.
  • Breaking: randomness delivered after its deadline no longer reaches onRandomness. The host cancels the session and refunds the stake, as cancelStuckRandomness does, and reports a zero randomness in CasinoSessionSettled.
  • Breaking: a game gets one random value per request. A step reached from onRandomness may not move to WAITING_RANDOMNESS. The casino facet reverts the callback with CasinoGameFacet__RandomnessRequestedFromCallback, and once the randomness deadline passes cancelStuckRandomness refunds the session’s whole remaining escrow. Derive further values from the one you received.
  • Breaking: a step reached from onRandomness may not change the escrow. The casino facet reverts the callback with CasinoGameFacet__EscrowChangedFromCallback, which replaces CasinoGameFacet__EscrowIncreaseNotAllowed, and once the randomness deadline passes cancelStuckRandomness refunds the session’s whole remaining escrow. Return escrow from onPlayerAction instead. The simulator refuses it with LocalCasinoHost__EscrowChangedFromCallback.
  • openSession and submitAction are payable. The call whose step requests randomness carries its fee, and a call whose step requests none gets its ETH back.
  • The simulator pays for randomness the same way. LocalCasinoHost starts with an empty router balance, the simulator page attaches the host’s requiredPayment to every open and action, and a step reached from onRandomness that requests randomness fails delivery with LocalCasinoHost__RandomnessRequestedFromCallback.
  • Guest panels. A game can declare panels the host may open from its toolbar: connectGameToHost(methods, { capabilities: { panels: [...] } }) with an openPanel({ panel }) method. Casino panels are info, history and audio. The host reads the declaration from connection.capabilities on the connection connectHostToGame returns. A game that declares nothing keeps working as before: the host gets an empty capability set and shows no panel controls. See “Host capabilities and panels” in the README.
  • The bridge runs on penpal 8, which fixes listener and timer leaks on failed or destroyed handshakes. A host on penpal 8 still connects to games built on SDK 0.5.0 with penpal 7.
  • sdkVersion in the snapshot. HostSnapshot.sdkVersion is the version of the SDK the host was built with, for example 0.5.0 or 0.6.0-canary.3. The production host and the simulator set it from the SDK’s package.json when they are built.
  • hostSupports(snapshot, version) answers whether the host’s SDK is at version or newer. It compares major, minor and patch and ignores the prerelease tag, so a canary host answers as the version it previews. It answers false for a snapshot without sdkVersion, which comes from a host older than 0.5.0.
  • Breaking: the package is @chain-protocol/casino-sdk. It was @chain/casino-sdk.

  • Breaking: apiVersion is removed from the snapshot and from game.manifest.json. schemaVersion stays as the manifest file’s format version. A manifest that still carries apiVersion: 1 validates, and the validated manifest leaves it out.

  • Breaking: the TypeScript type names lose their version suffixes, with no aliases left behind. The Solidity names, such as ICasinoGameV2, keep theirs.

    BeforeAfter
    CasinoGameManifestV1CasinoGameManifest
    HostSnapshotV1HostSnapshot
    HostApiV1HostApi
    GuestApiV1GuestApi
    RandomnessRequestV1RandomnessRequest
    RandomnessRequestEchoV1RandomnessRequestEcho
    RandomnessRequestVerificationV1RandomnessRequestVerification
    RandomnessVerificationV1RandomnessVerification
    VrfVerificationChecksV1VrfVerificationChecks
  • The slot engine is part of the SDK. Import its library from @chain-protocol/casino-sdk/slot-engine and its contracts from @chain-protocol/casino-sdk/slot-engine/contracts/*. Its title commands move to the chain CLI: chain slots <command> from @chain-protocol/cli replaces bun run cli -- <command> in slot-engine/, and the slot-engine bin is gone. From the SDK root, slots:e2e, slots:example and slots:test replace e2e:simulator, example:generate and test.

  • Breaking: the game interface and the session codec moved from simulator/contracts/ to contracts/. Import them as @chain-protocol/casino-sdk/contracts/ICasinoGameV2.sol, .../CasinoSession.sol and .../CasinoSessionCodec.sol.

  • The package publishes to npm as ESM with type declarations, MIT, for Node 22+ and Bun 1.4.2+. It publishes ., ./guest, ./contracts/*, ./slot-engine and ./slot-engine/contracts/* for games, and ./host and ./conformance as internal subpaths outside semver. ./scripts/* is gone.

  • Versions follow semver, in lockstep across both SDKs and the chain CLI. Breaking releases publish as X.Y.Z-canary.N under the npm canary tag before production switches.

  • The SDK ZIP on the docs site and the shadcn add @chain/casino-sdk registry item. Install @chain-protocol/casino-sdk from npm.
  • Replace @chain/casino-sdk with @chain-protocol/casino-sdk in your imports and package.json.
  • Rename the types in the table above.
  • Import ICasinoGameV2.sol from @chain-protocol/casino-sdk/contracts/ in Hardhat, or remap @chain-protocol/=node_modules/@chain-protocol/ in Foundry.
  • Stop reading snapshot.apiVersion. To branch on the host, call hostSupports(snapshot, '0.6.0'); never parse sdkVersion yourself.
  • Remove apiVersion from your game.manifest.json. A manifest that keeps it still validates.
  • hostApi.getMaxWager({ gameData }) on HostApiV1. A game asks the host for its maximum wager and passes the game data it is about to open the session with. The answer is a MaxWagerResult: { kind: 'limit', maxWager }, { kind: 'no-limit' } or { kind: 'unknown' }. Without game data (hostApi.getMaxWager()) the host answers for empty game data, the game’s base configuration. The production host reads the vault’s maximum wager for the game it serves and answers unknown when it cannot be worked out.
  • “Max-wager answer” in the simulator’s setup panel. The harness answers getMaxWager with a fixed cap in whole tokens, 1000 by default, or with no-limit or unknown.
  • Breaking: computeMaxWager is removed. A vault’s bet-size cap follows from the game’s quotes and the vault’s state, so a game can no longer compute it from a multiplier.
  • Breaking: the snapshot’s casino block is removed, with its four fields availableLiquidity, maxBetRiskBps, maxAllowedReservedProfit and maxBetAmount.
  • Breaking: maxWager in MaxWagerResult is a decimal string in token base units, like the wager of openSession. It was a bigint.
  • Breaking for hosts: getMaxWager is a required method of HostApiV1.
  • The coinflip and Lucky Reels examples ask the host for their maximum wager. The coinflip example passes the game data of the selected pick.
  • A game’s quotes set its fees and its bet-size cap. openSession computes every fee of a session from its edge, wager − expectedPayout, and works the largest wager it accepts out of the four quoteRiskParams figures and maxReservedProfit. The game dev fee is credited when the session settles or is forfeited, whether the player won or lost. A cancelled session pays no fee. CONTRACT_CONSTRAINTS.md has a new section, “What your quotes decide”, with what each figure must mean, the fee split and the bet-size cap.
  • Quotes are checked against guardrails at whitelisting and at every openSession. A quote outside the RTP band, with maxPayout below expectedPayout, with a body variance of 0 while outcomes below maxPayout pay, or with a plain variance that is not positive reverts with QuoteFiguresLib__RtpOutsideBand, QuoteFiguresLib__MaxPayoutBelowExpectedPayout, QuoteFiguresLib__BodyVarianceMissing or QuoteFiguresLib__VarianceNotPositive.
  • A wager above its bet-size cap reverts with CasinoGameFacet__WagerAboveRiskBudget, CasinoGameFacet__WagerAboveBreakEvenRule or CasinoGameFacet__NetWinAboveSingleWinCap. These replace CasinoGameFacet__BetRiskExceedsLimit.
  • The reserve reads expectedPayout and holds the exact count of jackpots. It expects the vault to keep a session’s LP income and nothing else of its edge, and for a heavy-tail game it holds the smallest number of jackpots that covers 99.99% of outcomes, one or two more than before. SLOTS_RISK_AND_RESERVES.md has the rule and a worked example.
  • Breaking for hosts that decode session bytes: the fixed part of the encoded session grew from 255 to 295 bytes. The session’s edge and the three rates its fees are computed from follow operator.
  • Docs corrected: StepResult has no outcome field, the default randomness timeout is 15 blocks, and quoteCaps returns a session’s largest stake and net win, not a wager range.
  • Replace computeMaxWager(snapshot, { maxMultiplierX }) with await hostApi.getMaxWager({ gameData }), where gameData is what you pass to openSession. A game with one bet configuration calls hostApi.getMaxWager().
  • Read the limit as answer.kind === 'limit' ? BigInt(answer.maxWager) : undefined.
  • The call is asynchronous and the answer belongs to one bet configuration. Ask again when the player changes the configuration, and on an interval. The coinflip example’s src/lib/useMaxWager.ts is a hook you can copy.
  • Delete every read of snapshot.casino. A check of your reserved profit against maxAllowedReservedProfit becomes wager > BigInt(answer.maxWager).
  • Treat unknown, and a rejected call, as “fall back to your own limits”, never as unlimited.
  • ICasinoGameV2 is unchanged, so a game compiles and deploys as before. Check its quotes against “What each return must mean” in CONTRACT_CONSTRAINTS.md: expectedPayout under the player’s best policy, probabilityWad for exactly maxPayout, body variance in wei² × 1e18. openSession reverts for a game whose quote fails a guardrail.
  • A host that decodes CasinoSessionAdvanced.session itself must update its codec; snapshots from the previous layout no longer decode.
  • The simulator’s chUSD has six decimals, like USDC on mainnet. The local node funds the player and the vault in six-decimal amounts.
  • The coinflip and Lucky Reels examples fall back to USDC and six decimals when the snapshot’s token carries no symbol or decimals. The guide’s snippets fall back to 6, and the slot engine’s simulator end-to-end run sizes its wagers with the token’s decimals.
  • Parse and format amounts with snapshot.token.decimals. A game that assumes 18 decimals shows amounts a trillion times too small.
  • The player’s contract is called the Smart Wallet. HostSnapshotV1 reports it as wallet.smartWalletAddress and balances.smartWalletBalance; the docs, the simulator and the coinflip example use the new names.
  • Games that read the player’s contract address or balance from the snapshot must switch to the new field names.
  • Slot engine add-on in slot-engine/: ship a slot as a settlement table on one shared engine contract, with no per-title Solidity. bun run start:slots runs the local stack with the Lucky Reels example. See SLOT_ENGINE.md.
  • Stake Engine importer. bun run cli -- import-stake converts a Stake Engine math export into a slot engine title, keeping each mode’s RTP and a sample of its books for the frontend. See MIGRATING_FROM_STAKE_ENGINE.md.
  • The SDK’s start scripts run workspace members through scripts/run-in.ts, which uses the package manager that started them, so bun, npm, pnpm and yarn all work. A pnpm-workspace.yaml ships alongside package.json’s workspaces.
  • Nothing changes for existing games; the add-on is a separate workspace.
  • Operators (not open yet). CasinoGameFacet gained openSession(game, vault, wager, gameData, operator), which attributes a session to the frontend that served it. Operator registration is closed for now: every session resolves to the protocol’s default operator, and a missing or unregistered code never reverts. See §7.1 of CHAIN_WTF_CASINO_GAMES.md.
  • The simulator’s LocalCasinoHost mirrors the overload, with permissionless setOperator / setDefaultOperator stand-ins for the council-controlled registry.
  • Games and guests need no change: the four-argument openSession still works and the operator never reaches SessionContext or the iframe.
  • The encoded session grew from 235 to 255 fixed bytes (operator sits after the risk flags). A host that decodes CasinoSessionAdvanced.session itself must update its codec; snapshots from the previous layout no longer decode.
  • getRandomnessVerification follows the current Verify Network router. The router now keeps only the id of an open request and stores nothing after fulfillment, so the host verifies from the fulfillment transaction alone: the RandomnessFulfilled log, the request echoed in the calldata (hashed back to requestId, whose derivation gained the locked gasPrice and a service tag), and the enclave signature, whose EIP-712 domain is now version 2. The signing key is recovered from that signature — nodes register under the address of their key — and the ECVRF proof is checked under it; no router view is read.
  • checks are now the four that remain meaningful: vrfProofValid, vrfBetaMatchesRandomness, enclaveSignatureValid, signerMatchesFulfiller. valid requires all four.
  • request carries the router’s current request fields: consumer, sequence, fulfiller, assignedAt, fee, gasPrice, callbackGasLimit, callbackData.
  • The simulator’s bundled local-verify-network deploys the current router and fulfills with an RFC 9381 prover on @noble/curves; getRandomnessVerification runs the same checks against it as production.
  • Removed from verification results: the fulfillmentCommitted and fastVerifyComponentsMatch checks, the artifacts.uPoint / artifacts.vComponents hints, and fulfilledAt. The echoed request’s requester, payment and clientData are now consumer, fee and callbackData. Verify views that render checks by name should drop the two removed rows; valid keeps its meaning.
  • SessionPhase is now exported from the package root and ./guest. It mirrors the Solidity enum, so games can compare phase === SessionPhase.SETTLED instead of hardcoding 3. sessions.items[].phaseName is typed as the matching SessionPhaseName union.
  • Payout cap section in CONTRACT_CONSTRAINTS.md, covering two traps that revert large wins with InvalidPayout: a payout computed separately from the reserve and ending up a few base units above it, and releasing reserved profit on the settling step.
  • Breaking: computeMaxWager returns a MaxWagerResult instead of bigint | undefined: { kind: 'limit', maxWager }, { kind: 'no-limit' } (limits are published but none binds this bet: ≤1x multiplier and no maxBetAmount), or { kind: 'unknown' } (the host publishes nothing to derive a limit from). Previously the last two both returned undefined.
  • The game interface moved from solidity/ICasinoGameV2.sol to simulator/contracts/ICasinoGameV2.sol, so drop-in games import it as ./ICasinoGameV2.sol. The local node also resolves any other import path ending in ICasinoGameV2.sol to that file, so a contract can keep its own repo’s import path.
  • CONTRACT_CONSTRAINTS.md no longer says openSession calls onSessionStart twice. The facet has called it once since the stateless-session release.
  • Update computeMaxWager callers: result.kind === 'limit' ? result.maxWager : undefined restores the old value. Contracts are unaffected.
  • Games importing ../../solidity/ICasinoGameV2.sol keep compiling in the simulator. The import falls back to the new file by name.
  • Stateless casino sessions. The facet no longer stores sessions: it keeps keccak256(encodedSession) per id, emits the encoded snapshot in CasinoSessionAdvanced(sessionId, step, requestId, randomness, session) on every non-terminal step, and takes it back as calldata — openSession(game, vault, wager, gameData), submitAction(encodedSession, actionData), cancelStuckRandomness(encodedSession), forfeitExpiredSession(s)(bytes…). CasinoSessionOpened shrank to (sessionId, game, player, vault, wager) and CasinoSessionSettled now carries the fulfilling randomness instead of gameName; CasinoSessionPhaseAdvanced, CasinoSessionEscrowUpdated, CasinoSessionRandomnessRequested and CasinoSessionRandomnessFulfilled are gone. The game-facing bridge is unchanged — the host resolves the snapshot bytes itself.
  • Casino randomness provider. Like PvP, the casino facet requests through a swappable IRandomnessProvider (CasinoConfigFacet.setRandomnessProvider / getRandomnessProvider, event RandomnessProviderUpdated) and receives onCasinoRandomnessFulfilled(requestId, randomness, clientData) from it. The production provider is VerifyNetworkCasinoRandomnessProvider, a stateless adapter that echoes the session snapshot through the router’s clientData; it holds the prepaid VRF balance instead of the diamond.
  • randomnessRequests[].nonce is now the session step that made the request (strictly increasing, not consecutive: a coinflip’s only request is "1").
  • getRandomnessVerification verifies against the redesigned Verify Network router, which stores only keccak256(proofCommitment ‖ fulfilledAt) per request: artifacts come from the fulfillment transaction (the RandomnessFulfilled log and the request echoed in the calldata). artifacts.uPoint / artifacts.vComponents are now optional derived hints; new fields request, fulfilledAt; new check fulfillmentCommitted (the router still commits to this proof — a successful challenge clears it). valid requires all six checks.
  • The simulator’s LocalCasinoHost mirrors the stateless facet (simulator/contracts/casino/ holds the session struct + codec) and its getRandomnessVerification now runs the same checks as production against the local router.
  • randomnessRequestData on HostApiV1.openSession / submitAction is ignored; the facet requests randomness itself. Omit it.
  • sessions.items[].stake in HostSnapshotV1: the session’s total escrowed stake — the wager plus mid-session increases (e.g. a blackjack double/split), sourced from the CasinoSessionEscrowUpdated event. Updates flow through the flashblock layer, so an increase reaches the snapshot without waiting for the indexed feed. Games no longer need to reconstruct total escrow from their own gameState.
  • The field is optional — feature-detect it. It is absent on hosts that predate this release and on sessions from before CasinoSessionEscrowUpdated existed; fall back to wager (or your own reconstruction). On the live platform it stays absent until the facet upgrade that emits the event ships; the simulator publishes it already.
  • Forfeit cash-out quote — ICasinoGameV2.quoteForfeitPayout(ctx). New view on the game interface returning the session’s current cash-out value (stake + accrued winnings) decoded from ctx.gameState. When a session is abandoned past actionDeadlineBlock, forfeiture now pays the player min(quote, escrowedStake + reservedProfit) minus a 10% cut (FORFEIT_WINNINGS_CUT_BPS = 1000) instead of confiscating the whole stake. The facet calls it defensively (gas-capped staticcall, exactly 32 return bytes); any revert, malformed return, or absence of the method pays 0 — the pre-change behavior.
  • Who should quote: only games with a true anytime cash-out whose value is fully determined by already-revealed state (mines-style). Games whose mid-round value depends on unresolved randomness or hidden state (blackjack-style) must return 0 — any concrete quote there is an adverse-selection exploit (players abandon exactly when their real continuation value is below the quote). See CHAIN_WTF_CASINO_GAMES.md § quoteForfeitPayout semantics.
  • The simulator’s LocalCasinoHost mirrors the facet logic, so the forfeit payout path is testable locally.
  • Backwards compatible: already-deployed games lack the method and keep paying 0 on forfeit. The method is part of ICasinoGameV2, so newly compiled games must implement it — returning 0 unconditionally is a valid implementation (and the required one for blackjack-style games). On-chain activation on the live platform awaits the facet upgrade; the simulator enforces the new behavior already.
  • Live platform bet limits. The Chain.wtf host (and the simulator) now populate the snapshot’s casino block on every refresh: availableLiquidity, maxBetRiskBps, maxAllowedReservedProfit (the per-bet reserved-profit cap openSession enforces via CasinoGameFacet__BetRiskExceedsLimit), and the new casino.maxBetAmount — the absolute per-game wager ceiling when the platform configures one (unset or '0' = none).
  • computeMaxWager(snapshot, { maxMultiplierX }) guest helper (exported from the package root and ./guest): converts the casino block into the largest wager the facet accepts for a game whose worst-case payout is wager * maxMultiplierX, accounting for maxBetAmount. Rounds conservatively (never returns a wager the facet would still reject) and returns undefined on hosts that don’t publish limits. Use it to clamp bet inputs so players can’t submit bets the chain will bounce; games with gameData-dependent multipliers should pass the multiplier of the current selection, or invert their exact quoteCaps math against maxAllowedReservedProfit (the bundled coinflip example shows both the exact gate and the helper-driven “Max” clamp).
  • Additive and optional — existing games keep working. An absent casino block means “limits unknown” (older host), not “unlimited”.
  • simulator/ — a fully standalone local test setup, needing nothing outside the SDK package. The package is now an npm workspace: one npm install at the SDK root covers the simulator, the bundled VRF node and the example game, and one npm start runs the whole local stack (chain + VRF node + casino deployment + harness + example). Individually, npm run local-node boots a chain (spawns anvil if none is running), deploys the real Verify Network VRF router + fulfilling node (via the bundled local-verify-network), and deploys a minimal casino: a freely mintable test token, LocalCasinoHost — a stand-in for the production CasinoGameFacet that runs the full ICasinoGameV2 lifecycle and emits byte-identical events — and the real CoinflipGame contract (vendored compiled artifact) so the bundled example works out of the box. npm run dev starts the harness (Vite + React + Tailwind): it mounts any game iframe by URL and drives it through the same bridge as the Chain.wtf host, replicating the production host’s behavior so games face the real integration quirks: optimistic pending: session rows, a near-instant flashblock push layer plus a lagged (tunable) indexed session feed, atomic settled flips with monotonic results, the game-steered balance ledger, and the stuck-randomness cancel path. The collapsible setup panel can also force wallet.status to exercise non-ready game states. See simulator/README.md.
  • Tooling only — no bridge/type changes. getRandomnessVerification is not implemented by the harness (the call rejects, as it may on any host that doesn’t support it).
  • Added transaction hashes to session items so games can render real block-explorer /tx/ links instead of falling back to the game contract’s address page:
    • raw.openTransactionHash — the transaction that opened the session (the bet). The Chain.wtf host populates it on the optimistic row as soon as the relayer confirms, so it is available mid-round, before the indexer projection catches up.
    • raw.settleTransactionHash — the transaction that settled the session; absent until settled.
    • raw.randomnessRequests[].transactionHash — the VRF fulfillment transaction, per request; absent until that request is fulfilled. Because the getRandomnessVerification result extends the request shape, the same hash also appears on each verification entry.
  • Explorer links prove a transaction exists, not that the randomness is fair — keep the client-side getRandomnessVerification verdict as the primary fairness signal and treat /tx/ links as secondary. See RANDOMNESS_VERIFICATION.md.
  • Additive and optional: older hosts omit all three fields, and sessions projected before this version have no hashes (only a re-indexed environment backfills them). Keep an address-link (or no-link) fallback when a hash is absent.
  • Added raw.randomnessRequests to session items — every VRF request of the session in request order ({ nonce, requestId, randomness?, fulfilled }). Multi-step games (blackjack, pvp-dice) see one entry per step; single-shot games see exactly one. raw.randomness is now populated with the first fulfilled word.
  • Added optional HostApiV1.getRandomnessVerification({ sessionId }) — the host reads the ECVRF fulfillment artifacts from the Verify Network router and verifies them client-side (proof, output, EIP-712 enclave signature, signer identity). Returns per-request verdicts plus the raw artifacts so the result can be re-verified independently. See RANDOMNESS_VERIFICATION.md for the full contract and rendering rules.
  • Additive and optional: feature-detect host.getRandomnessVerification before calling; older hosts also omit raw.randomnessRequests. On environments without Verify Network (local dev) the method resolves with supported: false.
  • Added optional HostSnapshotV1.ui.viewport.availableHeight — the height in px from the top of the game’s iframe to the bottom edge of the visible screen, before the user scrolls. Games that want their primary action (e.g. the bet button) to land exactly at the screen edge should size the content above it to availableHeight minus the action’s own height. The value is measured against the small viewport (svh semantics), so it stays stable while scrolling collapses mobile browser chrome and only changes on real resizes or orientation changes.
  • CSS viewport units cannot replace this value: the host grows the iframe to fit the game’s reported content height, so inside the iframe 100vh equals the full content height, not the visible screen.
  • Additive and optional: games running against older hosts simply receive no ui.viewport and should fall back to their existing layout.
  • Added optional HostSnapshotV1.token.iconUrl — an absolute URL of the wager token’s icon so games can render it next to balances and wagers.
  • Additive and optional: games running against older hosts simply receive no iconUrl and should fall back to a text symbol.
  • Added optional HostApiV1.reportContentSize({ minHeight }) so casino iframe games can report their current required content height to the host after the bridge connects.
  • Added reportGameContentSize(hostApi) and observeGameContentSize(hostApi) helpers in @chain/casino-sdk/guest. Games can use the observer to report initial height and subsequent layout changes without wiring their own ResizeObserver.
  • Removed static presentation.minHeight from casino game manifest validation and examples. Height is now a runtime concern reported by the child iframe when capabilities.resize is supported.
  • Dynamic sizing is additive: older games that do not call reportContentSize continue to render using the host’s existing frame fallback.
  • reportContentSize is optional on the host API so games can safely run against older hosts by checking for the method or using observeGameContentSize, which no-ops when unsupported.
  • HostApiV1.revealOutcome({ sessionId }) (required) — games must call this when their result presentation has finished. Sessions settle on-chain long before a result animation completes (bonus buys can run past a minute), so from the moment a bet is placed until the game reveals the outcome, the Chain.wtf host clamps its balance displays (navbar, Smart Wallet modal, and the balances.smartWalletBalance pushed in the snapshot): the balance may move down (wager debit, extra escrow pulls) but never up. Guarding from bet placement — rather than subtracting the payout once it is known — means even a settlement that lands on-chain within a block can never flash the result. This is the reverse of an optimistic update: the payout is final, only its display is delayed. The guard exists only while the game is mounted — refreshing the page or navigating away from the game shows the real balance immediately.
  • Only credits are withheld: the wager debit still shows the moment a session opens, and a lost session moves no balance at settlement (the host releases the guard itself on a lost round), so games only need to reveal after wins — calling it after losses is a harmless no-op.
  • revealOutcome is a mandatory part of the game contract: a game that never calls it after a win leaves the host’s displayed balance stale until the player leaves or reloads the game page. Update existing games to call hostApi.revealOutcome({ sessionId }) at the end of their win presentation.
  • CasinoGameFacet events now carry opaque game bytes, so hosts can populate HostSnapshotV1.sessions.items[].raw from the session indexer without supplementary on-chain reads:
    • CasinoSessionOpened — added bytes gameData (the wager-time payload passed to openSession).
    • CasinoSessionPhaseAdvanced — added bytes gameState (post-step opaque state after each facet transition, including the first step after open).
    • CasinoSessionSettled — added bytes gameState (final opaque state at settlement).
  • Documented that the casino_sessions projection ingests these fields and exposes them on each session row; the Chain.wtf host maps them to raw.gameData / raw.gameState in the Penpal snapshot pushed via guestApi.setState.
  • Host snapshot freshness: integrators should treat indexer session rows as the source of truth for raw.gameData and raw.gameState. On-chain getSession(sessionId) is optional for recovery or debugging — it is no longer required on every phase advance to keep the iframe in sync.
  • The new event fields are additive. Indexers and hosts that already merge projection rows into HostSnapshotV1 gain automatic opaque-state updates; games that decode raw.gameState from the snapshot require no bridge API changes.