Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ba81082
chore: update solana program id to fresh address
tydreamer Jun 19, 2026
3234068
feat: map v2 pet fields from PetAccount in mapSolanaPet
tydreamer Jun 19, 2026
271bdaf
feat: resolve parent2Owner for cross-owner (married) breeding
tydreamer Jun 19, 2026
25e92f7
feat: parse BattleResolved event and return firstWins from settle tx
tydreamer Jun 19, 2026
3e634a0
feat: resolve explorer URL from connection rpcEndpoint
tydreamer Jun 19, 2026
8a0d5f1
feat: expose awaiting-vrf lifecycle phase during oracle wait
tydreamer Jun 19, 2026
4370202
feat: add pending VRF recovery notices for battle and breed
tydreamer Jun 19, 2026
a7a0875
feat: add openToChallenges field, toggle action, and UI
tydreamer Jun 19, 2026
b6015f3
feat: add cancel_battle and cancel_breed support
tydreamer Jun 19, 2026
d153e9c
feat: add sync_metadata instruction support
tydreamer Jun 19, 2026
ace2352
feat: add stud fee balance reader and withdraw_stud_fees action
tydreamer Jun 19, 2026
98626a1
feat: mainnet readiness — cluster-aware VRF timing
tydreamer Jun 19, 2026
467a01d
test: add tests for new Solana hooks and update adapter mock
tydreamer Jun 19, 2026
1d728b5
fix: wire VITE_CRYPTOPETS_IDL_ADDRESS into Program.fetchIdl
tydreamer Jun 19, 2026
7808f8d
fix: split IDL fetch from Program instantiation to prevent re-fetch o…
tydreamer Jun 20, 2026
31bfc8f
fix: restore idlAddress fallback in useProgram IDL fetch
tydreamer Jun 20, 2026
54fa578
fix: pass programId to fetchIdl so the on-chain IDL resolves
tydreamer Jun 20, 2026
9e43987
chore: add initialize script for fresh program deployments
tydreamer Jun 20, 2026
e772e71
fix(solana): drop update_authority from mpl-core asset create CPI
tydreamer Jun 20, 2026
559c1ad
feat(solana): add transfer_pet instruction and wire pet sending
tydreamer Jun 20, 2026
22378c0
feat: drop backend sign-in requirement for first pet
tydreamer Jun 20, 2026
1f7fc08
feat: allow custom devnet RPC via VITE_SOLANA_DEVNET_RPC_URL
tydreamer Jun 20, 2026
f332333
fix: make interactions panel body scroll in all layouts
tydreamer Jun 20, 2026
acfcf52
feat: two-lane matchmaking layout for fighter/opponent pickers
tydreamer Jun 20, 2026
7fce4b2
feat: flank the VS arena with fighter/opponent lanes
tydreamer Jun 20, 2026
af4e50b
fix: widen the standalone battle stage to use the full row
tydreamer Jun 20, 2026
dd96f76
fix: stretch the battle stage to fill the panel height
tydreamer Jun 21, 2026
693a31f
ui: redesign arena card with card shell, grid layout, and directional…
tydreamer Jun 21, 2026
8fc38e7
fix: auto-select married Solana pet and show spouse indicator in bree…
tydreamer Jun 21, 2026
541661d
test: update breed panel tests for married-pet auto-select and spouse…
tydreamer Jun 21, 2026
17c5eec
test: fix pre-existing CI failures in battle-setup and level-up tests
tydreamer Jun 21, 2026
f76e041
chore: prune unused deps and tidy marriage/account-dropdown
tydreamer Jun 22, 2026
2a23ab9
refactor: move formatExpiry to shared/utils/common/time
tydreamer Jun 22, 2026
490cde4
refactor: split marriage panel into focused part components
tydreamer Jun 22, 2026
bb49df9
chore(frontend): add prettier + editorconfig and reformat src
tydreamer Jun 22, 2026
5b9facc
refactor(frontend): colocate interaction panel CSS
tydreamer Jun 22, 2026
20bbb96
refactor: extract shared useSpousePet hook
tydreamer Jun 22, 2026
d38654b
refactor: decompose breed panel into parts
tydreamer Jun 22, 2026
8a88e84
refactor: extract usePetCooldowns hook from pet-gallery
tydreamer Jun 22, 2026
6265b7e
feat: extend NeonButton palette for app-wide adoption
tydreamer Jun 23, 2026
fa654d3
refactor: render auth action buttons via NeonButton
tydreamer Jun 23, 2026
7ff47cb
refactor: migrate dashboard hub buttons to NeonButton
tydreamer Jun 23, 2026
9689b54
fix(shared): add missing chainId deps to settle callbacks
tydreamer Jun 23, 2026
d9147ea
refactor: migrate remaining action buttons to NeonButton
tydreamer Jun 23, 2026
45d8bde
refactor: migrate send-pet modal to NeonModal
tydreamer Jun 23, 2026
8f352eb
refactor(frontend): migrate create-pet modal to NeonModal
tydreamer Jun 23, 2026
43f6726
refactor(frontend): migrate marriage confirm dialog to NeonModal
tydreamer Jun 23, 2026
ad01d38
chore: tighten no-explicit-any rule and drop dead CSS
tydreamer Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ DIRECT_URL="postgresql://postgres.<project-ref>:<password>@aws-1-<region>.pooler
# HELIUS_RPC_URL : full Helius RPC URL incl. ?api-key= (devnet or mainnet host).
# SOLANA_PROGRAM_ID : CryptoPets program id (base58) whose accounts we index.
# HELIUS_RPC_URL=https://devnet.helius-rpc.com/?api-key=<your-helius-key>
# SOLANA_PROGRAM_ID=78AXV46ks5oFoJHkukvbsfZTJixdj2MeStzuC6thiUry
# SOLANA_PROGRAM_ID=88HGagCw4i3BTMHEpdQy3YLeHrkTSKgXmvq66HJXKM7k

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tydreamer it looks weird to have this here :)
can you please remove the value?
so the actual value will be in .env.staging and .env.production

#
# Shared secret Helius sends in the webhook Authorization header — set it both
# here and in the Helius webhook's "Authorization Header" so POST /api/webhooks/
Expand Down
4 changes: 2 additions & 2 deletions contracts/solana/cryptopets/Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ resolution = true
skip-lint = false

[programs.localnet]
cryptopets = "78AXV46ks5oFoJHkukvbsfZTJixdj2MeStzuC6thiUry"
cryptopets = "88HGagCw4i3BTMHEpdQy3YLeHrkTSKgXmvq66HJXKM7k"

[programs.devnet]
cryptopets = "78AXV46ks5oFoJHkukvbsfZTJixdj2MeStzuC6thiUry"
cryptopets = "88HGagCw4i3BTMHEpdQy3YLeHrkTSKgXmvq66HJXKM7k"

[provider]
cluster = "devnet"
Expand Down
3 changes: 2 additions & 1 deletion contracts/solana/cryptopets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"scripts": {
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check",
"inject-ngrok": "tsx scripts/inject-ngrok.ts"
"inject-ngrok": "tsx scripts/inject-ngrok.ts",
"initialize": "tsx scripts/initialize.ts"
},
"dependencies": {
"@coral-xyz/anchor": "^0.32.1"
Expand Down
2 changes: 2 additions & 0 deletions contracts/solana/cryptopets/programs/cryptopets/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,7 @@ pub enum ErrorCode {
MintRequestNotFound,
#[msg("Asset account does not match this pet's Metaplex Core asset")]
InvalidPetAsset,
#[msg("Cannot transfer a married pet; divorce first")]
CannotTransferMarriedPet,
}

Original file line number Diff line number Diff line change
Expand Up @@ -123,21 +123,18 @@ pub fn handler(ctx: Context<SettleBreed>) -> Result<()> {
// `invoke_signed` (it does not sign the outer transaction). See `settle_mint`'s CPI
// for the same pattern.
//
// UNVERIFIED: `CreateV1CpiBuilder`'s method names/shapes (`asset`/`collection`/
// `authority`/`payer`/`owner`/`update_authority`/`system_program`/`name`/`uri`/
// `plugins`/`invoke_signed`), plus `PluginAuthorityPair`/`Plugin::Attributes`/
// `Attributes`'s field shapes, follow the usual mpl-core ~0.10 CPI convention but have
// not been checked against the real crate (no cargo registry cache or Rust toolchain
// in this environment). Fix up against `mpl_core::instructions::CreateV1CpiBuilder`
// and `mpl_core::types` when building.
// NOTE: do NOT set `update_authority` here. When an asset is created into a
// `collection`, mpl-core inherits the asset's update authority from the collection;
// passing both a collection and an explicit `update_authority` fails with
// `MplCoreError::ConflictingAuthority` (0x1d). `authority` (GlobalState) is the
// collection's update authority signing the add-to-collection — that is sufficient.
let global_state_seeds: &[&[u8]] = &[GlobalState::SEED, &[global_state.bump]];
CreateV1CpiBuilder::new(&ctx.accounts.mpl_core_program.to_account_info())
.asset(&ctx.accounts.asset.to_account_info())
.collection(Some(&ctx.accounts.collection.to_account_info()))
.authority(Some(&global_state.to_account_info()))
.payer(&ctx.accounts.owner.to_account_info())
.owner(Some(&ctx.accounts.owner.to_account_info()))
.update_authority(Some(&global_state.to_account_info()))
.system_program(&ctx.accounts.system_program.to_account_info())
.name(child.name())
.uri(String::new())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,18 @@ pub fn handler(ctx: Context<SettleMint>) -> Result<()> {
// GlobalState PDA is the collection's update authority and signs this CPI via
// `invoke_signed` (it does not sign the outer transaction).
//
// UNVERIFIED: `CreateV1CpiBuilder`'s method names/shapes (`asset`/`collection`/
// `authority`/`payer`/`owner`/`update_authority`/`system_program`/`name`/`uri`/
// `plugins`/`invoke_signed`), plus `PluginAuthorityPair`/`Plugin::Attributes`/
// `Attributes`'s field shapes, follow the usual mpl-core ~0.10 CPI convention but have
// not been checked against the real crate (no cargo registry cache or Rust toolchain
// in this environment). Fix up against `mpl_core::instructions::CreateV1CpiBuilder`
// and `mpl_core::types` when building.
// NOTE: do NOT set `update_authority` here. When an asset is created into a
// `collection`, mpl-core inherits the asset's update authority from the collection;
// passing both a collection and an explicit `update_authority` fails with
// `MplCoreError::ConflictingAuthority` (0x1d). `authority` (GlobalState) is the
// collection's update authority signing the add-to-collection — that is sufficient.
let global_state_seeds: &[&[u8]] = &[GlobalState::SEED, &[global_state.bump]];
CreateV1CpiBuilder::new(&ctx.accounts.mpl_core_program.to_account_info())
.asset(&ctx.accounts.asset.to_account_info())
.collection(Some(&ctx.accounts.collection.to_account_info()))
.authority(Some(&global_state.to_account_info()))
.payer(&ctx.accounts.owner.to_account_info())
.owner(Some(&ctx.accounts.owner.to_account_info()))
.update_authority(Some(&global_state.to_account_info()))
.system_program(&ctx.accounts.system_program.to_account_info())
.name(pet.name())
.uri(String::new())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ pub mod level_up;
pub mod rename_pet;
pub mod sync_metadata;
pub mod train;
pub mod transfer_pet;

pub use level_up::*;
pub use rename_pet::*;
pub use sync_metadata::*;
pub use train::*;
pub use transfer_pet::*;
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use anchor_lang::prelude::*;
use mpl_core::instructions::TransferV1CpiBuilder;

use crate::{
errors::ErrorCode,
state::{GlobalState, PetAccount},
utils::metadata::core_asset_owner,
};

/// Transfer a pet to another wallet. Pets are Metaplex Core assets and the asset is the
/// source of truth for ownership (plan §2.3/v2.1 Phase A), so this CPIs mpl-core
/// `TransferV1` to move the asset and then updates the denormalized `pet.owner` snapshot so
/// owner-filtered queries (the gallery's `getProgramAccounts` memcmp on `owner`) keep
/// finding the pet under its new owner.
///
/// A married pet is refused: the spouse's `PetAccount` cross-references this pet (and its
/// `marriage_owner_snapshot` drives cross-owner breeding), so it must be divorced first.
pub fn handler(ctx: Context<TransferPet>) -> Result<()> {
require!(!ctx.accounts.global_state.paused, ErrorCode::Paused);

// Only the current (live) asset owner may transfer. mpl-core re-checks this in the CPI;
// we assert it up front for a clean error and to gate the `pet.owner` write below.
require_keys_eq!(
core_asset_owner(&ctx.accounts.pet_asset.to_account_info())?,
ctx.accounts.owner.key(),
ErrorCode::Unauthorized
);

require!(
ctx.accounts.pet.spouse_id == 0,
ErrorCode::CannotTransferMarriedPet
);

// mpl-core CPI: move the Core asset to `new_owner`. The asset's owner is the transfer
// authority and signs the outer transaction, so this is `invoke()` (no PDA seeds). The
// collection must be supplied for collection-scoped assets; it is not mutated here.
TransferV1CpiBuilder::new(&ctx.accounts.mpl_core_program.to_account_info())
.asset(&ctx.accounts.pet_asset.to_account_info())
.collection(Some(&ctx.accounts.collection.to_account_info()))
.payer(&ctx.accounts.owner.to_account_info())
.authority(Some(&ctx.accounts.owner.to_account_info()))
.new_owner(&ctx.accounts.new_owner.to_account_info())
.system_program(Some(&ctx.accounts.system_program.to_account_info()))
.invoke()?;

// Keep the cached owner in sync with the asset's new owner.
ctx.accounts.pet.owner = ctx.accounts.new_owner.key();

Ok(())
}

#[derive(Accounts)]
pub struct TransferPet<'info> {
#[account(seeds = [GlobalState::SEED], bump = global_state.bump)]
pub global_state: Account<'info, GlobalState>,

/// CHECK: this pet's Metaplex Core asset account; PDA seed for `pet` and source of
/// truth for ownership (plan §2.3/v2.1 Phase A). Mutated by the `TransferV1` CPI.
#[account(mut, owner = mpl_core::ID)]
pub pet_asset: UncheckedAccount<'info>,

#[account(
mut,
seeds = [PetAccount::SEED, pet_asset.key().as_ref()],
bump = pet.bump,
)]
pub pet: Account<'info, PetAccount>,

/// CHECK: the "CryptoPets" collection account (`global_state.collection`); supplied to
/// the `TransferV1` CPI to validate collection membership. Not mutated.
#[account(address = global_state.collection)]
pub collection: UncheckedAccount<'info>,

/// CHECK: recipient wallet that receives the Core asset. Any pubkey is valid.
pub new_owner: UncheckedAccount<'info>,

#[account(mut)]
pub owner: Signer<'info>,

/// CHECK: address-constrained to the Metaplex Core program; invoked via CPI.
#[account(address = mpl_core::ID)]
pub mpl_core_program: UncheckedAccount<'info>,

pub system_program: Program<'info, System>,
}
6 changes: 5 additions & 1 deletion contracts/solana/cryptopets/programs/cryptopets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub mod utils;
use anchor_lang::{prelude::*, solana_program::system_program};
use instructions::*;

declare_id!("78AXV46ks5oFoJHkukvbsfZTJixdj2MeStzuC6thiUry");
declare_id!("88HGagCw4i3BTMHEpdQy3YLeHrkTSKgXmvq66HJXKM7k");

#[program]
pub mod cryptopets {
Expand All @@ -29,6 +29,10 @@ pub mod cryptopets {
rename_pet::handler(ctx, name)
}

pub fn transfer_pet(ctx: Context<TransferPet>) -> Result<()> {
transfer_pet::handler(ctx)
}

pub fn set_open_to_challenges(ctx: Context<SetOpenToChallenges>, value: bool) -> Result<()> {
set_open_to_challenges::handler(ctx, value)
}
Expand Down
98 changes: 98 additions & 0 deletions contracts/solana/cryptopets/scripts/initialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env tsx
//
// One-time on-chain setup for a freshly deployed `cryptopets` program: runs the
// `initialize` instruction, which creates the `global-state` PDA (admin + fee
// config + next_pet_id) and the Metaplex Core collection that every pet is
// minted into. Without this, mint/breed/battle and pet loading have nothing to
// read or write, so the frontend shows an empty list and create fails.
//
// Idempotent: if `global-state` already exists it prints the current config and
// exits without sending a transaction.
//
// Usage (devnet):
// ANCHOR_PROVIDER_URL=https://api.devnet.solana.com \
// ANCHOR_WALLET=$HOME/.config/solana/id.json \
// pnpm exec tsx scripts/initialize.ts
//
// The wallet must be the intended program admin and hold a little devnet SOL.

import * as anchor from "@coral-xyz/anchor";
import { globalStatePda } from "../tests/utils";

// Defaults to the program id pinned in Anchor.toml ([programs.devnet]); override
// with PROGRAM_ID=... if you deploy under a different key.
const PROGRAM_ID = new anchor.web3.PublicKey(
process.env.PROGRAM_ID ?? "88HGagCw4i3BTMHEpdQy3YLeHrkTSKgXmvq66HJXKM7k",
);

// mpl-core program (stable across clusters). Required by the `initialize` CPI
// that creates the collection.
const MPL_CORE_PROGRAM_ID = new anchor.web3.PublicKey(
"CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d",
);

// Initial level-up fee in lamports; matches the test default. Tune via
// LEVEL_UP_FEE_LAMPORTS=... or adjust later with the `set_level_up_fee_lamports`
// admin instruction.
const LEVEL_UP_FEE_LAMPORTS = new anchor.BN(
process.env.LEVEL_UP_FEE_LAMPORTS ?? 1_000_000,
);

async function main() {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const wallet = provider.wallet as anchor.Wallet;

const idl = await anchor.Program.fetchIdl(PROGRAM_ID, provider);
if (!idl) {
throw new Error(
`IDL not found on-chain for ${PROGRAM_ID.toBase58()}. Deploy the program and run \`anchor idl init\` first.`,
);
}
const program = new anchor.Program(idl, provider);

const [globalState] = globalStatePda(PROGRAM_ID);

console.log("program :", PROGRAM_ID.toBase58());
console.log("admin :", wallet.publicKey.toBase58());
console.log("cluster :", provider.connection.rpcEndpoint);
console.log("globalState:", globalState.toBase58());

const existing = await provider.connection.getAccountInfo(globalState);
if (existing) {
const gs = await (program.account as any).globalState.fetch(globalState);
console.log("\n✅ Already initialized — nothing to do.");
console.log(" collection :", gs.collection.toBase58());
console.log(" admin :", gs.admin.toBase58());
console.log(" nextPetId :", gs.nextPetId);
console.log(" paused :", gs.paused);
return;
}

const collection = anchor.web3.Keypair.generate();
console.log("\nInitializing… new collection:", collection.publicKey.toBase58());

const sig = await program.methods
.initialize(LEVEL_UP_FEE_LAMPORTS)
.accounts({
globalState,
collection: collection.publicKey,
admin: wallet.publicKey,
mplCoreProgram: MPL_CORE_PROGRAM_ID,
})
.signers([collection])
.rpc();

console.log("\n✅ Initialized.");
console.log(" tx :", sig);
console.log(" collection :", collection.publicKey.toBase58());
console.log(
"\nSave the collection address — it lives in global-state and is used by",
"every mint/sync. No env change is needed; the frontend reads it on-chain.",
);
}

main().catch((err) => {
console.error("\n❌ initialize failed:", err instanceof Error ? err.message : err);
process.exit(1);
});
Loading
Loading