@snapshotsol/snap
v0.1.0
Published
Deterministic match-authority runtime for on-chain games. Modular engine (scoring, wagers, tokenization, VRF, provenance) with MagicBlock Ephemeral Rollup support.
Downloads
46
Maintainers
Readme
SNAP
SNAP is a deterministic match authority runtime for Snapshot.
It provides:
- A universal state/action model (
SnapState,SnapAction,SnapManifest) - A local deterministic engine + event log + state hash
- Pluggable modules (scoring, mutation, registry, stake, acquisitionPolicy, burn, wagerEscrow, tokenization, settlement, provenance)
- Pluggable rulesets (game logic)
- Client adapters:
createLocalSnapClient(...)createMagicBlockSnapClient(...)
Verifiable Randomness Engine (Anchor)
A reusable Solana Anchor module for verifiable randomness has been added at:
programs/snap-vrf-engine/
Architecture and integration documentation:
docs/vrf-engine-architecture.md
TypeScript integration example:
examples/vrf-engine-client.tsexamples/magicblock-vrf-adapter-example.tsexamples/magicblock-vrf-idl-codec-example.tsexamples/templates/randomness/
MagicBlock VRF adapter architecture:
docs/magicblock-vrf-integration.mddocs/snap-randomness-module.md
Multiplayer Authority Engine (Anchor + MagicBlock)
A game-agnostic on-chain multiplayer authority engine is available at:
programs/snap-multiplayer-authority/
Architecture + backend replacement notes:
docs/multiplayer-authority-engine.mddocs/game-dev-migration-playbook.mddocs/web2-team-quickstart.mddocs/devnet-contract-deploy.mddocs/magicblock-er-devnet-settlement.md
TypeScript SDK entrypoint:
createSnapMultiplayerClient(...)(recommended)createSnapGoldenClients(...)(MagicBlock-first bundled clients for multiplayer + tokenization + optional VRF/provenance)createMagicBlockMultiplayerAuthorityClient(...)- Example:
examples/multiplayer-authority-client.ts
Provenance Registry (Anchor + MagicBlock)
A game-agnostic on-chain provenance CV registry is available at:
programs/snap-provenance-registry/
TypeScript SDK entrypoint:
createSnapProvenanceClient(...)(recommended)createMagicBlockProvenanceRegistryClient(...)- Architecture:
docs/provenance-registry.md - Example:
examples/provenance-registry-client.ts
What it stores:
- Global player CV (
player_cv) aggregated across all games. - Per-game player CV (
player_game_cv) for game-specific stats. - Immutable per-match provenance record (
match_provenance) keyed byplayer + gameId + matchId.
Example:
import { createSnapProvenanceClient } from '@snapshot/snap';
const provenance = createSnapProvenanceClient({
programId: '9sprEpyqwvhJxgMdANYVCLbkj1ygoag1zUzkUKAdcped',
signer,
rpcUrl: 'https://api.devnet.solana.com',
});
await provenance.recordMatchProvenance({
player: signer.publicKey,
gameId,
matchId,
finalStateHash,
logHash,
provenanceHash: logHash,
kills: 24,
deaths: 8,
assists: 11,
score: 3200,
won: true,
metadataUri: 'ipfs://<summary-cid>',
});Layout
src/engine/
- Engine runtime and hashing
src/modules/
- Generic modules (no game-specific naming)
src/rulesets/
snapshot-hardpointctf-2d(minimal portability ruleset)
src/adapters/
- Local and MagicBlock SnapClient adapters
Core Concepts
SnapManifest
- Declares game/ruleset and module config/toggles
SnapAction
- Envelope for deterministic actions:
matchId,actor,t,kind,payload, optionalsig
SnapState
- Engine state:
- phase/seq/hash/ruleVars/modules/custom
- Ruleset-specific fields live in
state.custom.<namespace>
Modules
Builtin module factories are exported from src/modules/index.ts:
stakeacquisitionPolicyregistryscoringmutationburnwagerEscrowtokenizationsettlementprovenance
Typical usage:
- Rulesets write counters via scoring (
SCORE_ADD) - Rulesets apply timed modifiers via mutation (
MODIFIER_START/MODIFIER_END) - Rulesets can enable burn-to-use economics via burn (
BURN_USE) - Rulesets can support wager posting/lock/settlement flows via wagerEscrow
- Rulesets can tokenize any game asset via tokenization (
TOKEN_CLASS_DEFINE,TOKEN_MINT,TOKEN_TRANSFER,TOKEN_BURN) - Rulesets can declare acquisition rules (starter packs, kill rewards, purchase credits) via acquisitionPolicy
Acquisition Policy Module
acquisitionPolicy is a declarative reward allocator for custom FT/NFT classes in tokenization.
Supported patterns:
- Starter pack claim:
ACQ_CLAIM_STARTER - Purchase credit grant:
ACQ_PURCHASE_CREDIT(sku -> token rewards) - Score thresholds (e.g. kills >= N): auto-grants on
SCORE_ADD - Action rewards (e.g. first reload, objective complete): auto-grants on matching action kinds
- Admin direct grants:
ACQ_GRANT(optional, configurable)
Example config (FPS: free mags + kill rewards + store packs):
moduleConfig: {
acquisitionPolicy: {
starterPacks: [
{ id: 'starter-mags', classId: 'ammo.mag', amount: 10, maxClaimsPerActor: 1 },
],
scoreRewards: [
{
id: 'kill-reward',
counter: 'kills',
threshold: 5,
classId: 'ammo.mag',
amount: 2,
actorPath: 'payload.entityId',
repeatable: true,
maxClaimsPerActor: 100,
},
],
purchaseRewards: [
{ sku: 'mag_pack_small', classId: 'ammo.mag', amount: 15, maxClaimsPerActor: 999999 },
],
actionRewards: [
{ id: 'daily-login', actionKind: 'DAILY_LOGIN', classId: 'ammo.mag', amount: 5, actorPath: 'actor', cooldownSec: 86400 },
],
adminActors: ['game:backend'],
allowDirectGrantAction: true,
},
}Tokenization Module (2D/3D + MagicBlock Hints)
The tokenization module is chain-authority deterministic state for turning game assets into:
- NFTs (
tokenType: 'NFT') - Fungible tokens (
tokenType: 'FT')
Class definitions support media metadata for both 2D and 3D assets:
media.kind:2d,3d, oranymedia.uri, optionalpreviewUri,modelUri,animationUri,format
It also supports optional execution hints to simplify MagicBlock ER routing in clients:
chain.backend:magicblock,solana, oranychain.useEphemeralRollup: booleanchain.priorityFeeLamports,chain.programId,chain.collectionAddress
Rulesets
snapshot-hardpoint
- Zone countdown/active rotation
- Presence ownership/contested state
- Signal scoring while owned and uncontested
- Drop extraction -> mutation modifier flow
ctf-2d
state.custom.ctf2d = { scoresByTeam, flagHeldBy?, timer }- Handles
FLAG_PICKUP,FLAG_CAPTURE,TICK - On capture, writes
ctf_scorevia scoring module
Run Sims
From repo root:
npm run sim --workspace=@snapshot/snapThis runs both rulesets and prints deterministic state snapshots and hashes.
Client Adapters
Local
import { createLocalSnapClient } from '@snapshot/snap';
const client = createLocalSnapClient(manifest);
await client.dispatch(action);
const state = await client.getState();
const stop = client.subscribe((s) => console.log(s.seq, s.stateHash));MagicBlock
import { createMagicBlockSnapClient } from '@snapshot/snap';
const client = createMagicBlockSnapClient({
backend: 'magicblock',
programId: 'DiTw7JwsHqrNZSfHhPDxLAfzKWoCcqpo1Pk4y2toABfK',
signer, // web3 signer adapter
magicblockRpcUrl: 'http://127.0.0.1:8899',
solanaRpcUrl: 'https://api.devnet.solana.com',
// Optional: auto-submit TOKEN_* actions and inject payload.txRef.
tokenization: true,
});MagicBlock Tokenization Adapter
import { createMagicBlockTokenizationClientAdapter } from '@snapshot/snap';
const tokenization = createMagicBlockTokenizationClientAdapter({
backend: 'magicblock',
signer,
magicblockRpcUrl: 'http://127.0.0.1:8899',
solanaRpcUrl: 'https://api.devnet.solana.com',
// Default mode is `spl`:
// - TOKEN_CLASS_DEFINE => creates SPL mint account
// - TOKEN_MINT / TOKEN_TRANSFER / TOKEN_BURN => sends SPL token instructions
instructionMode: 'spl',
nftMetadataMode: 'metaplex',
defaultChainHint: {
backend: 'magicblock',
useEphemeralRollup: true,
},
});
// Sends onchain tx and injects txRef(signature) into payload for SNAP modules.
const actionWithTx = await tokenization.dispatchAndAttachTxRef(tokenMintAction);Notes:
TOKEN_METADATA_SETremains memo-level unless you provide a custominstructionBuilder.- For NFT class defines, set
nftMetadataMode: 'metaplex'to auto-create a Metaplex metadata account frompayload.metadata/payload.media. - NFT classes in
splmode are handled as NFT-like SPL mints (decimals=0, supply1). - Optional overrides:
metaplexTokenMetadataProgramId,splTokenProgramId,associatedTokenProgramId. - Set
instructionMode: 'memo'to keep the previous transport-only behavior.
JS-Hosted Multiplayer (No Rust Plugin Required)
If you do not want to write an on-chain plugin program, use JS-hosted mode:
import { createSnapJsHostedMultiplayerClient } from '@snapshot/snap';
const client = createSnapJsHostedMultiplayerClient({
programId: 'DiTw7JwsHqrNZSfHhPDxLAfzKWoCcqpo1Pk4y2toABfK',
signer, // can be a delegated/session signer for low-friction rapid actions
useMagicBlock: true,
magicblockRpcUrl: 'http://127.0.0.1:8899',
codec: {
decodeState: (bytes) => decodeGameState(bytes),
encodeState: (state) => encodeGameState(state),
encodeActionType: (action) => action.typeId,
},
plugin: {
validateAction(state, action) {
// optional
},
applyAction(state, action) {
return reduceGameState(state, action);
},
},
});How it works:
- state transitions run in JS (
validateAction/applyAction) - the resulting next state bytes are submitted onchain through SNAP authority
- with
useMagicBlock: true, write txs route through MagicBlock ER for low latency
Quick migrated-games demo:
set SNAP_PROGRAM_ID=<SNAP_MULTIPLAYER_PROGRAM_ID>
set SNAP_SIGNER_KEYPAIR=C:\path\to\id.json
set MAGICBLOCK_RPC_URL=<MAGICBLOCK_RPC>
set SOLANA_RPC_URL=https://api.devnet.solana.com
npm run demo:migratedThis runs two pluginless migrated demos (migrated-tactics, migrated-card) against the same SNAP authority program.
SNAP_AUTHORITY_BACKEND defaults to magicblock; set it to local only if you want to disable ER routing.
FT Ammo / Charges Pattern (Magazines, Potions, Energy)
Yes, SNAP supports this pattern directly:
- Define a fungible token class for charges/ammo.
- Mint balances to player wallets/accounts.
- On reload/use, burn FT amount onchain (
TOKEN_BURN). - Dispatch gameplay action only after burn succeeds.
Example flow:
await client.dispatch({
matchId,
actor: playerPubkey,
t: Date.now(),
kind: 'TOKEN_BURN',
payload: {
classId: 'ammo.mag',
owner: playerPubkey,
amount: 1,
chain: { backend: 'magicblock', useEphemeralRollup: true },
},
});
await client.dispatch({
matchId,
actor: playerPubkey,
t: Date.now(),
kind: 'RELOAD',
payload: { weaponId: 'rifle.alpha' },
});With MagicBlock session/delegation, players do not need to approve every mid-match burn manually; the delegated signer/session handles tx submission.
Ruleset Guard Helper (Burn Before Action)
For shooter reloads, consumables, or ability charges, use applyTokenBurnGuards(...) in your ruleset:
import { applyTokenBurnGuards } from '@snapshot/snap';
const guardConfig = {
requirements: [
// Burn 1 FT ammo token before RELOAD.
{ actionKind: 'RELOAD', classId: 'ammo.mag', amount: 1 },
// Burn 1 FT energy token before GRENADE_THROW.
{ actionKind: 'GRENADE_THROW', classId: 'energy.frag', amount: 1 },
],
};
function reduce(state: SnapState, action: SnapAction, manifest: SnapManifest): SnapState {
state = applyTokenBurnGuards(state, action, guardConfig);
// continue with your gameplay reducer...
return state;
}FPS Setup: Wager Kills + FT Mags
- Pre-match:
WAGER_POSTwith single-player objective or multiplayer wager pool.- Kill objective can be tracked via scoring counter (e.g.
kills).
- During match:
- Shooter emits
TOKEN_BURNforammo.magon each reload. - Ruleset gates
RELOADusingapplyTokenBurnGuards.
- End of match:
- Resolve wager with
WAGER_SETTLE. - For single-player objective wager, set objective with comparator/target (for example,
kills >= 20).
Notes:
- Adapter is transport/execution only.
- Rules logic stays in rulesets/modules, not in adapter code.
