-
Notifications
You must be signed in to change notification settings - Fork 2
Project documentation
A continuously evolving, multi-chain Web3 "crypto pets" game. Players mint a starter pet (an NFT), then level it up, rename it, breed it with another pet to create offspring, battle other pets, and transfer pets to other wallets — all backed by on-chain state on both Ethereum and Solana.
Live demo: https://cryptopets.vercel.app
CryptoPets is a full-stack Web3 playground built as a pnpm monorepo. The same game logic is implemented twice — once as Solidity smart contracts for EVM chains and once as a Rust/Anchor program for Solana — and a single React frontend (plus a React Native mobile app) lets the user play on whichever chain their wallet is connected to. A shared TypeScript package abstracts the chain-specific differences behind a common set of hooks and types so the UI doesn't care which chain it's talking to.
The game is intentionally a "do not stop" project: a living codebase used to experiment with the latest Ethereum/Solana tooling and modern React patterns.
| Action | Description | EVM | Solana |
|---|---|---|---|
| Create | Mint a single random starter pet per wallet | createRandom(name) |
create_starter_pet |
| Level up | Pay a fee to increase a pet's level |
levelUp(tokenId) (0.001 ETH) |
level_up (fee in lamports) |
| Rename | Change a pet's name (gated by level on EVM) |
changeName (level ≥ 2) |
rename_pet |
| Breed | Combine two pets into a new child pet using verifiable randomness |
requestCreateFromDNA → VRF callback |
commit_breed → settle_breed
|
| Battle | Two pets fight; winner/loser stats update |
battle / attack
|
commit_battle → settle_battle
|
| Transfer | Send a pet to another wallet | ERC-721 transfer | transfer_pet |
Both implementations use verifiable randomness for fairness:
- Ethereum: Chainlink VRF v2.5 (asynchronous request/callback).
- Solana: Switchboard On-Demand VRF with a commit/settle two-step flow.
Frontend (web): React 19, TypeScript, Vite, React Router, TanStack Query, Wagmi + Viem (EVM), Solana web3.js + Anchor, Dynamic.xyz (wallet auth / embedded wallets).
Backend: Node.js, Express, TypeScript, JWT, Ethers.js (EVM signature
verification), TweetNaCl (Solana signature verification). Runs with tsx hot
reload in dev; deployed to Render.
Mobile: React Native + TypeScript (Android + iOS native projects included).
Marketing website: Next.js (App Router) + TypeScript, deployed to Vercel.
Shared core: TypeScript library (@shared/core) consumed by frontend,
mobile, and website.
Smart contracts:
- Ethereum: Solidity 0.8.24, Hardhat, Hardhat Ignition, OpenZeppelin ERC-721, Chainlink VRF v2.5.
- Solana: Rust, Anchor framework, Switchboard On-Demand VRF, Docker-based local validator.
Tooling: pnpm 9.15.9 workspaces, Husky pre-commit hooks, ESLint, Prettier,
concurrently for orchestrating multi-service dev, GitHub Actions for CI/CD,
Vercel + Render for hosting.
do-not-stop/
├── package.json # Root scripts that orchestrate all packages
├── pnpm-workspace.yaml # Workspace package list
├── README.md # High-level overview
├── DEVELOPMENT.md # Setup & command reference
├── PROJECT_DOCUMENTATION.md # (this file)
│
├── frontend/ # React 19 + Vite — the playable web app
├── website/ # Next.js marketing/landing site
├── backend/ # Express + TypeScript auth API
├── mobile/ # React Native app (Android + iOS)
├── shared/ # @shared/core — cross-platform hooks, utils, types
│
├── contracts/
│ ├── ethereum/ # Hardhat + Solidity (CryptoPets ERC-721)
│ └── solana/ # Anchor + Rust (cryptopets program) + Docker validator
│
├── .github/workflows/ # CI: frontend.yml, website.yml
├── .husky/ # Git pre-commit hook
└── .devcontainer/ # Dev container definition
The pnpm workspace (pnpm-workspace.yaml) declares these packages:
frontend, website, backend, mobile, shared, contracts/ethereum,
contracts/solana/cryptopets.
The root package.json is the control panel for the whole repo. It contains no
app code — only scripts that delegate into each package with pnpm --prefix and
run multiple services together with concurrently.
| Script | Purpose |
|---|---|
install:all |
Install dependencies in every package |
dev:fe / dev:web / dev:be / dev:mobile
|
Start a single app's dev server |
eth:node |
Start the Hardhat local EVM node |
eth:deploy |
Deploy EVM contracts and inject the address into the frontend |
eth:vrf:watch |
Watch for and fulfill local Chainlink VRF mock requests (needed for breeding) |
sol:docker |
Start the Solana validator in Docker |
sol:inject-ngrok |
Wire up an ngrok tunnel for Solana local dev |
fe:eth:local |
Full EVM stack: Hardhat node + deploy + VRF watcher + backend + frontend |
mobile:eth:local |
Same as above but with the mobile app instead of web |
fe:sol:local |
Full Solana stack: backend + frontend + Solana docker/ngrok |
dev:be:fe / dev:be:mobile
|
Backend + frontend / backend + mobile together |
compile / test
|
Compile / test EVM contracts |
build |
Build contracts, backend, frontend, and website |
lint / lint:fix
|
Lint/fix across frontend, shared, website, mobile |
prepare |
Install Husky git hooks |
Note:
DEVELOPMENT.mdreferences some convenience aliases (pnpm dev,pnpm dev:full, etc.). The authoritative list of working scripts is the rootpackage.jsontable above; thefe:eth:local/fe:sol:localtargets are the canonical "start everything" commands today.
pnpm override pins valtio to 2.1.8 across the workspace.
A Hardhat project implementing the game as a set of composed Solidity contracts.
CryptoPets is the public-facing ERC-721 NFT contract (collection name
CryptoPets, symbol PETS). It acts as a facade that composes four helper
contracts, deploying them in its constructor and authorizing them to mutate
shared state:
CryptoPets (ERC-721 + VRFConsumerBaseV2Plus)
├── Inventory # Pet data store (the Pet struct, stats, level, DNA, name)
├── Battle # Battle/attack logic, win/loss tracking, cooldowns
├── Breeding # Breeding rules + DNA combination
└── Utils # Random DNA generation, rarity calculation
| Source file | Responsibility |
|---|---|
src/CryptoPets.sol |
ERC-721 ownership, public API, VRF integration, fee handling |
src/Inventory.sol |
Canonical pet records and stat mutations (caller-authorized) |
src/Battle.sol |
Combat resolution and battle stats |
src/Breeding.sol |
Breed eligibility checks and VRF-driven child creation |
src/Utils.sol |
DNA + rarity helpers |
src/LocalCryptoPetsDeployer.sol |
Helper for local deployment wiring |
-
LEVEL_UP_FEE = 0.001 ether— exact fee required bylevelUp. -
NAME_CHANGE_LEVEL = 2— minimum level to rename a pet (changeName). -
DNA_CHANGE_LEVEL = 20— minimum level to change DNA (changeDna). -
One starter per wallet:
createRandomreverts ifownerPetCount > 0. -
tokenURIpoints metadata athttps://api.cryptopets.io/metadata/{id}. -
withdraw()lets the owner collect accumulated fees.
-
requestCreateFromDNA(petId1, petId2, name)validates eligibility, marks both parents as having a pending breed, and requests random words from the VRF coordinator. EmitsBreedRandomnessRequested. - The coordinator later calls back into
fulfillRandomWords, which combines parent DNA with the random word, mints the child to the owner, clears the pending flags, and emitsBreedFulfilled.
VRF parameters: callback gas limit 500,000, 3 confirmations, 1 random word. Subscription ID, key hash, coordinator address, and native-vs-LINK payment are constructor-injected (immutable).
Local dev: the VRF coordinator is mocked. You must run
pnpm vrf:watch(or the rootpnpm eth:vrf:watch, included infe:eth:local) so a watcher script fulfillsBreedRandomnessRequestedevents against the mock.
- Solidity: 0.8.24, optimizer enabled.
-
Networks:
hardhat,localhost(chain ID 31337, RPC:8545),sepolia. - Plugins: Hardhat Toolbox with Viem.
-
Deployment: Hardhat Ignition modules in
ignition/modules/(CryptoPets.ts,CryptoPetsLive.ts). Local deployed addresses land inignition/deployments/chain-31337/deployed_addresses.jsonand are auto-injected intofrontend/.env.local. -
Scripts:
deploy.ts,verify.ts,networks.ts,sync-abi.js(keeps the frontend ABI in sync),vrf-fulfill-watcher.ts. -
Tests:
test/CryptoPets.test.ts. - See
ARCHITECTURE_COMPARISON.mdfor the rationale behind the composed-contract design.
An Anchor program (cryptopets) re-implementing the same game for Solana.
Program ID: 78AXV46ks5oFoJHkukvbsfZTJixdj2MeStzuC6thiUry.
| Instruction | Purpose |
|---|---|
initialize(level_up_fee_lamports) |
Create the global state PDA (admin + config) |
create_starter_pet(name, dna, rarity) |
Mint a player's starter pet |
level_up() |
Pay the lamport fee to level up |
rename_pet(name) |
Rename a pet |
transfer_pet() |
Transfer ownership |
pause() / unpause()
|
Admin emergency switch |
commit_battle(randomness_account) / settle_battle()
|
Two-step battle with VRF |
commit_breed(randomness_account, name) / settle_breed()
|
Two-step breeding with VRF |
Each instruction lives in its own file under
programs/cryptopets/src/instructions/. Supporting modules: state.rs
(accounts), errors.rs, rarity.rs, util.rs.
-
GlobalState(PDA seedglobal-state) — admin pubkey,level_up_fee_lamports,next_pet_idcounter,pausedflag. -
PlayerProfile(seedplayer-profile) — per-walletpet_countandstarter_createdflag (enforces one starter per player). -
PetAccount(seedpet) —id,owner,dna,rarity,level,ready_time,win_count,loss_count, and a fixed 32-byte name buffer. Helpers:is_ready,trigger_cooldown(5-secondBATTLE_COOLDOWN_SECONDS). -
BreedRequest(seedbreed-request) — pending breed: parents, child id, Switchboard randomness account, commit slot, and the chosen name. Closed on settle. -
BattleRequest(seedbattle-request) — pending battle: attacker/defender owners and pet ids, randomness account, commit slot. Closed on settle.
Battle randomness uses ATTACK_VICTORY_PROBABILITY = 70 (70% victory chance for
the attacker model).
Unlike the EVM async callback, Solana uses an explicit two-transaction pattern:
commit_* records a pending request bound to a Switchboard randomness account
and the current slot; once randomness resolves, settle_* reads it and applies
the outcome (creates the child / decides the battle), then closes the request
account.
- Validator runs in Docker (
docker-compose.yml, imagetchambard/solana-test-validator:latest): RPC:8899, WS:8900, metrics:9900, quiet logging. -
scripts/inject-ngrok.tsexposes the local validator through an ngrok tunnel (used byfe:sol:local) so wallet providers can reach it. - Build/test with Anchor (
anchor build/anchor test); integration tests intests/cryptopets.ts.
A framework-agnostic TypeScript library that hides chain differences behind a single API. Consumed by the frontend, mobile app, and (partly) the website. This is the heart of the "write once, play on any chain" design.
-
Auth store:
setSolanaAuthSigner,getSolanaAuthSigner,subscribeSolanaAuth,getSolanaAuthAddress— manage the active Solana signing identity for backend auth. -
API client:
createAuthApiClient,setStorageAdapter,getStorageAdapter— talk to the backend auth API with a pluggable token-storage adapter (so web useslocalStorage, mobile uses async storage, etc.). -
Contexts (React providers):
-
AuthProvider/useAuth— login state and JWT lifecycle. -
ApiClientProvider/useApiClient. -
SolanaAnchorProvider/useSolanaAnchor— Anchor program + signing wallet. -
PetsConfigProvider/usePetsConfig— injects EVM contract config.
-
-
Types:
Pet,PetChain('evm' | 'solana'),PetAction(create | levelUp | rename | battle | breed | transfer). -
queryClient— a shared TanStack Query client.
interface Pet {
id: string;
chain: 'evm' | 'solana';
name: string;
dna: bigint;
level: number;
rarity: number;
winCount: number;
lossCount: number;
readyAt: number;
}EVM and Solana raw structures are normalized into this shape by
utils/pets/mapEvmPet.ts and utils/pets/mapSolanaPet.ts.
Action hooks expose one API regardless of chain; internally they dispatch to the EVM or Solana implementation based on the active chain:
-
useActiveChain— which chain is currently selected. -
usePetList— the connected wallet's pets (normalized). -
useCreatePet,useLevelUpPet,useRenamePet,useBattlePets,useBreedPets,useTransferPet— the gameplay mutations. -
useNonce— fetch a backend nonce for signature auth. - Chain-specific hook sets under
hooks/chains/ethereum(Wagmi-based:usePetsContract,useWatchPetsContract,useUserProfile,useVerifySignature) andhooks/chains/solana(useProgram,usePets,usePetActions,useGlobalState,usePlayerProfile,useSolana).
-
ethereum/— address validation, error parsing, pet card formatting, ready-time math. -
solana/— PDA derivation (pdas.ts), program ID, account client, Switchboard VRF transaction builders (battleWithSwitchboardVrf.ts,breedWithSwitchboardVrf.ts,switchboardVrfTx.ts), signature-auth codec, transaction error parsing, number/address helpers. -
pets/— chain-agnostic helpers:cosmetics(DNA → appearance),featureSupport(which actions a chain supports),readyPets, error mapping, and the EVM/Solana →Petmappers.
A small Express + TypeScript API whose sole job is Web3 authentication: prove a user controls a wallet address, then issue a JWT for authenticated requests. User records are kept in-memory (a database would replace this in production).
-
src/server.ts— entrypoint, binds toPORT(default 3001). -
src/app.ts— Express app: CORS (optionally restricted viaCORS_ORIGIN), JSON body parsing, route mounting. -
src/routes/auth.ts— nonce + signature verification + JWT issuance. -
src/routes/protected.ts— example JWT-gated endpoints. -
src/routes/health.ts— health check. -
api/index.ts— serverless entry (Vercel-style) wrapper.
| Method & path | Description | Auth |
|---|---|---|
GET / |
API info + endpoint map | — |
GET /api/auth/nonce |
Get a one-time nonce to sign | — |
POST /api/auth/verify |
Verify {address, signature, nonce}; returns JWT + user |
— |
GET /api/protected/profile |
Current user profile | Bearer JWT |
GET /api/protected/users |
All known users | Bearer JWT |
GET /api/health |
{status, timestamp, users} |
— |
- Frontend requests a nonce (
GET /api/auth/nonce). - User signs a message containing that nonce in their wallet.
- Frontend posts
{address, signature, nonce}to/api/auth/verify. - Backend verifies the signature against the address — ethers.js for EVM signatures, TweetNaCl for Solana signatures.
- On success it issues a JWT; the frontend stores it and sends it as
Authorization: Bearer <token>on protected calls.
- Env vars:
JWT_SECRET(required),PORT(default 3001),CORS_ORIGIN(optional comma-separated allowlist). Seebackend/env.example. - Dev:
pnpm dev:be(tsx hot reload). Build:pnpm --filter backend build. - Deployed to Render via the repo-root
render.yamlblueprint (servicedo-not-stop-api, health check/api/health).backend/README.mdhas a full Render setup + troubleshooting guide.backend/vercel.jsonalso supports a serverless deployment.
The playable game: React 19 + Vite + TypeScript.
-
EVM: Wagmi + Viem; ABI in
src/chains/ethereum/ethereumAbi.json, config insrc/chains/ethereum/wagmi.ts. -
Solana:
src/chains/solana/— Anchor wallet adapter, auth signer, provider, anduseDynamicSolanaWallet. -
Wallet auth / embedded wallets: Dynamic.xyz, wired in
src/contexts/dynamic/. -
Server state: TanStack Query (shared client from
@shared/core). -
Routing: React Router (
src/router/).
frontend/src/
├── main.tsx, App.tsx # Bootstrap + root component
├── config.ts # API_URL + CONTRACT_ADDRESS env wiring + token storage
├── chains/{ethereum,solana}/ # Per-chain wallet + provider plumbing
├── contexts/dynamic/ # Dynamic.xyz auth provider
├── components/
│ ├── common/ # icon, neon-button, neon-card, neon-modal, transaction-status
│ ├── layout/ # App shell
│ ├── pet/ # create-pet-modal, pet-gallery, pet-creator, pet-container,
│ │ # pet-collection-layout, send-pet-modal, pet-interactions,
│ │ # interactions/{battle,breed,level-up,rename,state-card}
│ └── wallet/ # account-dropdown, native-balance, token-balance,
│ # network-switcher (ethereum/solana), solana-wallet-trigger
├── hooks/ # useIsLoggedIn, usePetActionErrorDisplay, useWriteContractErrorState
├── pages/main/ # Main game page
├── router/ # app-routes + per-interaction routes (battle/breed/level-up/rename)
├── constants/ # chains (ethereum/solana), tokens, tones, interactionRoutes
├── styles/ # variables.css, messages.css
└── utils/errorParser.ts
-
VITE_API_URL— backend base URL (defaults tohttp://localhost:3001). -
VITE_CONTRACT_ADDRESS— EVM contract address; required, and auto-injected by the EVM deploy step. The app throws on startup if it's missing. - Registers a
localStorage-backed token storage adapter and a token-success callback with@shared/core.
- Dev:
pnpm dev:fe, or full stacks viapnpm fe:eth:local/pnpm fe:sol:local. - Build:
pnpm build→ optimized output. Deployed on Vercel (vercel.json), CI in.github/workflows/frontend.yml.
React Native + TypeScript app (@react-native-community/cli bootstrap) that
reuses @shared/core for game logic and auth.
- Entry:
App.tsx; tests in__tests__/. - Native projects:
android/(Kotlin, packagecom.cryptozombies) andios/. - Display name is CryptoPets, but the Android namespace/Kotlin package stays
com.cryptozombiesdeliberately — seemobile/README.mdfor the React NativeBuildConfigbug (facebook/react-native#52754) that blocks renaming it. - Run:
pnpm dev:mobile(Metro) then build to Android/iOS, or full local stack viapnpm mobile:eth:local.
A separate Next.js (App Router) landing site — not the playable app.
-
src/app/—layout.tsx,page.tsx. -
src/components/landing/— Hero, Features, HowItWorks, Pets, Stats, Roadmap, Testimonials, Backers, Community, Faq, Cta, Footer. -
src/components/common/— NeonButton, NeonCard (mirrors the app's neon theme). -
src/content/landing.ts— landing page copy/content. -
src/lib/openApp.ts— deep-links into the playable app usingNEXT_PUBLIC_APP_URL. - Styles in
src/styles/(globals, landing-page, layout, variables).
Deployment: its own Vercel project with Root Directory set to website
(the repo root has no next dependency). CI in .github/workflows/website.yml
runs remote Vercel builds from the repo root with rootDirectory: website. Set
NEXT_PUBLIC_APP_URL in both the Vercel project and as a GitHub Actions
variable. Full guide in website/README.md.
┌─────────────────────────────────────────────┐
│ User wallet (MetaMask / Solana / Dynamic) │
└───────────────┬─────────────────────────────┘
│ sign nonce
┌───────────────────────┴───────────────────────┐
│ │
▼ ▼
┌───────────────┐ nonce/verify (JWT) ┌──────────────────────┐
│ Frontend / │ ──────────────────────────▶ │ Backend (Express) │
│ Mobile app │ ◀────────────────────────── │ ethers / tweetnacl │
└──────┬────────┘ JWT └──────────────────────┘
│
│ via @shared/core hooks (chain-agnostic)
│
┌───┴───────────────┐
▼ ▼
┌──────────────┐ ┌────────────────────────────┐
│ EVM chain │ │ Solana chain │
│ CryptoPets │ │ cryptopets program │
│ ERC-721 │ │ (PDAs: pet/profile/global) │
│ + Chainlink │ │ + Switchboard VRF │
│ VRF │ │ (commit/settle) │
└──────────────┘ └────────────────────────────┘
The frontend calls @shared/core hooks (useCreatePet, useBattlePets, …).
Each hook checks the active chain and routes to the EVM (Wagmi/Viem) or Solana
(Anchor) implementation, then normalizes results into the unified Pet type for
the UI. Authentication is chain-agnostic: the backend verifies either an EVM or
a Solana signature and returns the same JWT.
- Node.js 20+
- pnpm 9.15.9 (the repo pins this via
packageManager) - For EVM: nothing extra (Hardhat runs in-process)
- For Solana: Docker + Docker Compose (validator), and optionally Anchor/Solana CLI + Rust to build the program
- For mobile: React Native toolchain (Android Studio / Xcode)
pnpm install:all # install every workspace package
# Then pick a stack:
pnpm fe:eth:local # Hardhat node + deploy + VRF watcher + backend + web frontend
pnpm fe:sol:local # backend + web frontend + Solana validator (Docker + ngrok)
pnpm mobile:eth:local # same as fe:eth:local but with the mobile app| Service | URL |
|---|---|
| Web frontend | http://localhost:5173 |
| Backend API | http://localhost:3001 |
| Ethereum RPC (Hardhat) | http://localhost:8545 (chain ID 31337) |
| Solana RPC | http://localhost:8899 (WS 8900, metrics 9900) |
Each package ships an env.example; copy it to the appropriate dotenv file:
-
backend/.env—JWT_SECRET,PORT,CORS_ORIGIN. -
contracts/ethereum/.env—SEPOLIA_URL,PRIVATE_KEY(testnet only). -
frontend/.env.local—VITE_API_URL,VITE_CONTRACT_ADDRESS(the latter is auto-injected by the EVM deploy step). -
contracts/solana/.env—ANCHOR_PROVIDER_URL,ANCHOR_WALLET. -
website/.env.local—NEXT_PUBLIC_APP_URL.
Breeding on local EVM needs the VRF watcher running.
fe:eth:local/mobile:eth:localstart it automatically; if you run pieces manually, also runpnpm eth:vrf:watch.
-
Build all:
pnpm build(contracts → backend → frontend → website). -
EVM tests:
pnpm test(Hardhat /CryptoPets.test.ts). -
Solana tests:
anchor testincontracts/solana/cryptopets. -
Lint:
pnpm lint/pnpm lint:fixacross frontend, shared, website, mobile. -
Pre-commit: Husky hook in
.husky/pre-commit. -
CI: GitHub Actions —
frontend.ymlandwebsite.ymlbuild and deploy to Vercel; the backend deploys to Render viarender.yaml.
| I want to… | Look here |
|---|---|
| Understand the EVM game rules |
contracts/ethereum/src/CryptoPets.sol (+ Inventory/Battle/Breeding/Utils) |
| Understand the Solana game rules |
contracts/solana/cryptopets/programs/cryptopets/src/{lib,state}.rs + instructions/
|
| Add/modify a gameplay action in the UI |
shared/src/hooks/use*.ts then the per-chain impl under hooks/chains/
|
| Change how a pet is displayed |
shared/src/utils/pets/ + frontend/src/components/pet/
|
| Touch auth / JWT |
backend/src/routes/auth.ts + shared/src/contexts/AuthContext.tsx
|
| Add a wallet/chain |
frontend/src/chains/ + frontend/src/constants/chains/
|
| Edit the landing page |
website/src/components/landing/ + website/src/content/landing.ts
|
| Adjust dev orchestration | root package.json scripts |
Generated documentation for the do-not-stop / CryptoPets monorepo. Built with
❤️ by the radcrew team.