# Changelog

Versions use `YYYY.MM.DD-N`, where `N` increments when multiple SDK changes ship on the same day.

## 2026.09.17-2

### Added

* **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.

### Compatibility

* 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.

## 2026.09.17-1

### Changed

* **`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`. chUSD has 18 decimals in
  production and locally.
* **`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`).

### Compatibility

* 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.

### Fixed

* `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.

## 2026.09.16-4

### Changed

* 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`.

### Compatibility

* 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.

## 2026.09.16-3

### Added

* **`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`.

### Fixed

* **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.

## 2026.09.16-2

### Added

* **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.

### Changed

* 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'`.

### Compatibility

* `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.

## 2026.09.16-1

### Added

* **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.

### Changed

* `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.

### Compatibility

* 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.

## 2026.09.15-1

### Changed

* **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`.

### Compatibility

* 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.

## 2026.09.14-1

### Added

* **`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.

### Removed

* `PointDuelGame.sol` and the `examples/point-duel` guest.

## 2026.09.11-1

### Added

* 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.

### Compatibility

* Additive and optional: older hosts omit both fields, and games must treat absence as unknown.

## 2026.09.03-1

### Added

* 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'`.

### Compatibility

* 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`.

## 2026.08.31-1

### Added

* 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.

### Compatibility

* 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.

## 2026.08.30-1

### Added

* 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.

### Compatibility

* Additive and optional: feature-detect `host.claimPayout` before calling — older hosts omit it,
  and manifests without `capabilities.claimPayout` remain valid.

## 2026.08.20-1

### Changed

* **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? }`.

## 2026.07.26-1

### Added

* 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`.

### Compatibility

* 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.

## 2026.07.20-1

### Added

* 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.

### Compatibility

* 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`.

## 2026.07.19-1

### Added

* 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.

### Compatibility

* Additive and optional: games running against older hosts simply receive no `ui.viewport` and
  should fall back to their existing layout.

## 2026.07.18-1

### Added

* 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.

### Compatibility

* Additive and optional: games running against older hosts simply receive no `iconUrl` and should
  fall back to a text symbol.

## 2026.07.10-1

### Breaking

* 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 vault identity from PvP types; the submitting address is the participant.
* Replaced `IPvpGameV1` with `IPvpGameV2` and added a scheduled weighted Jackpot reference game.

## 2026.07.03-1

### Added

* 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`.

### Changed

* 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.

### Compatibility

* 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.

## 2026.06.28-1

### Added

* 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.

### Compatibility

* The metadata fields are optional and backward-compatible with existing hosts and games.
