Skip to content

Changelog

Follow PvP SDK releases and the contract, simulator, and bridge changes that affect your game. Check breaking changes before updating your integration.

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.

  • A PvP game’s developer, or the wallet that sent its creation call, may choose the game’s rake once with setPvpDeveloperRake(game, rakeBps), from 100 to 500 bps, within 60 seconds of its registration. The council’s per-game figure still overrides it, and the protocol-wide default of 500 bps applies without either. A successful call emits PvpDeveloperRakeSet(game, rakeBps), and getPvpDeveloperRake(game) returns (set, rakeBps). LocalPvpHost applies the same rules, reverting with LocalPvpHost__NotDeveloperRakeSetter, LocalPvpHost__DeveloperRakeWindowClosed, LocalPvpHost__DeveloperRakeAlreadySet or LocalPvpHost__InvalidDeveloperRakeBps.
  • The local node’s createGame(initCode, { developer, rakeBps }) sends rakeBps as the developer rake right after the creation call. Breaking for callers that passed developer as the second argument.

Released in lockstep with @chain-protocol/casino-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.
  • defineDeployment takes an optional frontend: 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.
  • The player’s contract is called the Smart Vault again. The docs and the simulator’s host dependencies use the new name: getSmartWalletBalance is now getSmartVaultBalance.
  • Breaking: the PvP bytecode deployer’s creation call names the game developer, as createGame(initCode, salt, developer, authorshipId), in the protocol’s GameBytecodeDeployer and the simulator’s LocalGameBytecodeDeployer. Anyone may name any developer, since the name only directs the game dev fee. An authorship is an optional way for a developer’s wallet to authorize a specific creator wallet; a call that consumes one must name the authorship’s writer, or LocalPvpHost reverts with Authorship__DeveloperMismatch. A zero developer reverts with LocalPvpHost__ZeroDeveloper. GameRegistered is unchanged. The simulator still creates every game from dev account #0 and names that account as its developer.

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 0.6.0-canary.1 below. Released in lockstep with @chain-protocol/casino-sdk 0.6.0.

No changes. Released in lockstep with @chain-protocol/casino-sdk 0.6.0-canary.3.

No changes. Released in lockstep with @chain-protocol/casino-sdk 0.6.0-canary.2.

  • Breaking: a game gets one random value per request. A step reached from onRandomness may not move to WAITING_RANDOMNESS. The PvP facet reverts the fulfilment with PvpGameFacet__RandomnessRequestedFromCallback. Derive further values from the one you received.
  • startLobby and submitLobbyAction 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. LocalPvpHost starts with an empty router balance, the simulator page attaches the host’s requiredPayment to every start and move, and a fulfilment whose step requests randomness again reverts with LocalPvpHost__RandomnessRequestedFromCallback.
  • Breaking: runPvpProperties from @chain-protocol/pvp-sdk/conformance fails hooksDoNotRevert when a lobby has one actor as its only exit. It plays the started lobby with each seated player silent, and opens the started config again with a never-acting address where the opener’s address was. Someone else must still be able to move or cancel the lobby, after waiting out about a day. A game whose start, cancel or every move belongs to one opener-chosen address now fails game-verify.
  • Breaking: PvpLobbyOpened no longer carries string gameName, in production and in LocalPvpHost. The chain keeps no game names, so its topic changed. Read a lobby’s game by its game address.
  • 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; omitted Host declarations derive support from the supplied handler, while explicit empty/false declarations disable it. 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: a game gets one random value per request. A step reached from onRandomness may not move to WAITING_RANDOMNESS. The PvP facet reverts the fulfilment with PvpGameFacet__RandomnessRequestedFromCallback. Derive further values from the one you received.
  • startLobby and submitLobbyAction 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. LocalPvpHost starts with an empty router balance, the simulator page attaches the host’s requiredPayment to every start and move, and a fulfilment whose step requests randomness again reverts with LocalPvpHost__RandomnessRequestedFromCallback.
  • Breaking: runPvpProperties from @chain-protocol/pvp-sdk/conformance fails hooksDoNotRevert when a lobby has one actor as its only exit. It plays the started lobby with each seated player silent, and opens the started config again with a never-acting address where the opener’s address was. Someone else must still be able to move or cancel the lobby, after waiting out about a day. A game whose start, cancel or every move belongs to one opener-chosen address now fails game-verify.
  • Guest panels. A game can declare panels the host may open from its toolbar: connectGameToHost(methods, { capabilities: { panels: [...] } }) with an openPanel({ panel }) method. PvP panels are info and history. 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. PvpHostSnapshot.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/pvp-sdk. It was @chain/pvp-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: 2 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 IPvpGameV2, keep theirs.

    BeforeAfter
    PvpGameManifestV2PvpGameManifest
    PvpHostSnapshotV2PvpHostSnapshot
    PvpHostApiV2PvpHostApi
    PvpGuestApiV2PvpGuestApi
    PvpViewerAccessV2PvpViewerAccess
    PvpHostMetadataV2PvpHostMetadata
    PvpLobbyMetadataV2PvpLobbyMetadata
    PvpPlayerMetadataV2PvpPlayerMetadata
    PvpRoomMetadataV2PvpRoomMetadata
    PvpRoomParticipantMetadataV2PvpRoomParticipantMetadata
    RandomnessRequestV2RandomnessRequest
    RandomnessRequestEchoV2RandomnessRequestEcho
    RandomnessRequestVerificationV2RandomnessRequestVerification
    RandomnessVerificationV2RandomnessVerification
    VrfVerificationChecksV2VrfVerificationChecks
  • Breaking: IPvpGameV2.sol and PvpLiveness.sol moved from simulator/contracts/ to contracts/. Import them as @chain-protocol/pvp-sdk/contracts/IPvpGameV2.sol and .../PvpLiveness.sol.

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

  • 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/pvp-sdk registry item. Install @chain-protocol/pvp-sdk from npm.
  • Replace @chain/pvp-sdk with @chain-protocol/pvp-sdk in your imports and package.json.
  • Import IPvpGameV2.sol and PvpLiveness.sol from @chain-protocol/pvp-sdk/contracts/ in Hardhat, or remap @chain-protocol/=node_modules/@chain-protocol/ in Foundry.
  • Rename the types in the table above.
  • 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.
  • The simulator’s chUSD has six decimals, like USDC on mainnet. The local node funds each account and sets the gas charge in six-decimal amounts, and LocalPvpHost defaults to a 0.01 chUSD gas charge at six decimals.
  • The Bones and RPS examples fall back to USDC and six decimals when the lobby’s asset carries no symbol or decimals.
  • Format and parse amounts with the asset’s decimals. A game that assumes 18 decimals shows amounts a trillion times too small.
  • The player’s contract is called the Smart Wallet. The docs and the simulator’s host dependencies use the new name: getVaultBalance is now getSmartWalletBalance.
  • No game-facing API change.
  • Entries, the creation with an entry and the cancel follow the rake reserve. The chain.wtf host and the simulator carry each as a sponsored call when the lobby’s rake reserve, after the call’s own stake, can pay its gas charge, and as a covered call otherwise. The relayer’s sponsored minimum no longer decides them; it still decides claims of winnings and deferred payouts. See §6.3 of CHAIN_WTF_PVP_GAMES.md.
  • A refund claim is always a covered call, repaid from the refund itself, so it goes through from an empty balance. The host shows the gas line item.
  • Cancel and refund in one call. A player whose balance cannot cover a covered cancel, and who has a stake in the lobby, cancels and claims their own refund in one covered call. cancelLobby resolves as before; lobby.viewer.refunded is then true, so read it before offering that player a refund claim.
  • No API change. A game that already handles PVP_CALL_UNAFFORDABLE and reads lobby.viewer.refunded needs no code.
  • Rake reserve figures on the snapshot. LobbySnapshot gains optional reserveRemaining (what the lobby’s rake reserve, the part of the rake the gas tank does not hold, can still pay for its entries and cancel; present while the lobby is open or in play) and reserveSpent (what it has paid for the open-and-enter, entries and cancel), both in the asset’s base units. LobbySettlement gains optional reserveSpent beside tankSpent: the fee recipients split feeAmount - tankSpent - reserveSpent, and winners still share pot - feeAmount. See §4 and §6.5 of CHAIN_WTF_PVP_GAMES.md.
  • lobbyNetRefund(lobby, stake). What claimRefund pays a participant of a cancelled lobby: the stake less its share, by stake, of tankSpent plus reserveSpent, rounded up against the participant and never below zero. The Chain Bones example shows it on its refunds to collect.
  • The simulator mirrors the rake reserve. LocalPvpHost repays the sender of openAndEnterLobby, enterLobby and cancelLobby from the reserve at the simulator’s gas charge, in full or not at all, emits PvpLobbyReservePaid, refunds net of tank and reserve spend, and splits the rake less both at resolution. getLobbyTank ends with reserveRemaining and reserveSpent, like the diamond’s lobby view. See “Rake reserve” in LOCAL_SIMULATOR.md.
  • PvpLobbyReservePaid(lobbyId, payee, gasUsed, gasCharge, reserveRemaining) reports each reserve payment, after the paid call’s own entry or cancellation in the same transaction. PvpLobbyResolved is unchanged and still ends with tankSpent only; read reserve spend from the payment events or the lobby view.
  • A cancelled lobby’s refunds are smaller than before by each participant’s share of what the reserve spent, on chain.wtf and in the simulator.
  • Additive for guests: the snapshot fields are optional and a guest that ignores them reads the same snapshot as before. Hosts that predate them, and lobbies without a gas tank, omit them.
  • Anything decoding getLobbyTank with its own ABI must take the two trailing fields.
  • PVP_CALL_UNAFFORDABLE. The message prefix of the error a write method rejects with when the call would be a covered call and the player’s balance cannot pay the stake plus the cover limit. Nothing is sent; the game keeps its state and may retry. Exported from @chain/pvp-sdk; the chain.wtf host reports the same code. See §6.3 of CHAIN_WTF_PVP_GAMES.md.
  • The simulator mirrors the gas tank. LocalPvpHost has the per-game rake (setPvpGameRake / clearPvpGameRake / getPvpGameRake), the tank share (setPvpTankShare, 3500 bps by default), the lobby gas tank that pays a participant’s moves in full or not at all and credits the move’s sender (getClaimable, claim), refunds net of tank spend (getLobbyRefund), the relayer’s lobby view (getLobbyTank) and the production event shapes. The simulator’s host applies the relayer’s policy in process: moves are sponsored while the tank can pay their charge, entries at or above the sponsored minimum are sponsored, everything else is a covered call charged to the tab’s balance and shown as a gas line item over the frame, and an unaffordable call is refused with PVP_CALL_UNAFFORDABLE. The snapshot now carries tankRemaining / tankSpent and settlement.tankSpent. The rake, tank share, gas charge and sponsored minimum are adjustable from the setup panel and the local node’s environment; see LOCAL_SIMULATOR.md.
  • LocalPvpHost events follow production. PvpLobbyOpened ends with uint16 tankShareBps, PvpLobbyResolved ends with uint256 tankSpent and no longer carries feeRecipient, and PvpLobbyTankPaid reports each tank payment. Anything decoding the local host’s logs with its own ABI must take the new signatures.
  • The local node deploys the host with the protocol’s default rake of 500 bps (was 250) and prices every call at a gas charge of 0.01 chUSD.
  • The SDK docs describe cancellation refunds as net of tank spend, which they have been since the tank shipped.
  • Additive for guests: the error prefix is new, the snapshot fields are the optional ones from 2026.09.30-1, and a game that ignores them behaves as before.
  • Creation with a first entry. PvpHostApiV2.createLobby accepts an optional entry: { stake, entryData? }. With it the host opens the lobby and makes the creator’s entry in one transaction (the diamond’s openAndEnterLobby) and resolves with contributionId beside lobbyId. The entry obeys the same rules as enterLobby (game policy, invitee list); a rejected entry fails the whole call and leaves no lobby open. See §6.1 of CHAIN_WTF_PVP_GAMES.md.
  • The bundled examples (examples/bones, examples/rps) create and enter in one call; the simulator’s LocalPvpHost gained the same entry point.
  • Creating without an entry is a covered call. The chain.wtf host relays a creation with an entry as it relays an entry (sponsored when the stake reaches the PvP sponsored minimum, else a covered call with the gas line item), and a creation without one always as a covered call paid by the creator’s Smart Wallet. Games whose creator plays should pass entry; a creation-then-enter pair keeps working but costs the creator its gas.
  • Additive: entry is optional and contributionId is only present when it was passed. Guests that do not pass entry see the same result shape as before.
  • Gas tank figures on the snapshot. LobbySnapshot gains optional tankRemaining (what the lobby’s gas tank can still pay for participants’ moves; present while the lobby is in play) and tankSpent (what it has paid so far), both in the asset’s base units. LobbySettlement gains optional tankSpent beside feeAmount, which stays the gross rake: winners share pot - feeAmount, the fee recipients split feeAmount - tankSpent. See §4 and §6.5 of CHAIN_WTF_PVP_GAMES.md.
  • The figures are informational. The relayer decides whether a move is sponsored; never gate a move on tankRemaining.
  • Two lobby events carry the tank. PvpLobbyOpened ends with a new uint16 tankShareBps (the share of the rake that funds the lobby’s tank, fixed when the lobby opens) and PvpLobbyResolved ends with a new uint256 tankSpent. Their topic hashes change; every earlier field keeps its position. PvpLobbyTankPaid(lobbyId, payee, gasUsed, gasCharge, tankRemaining) reports each tank payment.
  • Guests need no change: the three fields are optional and additive to snapshot apiVersion: 2, and a guest that ignores them reads the same snapshot as before. Hosts that predate them, and lobbies without a tank, omit them.
  • The chain.wtf host sends the fields.
  • Anything that decodes diamond logs with its own copy of the ABI must take the two new event signatures.
  • Operators (not open yet). PvpGameFacet gained openLobby(game, token, config, openData, invitees, operator), which attributes a room to the frontend that opened it; that operator earns its share of the lobby’s rake. It also emits PvpLobbyOperatorAttributed(lobbyId, operator) and exposes getLobbyOperator(lobbyId). Operator registration is closed for now: every lobby resolves to the protocol’s default operator, and a missing or unregistered code never reverts. See §9.1 of CHAIN_WTF_PVP_GAMES.md.
  • The simulator’s LocalPvpHost mirrors the overload, event and view. It pays the room operator operatorRakeShareBps (default 5%) of the rake, with permissionless setOperator, setDefaultOperator and setOperatorRakeShareBps stand-ins for council configuration.
  • Games and guests need no change: the five-argument openLobby still works, the operator never reaches LobbyContext or the iframe, and the distributable pot is unchanged.
  • lobby.defaultStake in the manifest is now a whole-token decimal string ("1", "0.1") rather than base units, so one manifest serves every deployment regardless of the asset’s decimals. Games convert it with the live asset’s decimals (parseUnits(defaultStake, asset.decimals)); the RPS example does this in state/stake.ts.
  • getRandomnessVerification follows the current Verify Network router and matches the casino SDK’s verification. The router 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), and the enclave signature (EIP-712 domain version 2). The signing key is recovered from that signature and the ECVRF proof is checked under it; no router view is read. The chain.wtf host now implements the method for PvP lobbies, and the simulator runs the same checks against its bundled router.
  • checks are vrfProofValid, vrfBetaMatchesRandomness, enclaveSignatureValid, signerMatchesFulfiller; valid requires all four. New request field: the echoed router request (consumer, sequence, fulfiller, assignedAt, fee, gasPrice, callbackGasLimit, callbackData).
  • Manifests written with base-unit defaultStake values must be converted to whole tokens ("1000000000000000000" → "1"); a leftover base-unit string would now prefill an enormous stake. The RPS and Bones example manifests are updated.
  • Removed from verification results: the fastVerifyComponentsMatch check and the artifacts.uPoint / artifacts.vComponents hints. Verify views that render checks by name should drop that row; valid keeps its meaning.
  • npm install at the SDK root no longer fails with EBADDEVENGINES: package.json declares npm as the dev package manager, matching the documented install path.
  • npm run local-node crashed with verifyNetwork.depositLocalVrfClientBalance is not a function. The bundled local-verify-network renamed that helper to depositLocalVrfBalance (with an account argument) and the simulator now calls it by its current name. The local-node module type is derived from the bundled package’s real exports, so a future rename fails npm run check-types in simulator/ instead of surfacing at runtime.
  • Every randomness step in the simulator reverted with 0x87c9fc34 (Proxy__ImplementationIsNotContract() from the bundled router). LocalPvpHost.sol still called the previous router’s requestRandomness() and exposed a two-argument onRandomnessFulfilled. It now calls requestRandomness(bytes callbackData) with empty data and implements the three-argument callback, matching the production PvP randomness adapter and LocalCasinoHost. src/local-node/artifacts.ts is regenerated.
  • The Solidity sources moved from solidity/ to simulator/contracts/: IPvpGameV2.sol and PvpLiveness.sol sit next to the harness contracts, the reference games in simulator/contracts/examples/. Drop-in games import the interface as ./IPvpGameV2.sol.
  • Games importing ../../solidity/IPvpGameV2.sol or ../../solidity/PvpLiveness.sol keep compiling in the simulator. Any import path ending in either file name resolves to the shared file. Contracts are unaffected.
  • simulator/ room browser. A Room browser tab above the frame renders production’s PvP landing page from the local host: every game’s lobbies railed as Open lobbies / Matches in progress / Recently archived with the production card anatomy and info popover, fed by the same lagged indexed feed the game reads. Clicking a card opens that lobby on the game page (switching the harness to the lobby’s game contract when it differs); ?view=rooms keeps the browser open across reloads. Titles and covers come from the running game’s manifest (assets.coverUrl), other games fall back to their registered name. See LOCAL_SIMULATOR.md.
  • Rock Paper Scissors waiting room offers the open seat to a viewer the host routed into a filling lobby (the lobby route, a room browser card, the simulator’s Join) — previously only the example’s own lobby list could join, so a routed viewer saw “Waiting for opponent” with no way in. Joining now goes through one joinLobby action that starts the match when the join fills the lobby, replacing the waiting room’s last-entrant auto-start; a full lobby nobody started shows a Start the match fallback.
  • Spectator mode (optional). A viewer can open any lobby without a wallet and watch it. The snapshot gains access — { mode: 'play' } or { mode: 'spectate', reason, canRequestPlayAccess } with reason one of disconnected, setup-required, session-key-mismatch, chain-mismatch, host-policy — read through getViewerAccess(snapshot), which derives it from wallet.status on older hosts. While spectating the host rejects every write with PVP_SPECTATOR_READ_ONLY (isSpectatorReadOnlyError) and, when it can lead the viewer to play, exposes the optional hostApi.requestPlayAccess() that opens its sign-in or wallet-setup flow. Games declare capabilities.spectate: true in the manifest; the Chain.wtf host shows a sign-in placeholder instead of the game to spectators of a game that does not. See CHAIN_WTF_PVP_GAMES.md §8.
  • Bones and Rock Paper Scissors implement it: a live-matches list with Watch, a recent-matches list with Replay that opens a finished lobby cold on its final board and result, seats named by position instead of “you”, no write controls, no automatic reveal / claim / start as a watcher, a Sign in to play hand-off, and a result screen that names the winning seat. Both follow metadata.room.roomId into any phase, so the host’s Watch and Replay actions open the lobby.
  • simulator/ sends access, rejects writes while spectating, replicates the production placeholder, and gains a Spectate by host policy toggle next to the wallet status override; the sign-in hand-off flips the tab’s wallet status to ready. The match panel gains a Past games tab with a Replay action for every finished lobby of the game, and reopening the lobby the tab already has open reloads the frame instead of doing nothing.
  • A write attempted while the viewer cannot play now rejects with PVP_SPECTATOR_READ_ONLY instead of PVP_WALLET_NOT_READY; the latter remains for a play viewer whose relay is not ready.
  • The Chain.wtf host no longer errors out for a wallet on another chain; it spectates with reason: 'chain-mismatch'.
  • access, requestPlayAccess and capabilities.spectate are all optional. Games that ignore them behave as before for players; add capabilities.spectate only once the game renders without a wallet, because the host hides the iframe from spectators otherwise.
  • Liveness standard (optional). solidity/PvpLiveness.sol defines IPvpLiveness (ERC-165 id 0xae5c86f9), a liveness(phase, config, gameState) view returning the lobby’s pending waits plus a capability bitmask, and four reserved actionData payloads (abi.encodePacked(tag, bytes32 subject)): SKIP 0x021033a8, FORFEIT 0xe04af859, RESIGN 0x1fbe2408, ADVANCE 0x46c0809d. A wait says who owes a step, until when it can be skipped and when it costs them the match, so turn-based games publish one wait and simultaneous games publish one per player. The abstract PvpLiveness base handles detection, phase gating, payload parsing and wait resolution; @chain/pvp-sdk/liveness exports the tags, ABI, encodeLivenessAction and getDueWaits. See PVP_CONTRACT_CONSTRAINTS.md.
  • solidity/examples/RockPaperScissorsGame.sol — the simultaneous reference game: fixed stake, commit and reveal windows of phaseSeconds each, first to winsNeeded rounds takes the pot. It has no skip path; a player who misses a window while the other has acted can be forfeited immediately, and a window both players miss ends as a no-contest that each seat claims half of. The local backend deploys and registers it.
  • examples/rps/ — a guest UI for it, the SDK’s first example of a commit–reveal game. The sealed pick lives in browser storage and is written and read back BEFORE the commit is sent, since a commitment whose preimage was never stored is unopenable and loses the round silently. The guest reveals automatically, because the reveal window makes a missed click cost the match, and mirrors the contract’s _waitsOf so it can offer the right liveness control without an RPC: a forfeit only ever against the opponent, a no-contest only when both seats missed. Served on :3500.
  • Bones forfeit and resign. BonesGame implements the standard: FORFEIT two turn lengths after the deadline and RESIGN from either seat hand the whole pot to the other seat, whatever the score. The guest shows a countdown, a Claim the win button and a Resign option, and the result screen marks conceded matches.
  • BonesGame SKIP moved from abi.encode(uint8(2), uint8(0)) to the reserved liveness payload; lastMove.kind is now 0 = ROLL, 1 = PLACE, 2 = FORFEIT, 3 = RESIGN.
  • Games that do not implement the standard are unaffected. Redeploy BonesGame (the simulator picks the new bytecode up on restart); older Bones guests cannot skip on the new contract.
  • Flashblock push layer on the host. The Chain.wtf host now overlays flashblock-pushed lobby events (open, entries, phase advances, randomness, resolve, payouts, claims, refunds — from every participant) onto the indexed lobby feed, so lobbies.items, getLobbyParticipants and getLobbyContributions reflect a move roughly a block before the indexed feed delivers it. createLobby and enterLobby resolve from the pushed log or the receipt, whichever lands first. The layer is best-effort: without it the host behaves exactly as before.
  • simulator/ gains the same two-layer timing: a flashblock channel next to the indexed feed, a live Flashblock lag slider beside the indexer lag, and the same overlay of the lobby list, roster and ledger. See docs/LOCAL_SIMULATOR.md.
  • No bridge or type changes. A lobby can now appear in lobbies.items before the indexed feed has it, but games must keep tracking the lobbyId returned by createLobby until it appears there — the pushed log is an accelerator, not a guarantee.
  • simulator/ — a fully standalone local test setup, needing nothing outside the SDK: a Vite harness that mounts a game iframe through the production host bridge (strict manifest validation, the lagged indexed lobby feed, per-lobby room routing, browser-side randomness verification) and a one-command local backend (in-memory Hardhat node, LocalPvpHost — a minimal PvpGameFacet stand-in emitting byte-identical events — the reference BonesGame and JackpotGame, and a real Verify Network VRF node). Each browser tab plays as its own dev wallet, so a duel is two tabs of the same page. See docs/LOCAL_SIMULATOR.md.
  • solidity/examples/BonesGame.sol — Chain Bones (Knucklebones) replaces PointDuelGame as the fixed-stake reference game: 1v1, per-roll VRF, turn deadlines with a permissionless SKIP, immediate winner payout and a claimable 50/50 draw split.
  • examples/bones/ — the Chain Bones guest UI (create, join + start, roll, place, skip, claim, refund, rematch) with its deterministic rules engine and tests, served on :3200 and wired into npm start at the SDK root.
  • PointDuelGame.sol and the examples/point-duel guest.
  • Added optional cancellation-refund state to LobbyParticipant:

    • refunded — true once the address has collected its cancelled-lobby refund.
    • refundAmount — amount already returned to the address.

    This lets a returning player recover a refund whose first transaction failed after cancellation, without attempting to cancel the lobby again.

  • Additive and optional: older hosts omit both fields, and games must treat absence as unknown.
  • Added optional invitees to the PvpHostApiV2.createLobby input. A non-empty list makes the lobby invite-only: the protocol rejects enterLobby from any address not on the list, before the game’s own admission policy runs (AND semantics). The list is immutable after creation, bounded at 64 addresses, and the creator is not auto-invited. Claims, refunds, and cancellation are untouched — invitations decide who can put money in, never who can take theirs out. Omit or pass empty for a public lobby (exactly the previous behavior).
  • Added optional invitees to LobbySnapshot — present only on invite-only lobbies; absent on public lobbies and on older hosts. Guests derive “am I invited” by comparing against snapshot.wallet.address.
  • No manifest schema change: a game whose UI creates invite-only lobbies should declare lobby.access: 'game-defined'.
  • The createLobby input change is not additive-optional for hosts (the underlying openLobby gained a parameter), but it ships before the first PvP V2 deployment. Games running against older hosts must omit invitees.
  • Added per-player claim state to LobbyParticipant (so it appears on lobby.viewer and on rows from getLobbyParticipants):

    • claimed — true once the address has collected at least one claimable-settlement claim.
    • claimedAmount — sum of claims already paid to the address.
    • claimedClaimIds — the claim ids already collected; check the specific id when a game issues several claims per player. Claim id semantics are game-defined.

    This lets a returning player’s game skip a claimWinnings transaction it already sent — the settlement alone never said whether the share was collected.

  • Additive and optional: older hosts omit all three fields, and an absent field means “unknown”, not “unclaimed” — a resent claim is safe (it reverts as already collected) but wasted.
  • Added optional PvpHostApiV2.claimPayout({ lobbyId }) so games can offer collection of an immediate payout whose settlement-time transfer failed (settlement.immediate.deferred). The call is permissionless — it always pays the winner recorded at resolution — and resolves with { winner, amount, transactionHash }.
  • Added optional manifest capability capabilities.claimPayout declaring that the game renders its own collect control for parked payouts; hosts may keep their own fallback control when the flag is absent or false.
  • Additive and optional: feature-detect host.claimPayout before calling — older hosts omit it, and manifests without capabilities.claimPayout remain valid.
  • Breaking (contract interface): PvpStepResult now returns address[] recipients on resolution and no longer declares a settlement mode; the protocol derives it from the length. Exactly one recipient settles IMMEDIATE — that address receives the whole distributable pot. Zero (not enumerable) or several recipients settle CLAIMABLE via getClaim. The old ImmediatePayout amounts arrays and MAX_IMMEDIATE_RECIPIENTS are removed (Point Duel now settles via per-participant claims, or immediately when a single player scores).
  • The winner transfer no longer reverts resolution on failure: the payout is parked on the lobby and collected via the new permissionless claimPayout(lobbyId); the facet emits PvpLobbyPayoutDeferred when this happens.
  • ImmediatePayoutSnapshot is now { winner, amount, deferred? }.
  • Added transaction hashes to lobby snapshots so games can render real block-explorer /tx/ links instead of falling back to the game contract’s address page:
    • raw.createTransactionHash — the transaction that created the lobby.
    • raw.resolveTransactionHash — the transaction that resolved the lobby; absent until resolved.
    • 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 lobbies 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 lobby snapshots — every VRF request of the lobby in request order ({ nonce, requestId, randomness?, fulfilled }). Per-throw games see one entry per throw; raw.randomness / raw.requestId keep reflecting only the latest request.
  • Added optional PvpHostApiV2.getRandomnessVerification({ lobbyId }) — 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 PvpHostSnapshotV2.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 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 iconUrl to PvpAssetBalance and LobbySnapshot.asset — an absolute URL of the token’s icon so games can render it next to balances and stakes.
  • Additive and optional: games running against older hosts simply receive no iconUrl and should fall back to a text symbol.
  • Replaced API v1’s fixed buy-in, unique seat, creator privilege, uint8 player cap, and full players[] snapshots with the game-policy-oriented API v2 model.
  • Added explicit game-validated entries (including zero and repeated stakes), game-defined positions, optional lobby keys, a protocol-owned contribution ledger, and paginated reads.
  • Added immediate settlement for at most ten recipients and game-computed claimable settlement.
  • Removed the separate Smart Wallet field from PvP types; the submitting address is the participant.
  • Replaced IPvpGameV1 with IPvpGameV2 and added a scheduled weighted Jackpot reference game.
  • Added optional PvpHostApiV1.reportContentSize({ minHeight }) so PvP 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/pvp-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 PvP 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.
  • Added host-to-game metadata support to PvpHostSnapshotV1.
  • Added PvpHostSnapshotV1.metadata for parent-app context shared with the iframe, including:
    • viewer profile metadata for the connected viewer.
    • room metadata for room id, slug, title, invite URL, and participants/spectators.
    • custom JSON-serializable host data for presentation-only features.
  • Added LobbySnapshot.metadata for lobby/table labels, invite URLs, and custom presentation data.
  • Added LobbyPlayer.metadata for seated-player profile data such as userId, username, displayName, avatarUrl, and profileUrl.
  • Added JSON-safe metadata helper types:
    • PvpMetadataPrimitive
    • PvpMetadataValue
    • PvpMetadataBag
  • Added structured metadata types:
    • PvpPlayerMetadataV1
    • PvpRoomParticipantMetadataV1
    • PvpRoomMetadataV1
    • PvpHostMetadataV1
    • PvpLobbyMetadataV1
  • Re-exported the metadata types from @chain/pvp-sdk/host and @chain/pvp-sdk/guest, so host and game iframe code can import them from the bridge entrypoints.
  • Documented common metadata that host apps may pass to games: usernames, display names, avatars, profile URLs, room titles, room/invite links, participants, spectators, lobby labels, and app-specific custom metadata.
  • Documented that host-provided metadata is display-only and must not be used for authoritative game rules, turn ownership, eligibility, scoring, payouts, or any contract-sensitive logic.
  • The metadata fields are optional and backward-compatible with existing hosts and games.