@chain-protocol/pvp-sdk
v0.7.0
Published
Build PvP games for the Chain.wtf host: the SDK bridge, the game manifest, the liveness and spectator helpers and the game interface in Solidity.
Readme
@chain-protocol/pvp-sdk
Bridge types and the canonical Solidity policy interface for wagered multiplayer games embedded in the Chain.wtf host application.
API v2 removes protocol-level assumptions that every participant pays the same buy-in, enters once, or fits in a bounded seat array. A lobby has one escrow asset, while its game decides admission, stake validation, repeat entries, positions, lifecycle permissions, lobby keys, and settlement mode.
Start a game
Create a game from the Bones example, install it and play it in the SDK simulator:
npx @chain-protocol/cli init pvp my-game --template bones
cd my-game
npm install
npm run dev # the game frontend on http://localhost:3200In a second terminal, start the SDK simulator:
npm run simulatorWith Bun, run bunx @chain-protocol/cli init pvp my-game --template bones, bun install,
bun run dev and bun run simulator. --template rps starts from Rock Paper Scissors instead.
Open the URL npm run simulator prints, in as many tabs as you want players. See
docs/LOCAL_SIMULATOR.md.
To add the SDK to an existing project, install it with the CLI at the same version:
npm install @chain-protocol/pvp-sdk
npm install --save-dev @chain-protocol/cliWith Bun: bun add @chain-protocol/pvp-sdk and bun add --dev @chain-protocol/cli.
Core model
- Anyone opens a lobby; the opener receives no implicit authority.
- The game validates the lobby config and may assign a game-scoped unique
lobbyKey. - An address enters with an explicit stake, including zero. The game may reject the amount or a
repeated entry and returns the entry's
positionId. - The protocol escrows accepted stakes and appends them to an indexed contribution ledger.
- The game controls start, actions, randomness transitions, and cancellation eligibility.
- Resolution derives settlement from the recipients the game returns: a single recipient is paid the whole distributable pot immediately; zero or several settle via claims. Cancellation makes every participant's stake refundable, net of their share of what the lobby's gas tank and rake reserve spent.
Unbounded participant and contribution lists are paginated. They are never embedded in lobby snapshots or passed wholesale to game hooks.
Subpaths
| Import | What it holds |
| ------------------------------------- | ----------------------------------------------------------------------------------- |
| @chain-protocol/pvp-sdk | Types, manifest validation, defineDeployment, hostSupports, liveness, spectator |
| @chain-protocol/pvp-sdk/guest | connectGameToHost, for the game frontend |
| @chain-protocol/pvp-sdk/liveness | Liveness standard tags, ABI and the due-wait helper |
| @chain-protocol/pvp-sdk/spectator | Spectator standard: viewer access and the read-only error |
| @chain-protocol/pvp-sdk/contracts/* | IPvpGameV2.sol and PvpLiveness.sol |
./host, ./simulator and ./conformance are internal. They serve the Chain.wtf host, the
chain dev pvp command of @chain-protocol/cli and game-verify, sit outside semver and can
change in any release. Do not import them from a game.
The package ships ESM with type declarations and runs on Node 22+ and Bun 1.4.2+. Your frontend bundle holds only the subpaths it imports.
Host capabilities and panels
Available from SDK 0.5.1.
Manifest capabilities gate actions and access before the game mounts. Connection capabilities describe guest-owned panels; they remain separate from manifest gates. presentation.hostPanels.history describes host-drawn history chrome, while declaring capabilities: { panels: ['history'] } opts into a guest-owned history panel. Panels stay out of manifests.
connectGameToHost accepts the existing guest methods and optional capabilities. The SDK injects getCapabilities through the guest connection and records its result as connection.capabilities on the host connection. Discovery runs once per SDK connection. Penpal 7 may reconnect after an iframe reload while retaining that connection, but capability discovery does not rerun on this reconnect; recreate the SDK connection to refresh capabilities. This does not resolve the separate reload/referrer/origin continuity follow-up. The SDK provides the typed remote proxy on each connection; proxy presence alone does not establish optional-operation support.
Panel IDs describe stable user intents, not layouts or components. PvP supports info, history.
- The host chooses toolbar affordances and owns toolbar UI, localization and wallet authority. The guest owns game surfaces and game-specific preferences, and respects authoritative host preferences such as snapshot-provided locale and theme. One guest dialog may serve multiple panel IDs.
- Missing or unknown capability data disables the corresponding control. The SDK handles a legacy
METHOD_NOT_FOUNDfrom discovery as an empty capability set; other discovery failures remain errors. openPanelshows or focuses the requested surface idempotently, never toggles it. It resolves when the request is accepted, not when the surface is dismissed. Reject unsupported panel requests; hosts should keep that control disabled and handle command errors.- General game settings are not a panel intent. Hosts ignore unrecognized panel IDs. PvP has no audio intent; casino audio remains game audio/settings.
Extend the existing guest implementation and pass capabilities as the second argument:
import { connectGameToHost, type PvpGuestApi } from '@chain-protocol/pvp-sdk/guest';
declare const existingGuest: PvpGuestApi;
declare function showOrFocusInfo(): void;
declare function showOrFocusHistory(): void;
connectGameToHost(
{
setState: existingGuest.setState.bind(existingGuest),
openPanel: async ({ panel }) => {
switch (panel) {
case 'info':
showOrFocusInfo();
return;
case 'history':
showOrFocusHistory();
return;
default:
throw new Error('Unsupported guest panel');
}
},
},
{ capabilities: { panels: ['info', 'history'] } },
);Logical guest windows
openWindow(target) shows or focuses one guest-owned logical surface and returns { status: 'accepted' | 'unsupported' | 'unavailable' }. The request is idempotent, never a toggle. accepted means the view was selected; it does not mean a randomness proof passed. Advertise exact targets under capabilities.windows: { protocol: 1, targets: [...] }; the SDK rejects a local declaration without a matching callable receiver, validates each request against that connection's targets, and keeps an unknown or malformed future window channel from erasing valid legacy panel discovery. A Penpal proxy's presence is not evidence of support. Call openWindow only after the connected guest advertises the exact target; the API is optional and legacy openPanel behavior is unchanged.
info/overview is the default Info view; info/rules, info/session, and info/verify name other views in that logical family. history and settings are separate windows. settings/audio explicitly means game audio controls; advertising settings alone does not promise audio. The guest may render several targets in one dialog. The legacy PvP openPanel contract still has only info and history; settings/audio is available only through the new exact window channel when a guest implements it.
import {
connectGameToHost,
WINDOW_TARGETS,
type PvpGuestApi,
type WindowTarget,
} from '@chain-protocol/pvp-sdk/guest';
declare const existingGuest: PvpGuestApi;
declare function showOrFocusGuestView(
target: WindowTarget,
): Promise<'accepted' | 'unsupported' | 'unavailable'>;
connectGameToHost(
{
setState: existingGuest.setState.bind(existingGuest),
async openWindow(target) {
return { status: await showOrFocusGuestView(target) };
},
},
{
capabilities: {
panels: ['info'], // Optional old-Host alias for Info Overview only.
windows: { protocol: 1, targets: [WINDOW_TARGETS.overview, WINDOW_TARGETS.verify] },
},
},
);Each connection may advertise at most 32 exact targets. A window or tab identifier starts with a lowercase letter and contains up to 48 lowercase letters, digits or hyphens ([a-z][a-z0-9-]{0,47}). Extension windows use x:provider/window: the provider follows the same syntax with a 32-character limit, and the window uses the 48-character limit; tabs keep the ordinary identifier syntax. The Host-owned window names chat, account, wallet, transaction and verification are reserved. These identifiers are opaque data, not routes, URLs or commands.
The application-owned receiver must recheck its current connection, account, and lobby context before acting and after asynchronous work; an advertised target may still return unavailable at request time. Keep proof data and verification authority in the existing trusted Host interfaces. Login, wallet and transaction flows continue through their dedicated controllers, including requestPlayAccess; fullscreen and microphone publication are separate explicit actions. The window request carries no URL, selector, route, RPC method name, permission grant, arbitrary parameters, or readiness claim.
The guest owns panel behavior. As an illustrative example only, a game could advertise supported settings (e.g., graphics quality levels), accept a typed change request and report the applied value to keep both UIs aligned. This carries no roadmap commitment; no graphics or direct-mute API is included.
Requesting play access
A game may ask the Host to present its existing login, vault setup or signing-key recovery UI. It grants no account access or transaction authority. On the connection returned by connectGameToHost, read await connection.capabilities; offer the new action only when playAccess === true. Missing discovery on an older Host resolves to {}; other discovery failures disable this affordance without blocking normal snapshots or gameplay RPCs.
const capabilities = await connection.capabilities;
const hostApi = await connection.promise;
// Call only from an explicit player action, after checking the game's access gates.
async function requestAccessFromClick() {
if (capabilities.playAccess !== true) return;
await hostApi.requestPlayAccess();
// Acknowledges UI presentation, not login success. Wait for authoritative snapshots.
}The Host opts in with connectHostToGame({ iframe, childOrigin, methods, capabilities: { playAccess: true } }), implementing PvpHostApi.requestPlayAccess. The SDK owns getCapabilities; do not implement that reserved RPC. An affirmative declaration without a callable implementation throws at connection creation. Capability discovery is per connection, not a readiness or permission signal.
The request takes no provider, account, route or transaction arguments and resolves once the current Host flow has opened/focused, or when the account is already ready. The Host must check its current player/game/deployment context, preserve unrelated modal and recovery ownership, reject an unavailable flow and coalesce or focus repeated UI requests. Concurrent pending requests share one invocation; destroying the connection rejects pending requests and stops queued ones. It cannot undo UI already opened or cancel an operation the player has separately authorized. Hosts must fence their own asynchronous continuations against context changes.
Keep betting disabled until a new Host snapshot permits it, and require a fresh bet action. Rejections and timeouts never imply readiness or prove that remote UI did not open; do not automatically retry. Use localized fallback guidance rather than parsing error text. The SDK adds no stable policy-error taxonomy, wallet kit, signing call, funding request or automatic bet. Existing Host entry points remain available on unsupported peers.
PvP already exposed requestPlayAccess before Host capability discovery. When a Host omits capabilities, the SDK derives the declaration from its supplied methods: a callable requestPlayAccess advertises { playAccess: true } and remains exposed; otherwise it advertises { playAccess: false } and is not exposed. This preserves old guests and lets new guests discover existing Host support. Explicit capabilities: {} or { playAccess: false } disables it. Discovery never overrides access.canRequestPlayAccess, spectator mode, lobby admission, chain restrictions or Host policy. Do not turn a policy-only spectator block into a wallet setup request.
Versions
The PvP SDK, the casino SDK and @chain-protocol/cli share one version. In 0.x a minor bump is
breaking and a patch is not. Breaking releases publish under the canary dist-tag first, and the
host reports its version in snapshot.sdkVersion; branch on it with hostSupports. See
SDK versions and breaking releases
and the changelog.
Docs
docs/CHAIN_WTF_PVP_GAMES.md: the integration guide.docs/PVP_CONTRACT_CONSTRAINTS.md: the protocol and game responsibility boundary, and the reference games.docs/LOCAL_SIMULATOR.md: whatchain dev pvpruns, including the multi-tab player-vs-player flow.
Work on the SDK in the Chain repository
In the monorepo this package is a workspace root. npm install then npm start runs the dev
chain, the VRF node, the PvP deployment, the simulator page on :3400 and the Bones and Rock Paper
Scissors examples on :3200 and :3500. See simulator/README.md.
