Sol.College

Full lesson

Gali

Mine $ORE in an open-world pixel game.

What it does

From the project README:

Walk into a shared town and ride a mine cart around it. Head to the island and deploy SOL on real ORE rounds. Stay for the arcade, the Arena and the people.
Original
juntdoe-sol/Gali
SuperStack fork
Blockchains/Gali @ 4ecc50a
Stars
0
Last push (original)
2026-11-04
Languages
TypeScript, Python, JavaScript, Rust, PLpgSQL, CSS
Detected stack
AnchorMetaplexweb3.js
Categories
Gaming
Licence
MIT
Homepage
gali.lol

Architecture

256 source files. By extension: .ts ×155, .tsx ×49, .py ×41, .js ×8, .rs ×3.

Top-level directorySource files
app161
scripts46
supabase15
video9
admin7
tv6
relay5
programs3

On-chain programs: programs/gali, programs/ore-mock

Dapp: devnet deploy pending

⏳ Devnet deploy pending — goes live when the devnet wallet is topped up. This fork's own dapp is not deployed yet, so there is no program address or transaction to show. It is queued in dapp-queue.txt; once devnet SOL is available it is deployed, exercised with real devnet transactions, and this section switches to the live dapp at /dapps/gali/. Until then, the lesson and step-by-step guide below are complete, and you can run it locally with the commands below.

Key code, explained

Excerpts chosen automatically where the code uses the most Solana concepts, pinned to the fork's commit.

juntdoe-sol/Gali · programs/gali/src/lib.rs L781–804 @ 4ecc50a · MIT · original
            .ok_or(GaliError::BadAmount)?;        p.stake_slot = Clock::get()?.slot;        emit!(StakedOre {            owner: p.owner,            amount,            total: p.staked_ore        });        Ok(())    }     /// Take staked $ORE back, at any time; there is no lock. The same ORE still    /// cannot boost two wallets for one round: `record_ore_round` reads the stake    /// at record time and only honours it if `stake_slot` predates the round's end,    /// so tokens moved to another wallet after a round cannot boost that round    /// there, and the wallet they left no longer holds them when it is recorded.    pub fn unstake_ore(ctx: Context<UnstakeOre>, amount: u64) -> Result<()> {        let p = &mut ctx.accounts.player;        require!(amount > 0 && amount <= p.staked_ore, GaliError::BadAmount);        p.staked_ore -= amount;        let bump = ctx.accounts.config.bump;        let signer: &[&[&[u8]]] = &[&[b"config", &[bump]]];        token_interface::transfer_checked(            CpiContext::new_with_signer(                ctx.accounts.token_program.to_account_info(),
  1. Line 782 p.stake_slot = Clock::get()?.slot;: Reads on-chain time from the Clock sysvar.
  2. Line 783 emit!(StakedOre {: Emits an Anchor event that clients can parse from the logs.
  3. Line 791 /// Take staked $ORE back, at any time; there is no lock. The same ORE still: Touches staking state: the amount locked and when it was locked.
  4. Line 798 require!(amount > 0 && amount <= p.staked_ore, GaliError::BadAmount);: An explicit validation that aborts the instruction with an error.
  5. Line 800 let bump = ctx.accounts.config.bump;: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
  6. Line 802 token_interface::transfer_checked(: A checked token transfer, which also verifies the mint and decimals.
  7. Line 803 CpiContext::new_with_signer(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
juntdoe-sol/Gali · programs/ore-mock/src/lib.rs L85–108 @ 4ecc50a · MIT · original
        let lamports = Rent::get()?.minimum_balance(len);        invoke_signed(            &system_instruction::create_account(payer.key, account.key, lamports, len as u64, &crate::ID),            &[payer.to_account_info(), account.clone(), system_program.to_account_info()],            &[seeds],        )?;    }    account.try_borrow_mut_data()?[..data.len()].copy_from_slice(data);    Ok(())} #[derive(Accounts)]pub struct PutBoard<'info> {    #[account(mut)]    pub payer: Signer<'info>,    /// CHECK: written as raw bytes; this program is a test fixture.    #[account(mut, seeds = [b"board"], bump)]    pub account: UncheckedAccount<'info>,    pub system_program: Program<'info, System>,} #[derive(Accounts)]#[instruction(round_id: u64)]pub struct PutRound<'info> {
  1. Line 86 invoke_signed(: Makes a CPI signed by a PDA. The signer seeds prove the program controls that address.
  2. Line 99 pub payer: Signer<'info>,: Requires this account to have signed the transaction.
  3. Line 101 #[account(mut, seeds = [b"board"], bump)]: Declares the PDA seeds. Anchor re-derives the address and rejects the account if it doesn't match.
  4. Line 101 #[account(mut, seeds = [b"board"], bump)]: Uses the bump seed, the extra byte that pushes the derived address off the ed25519 curve.
juntdoe-sol/Gali · programs/gali/src/ore.rs L133–156 @ 4ecc50a · MIT · original
    /// value means the draw has not happened, and awarding anything from that    /// would be awarding from a result nobody knows yet.    ///    /// An all-0xFF entropy is ORE's marker for a round it could not draw and    /// refunded instead. ORE's own `Round::rng` returns `None` for it, so no    /// square wins and nobody is paid; this mirrors that by loading the round    /// with `winning_square: None`, which every caller treats as "no winner".    pub fn load(info: &AccountInfo<'_>, round_id: u64) -> Result<Self> {        let (expected, _) = Pubkey::find_program_address(            &[ORE_ROUND_SEED, &round_id.to_le_bytes()],            &ORE_PROGRAM_ID,        );        let data = ore_bytes(info, expected, round_at::SIZE)?;         let id = u64_at(&data, round_at::ID);        require!(id == round_id, GaliError::BadOreAccount);         let entropy = &data[DISC + round_at::SLOT_HASH..DISC + round_at::SLOT_HASH + 32];        require!(entropy.iter().any(|b| *b != 0), GaliError::OreRoundUnsettled);        let square = winning_square_from(entropy);         // Range checks: if ORE moves a field, these trip before a misread value pays anything.        let expires_at = u64_at(&data, round_at::EXPIRES_AT);        let slot = Clock::get()?.slot;
  1. Line 141 let (expected, _) = Pubkey::find_program_address(: Derives a PDA from seeds and the program ID and returns the address with its canonical bump.
  2. Line 148 require!(id == round_id, GaliError::BadOreAccount);: An explicit validation that aborts the instruction with an error.
  3. Line 156 let slot = Clock::get()?.slot;: Reads on-chain time from the Clock sysvar.
juntdoe-sol/Gali · app/src/chain/arcade.ts L73–96 @ 4ecc50a · MIT · original
} /** Send one stake. `memo` is `gali1|<game>|<token>|<round>|<choice>`. Returns the transaction signature. */export async function sendStake(t: StakeTable, amountUi: string, memo: string): Promise<string> {  if (!/^gali1\|[tpc]\|[os]\|\d{1,9}\|\d{1,4}$/.test(memo)) throw new Error('Bad stake');  const pin = PINNED[memo.split('|')[2]];  if (!pin || t.mint !== pin.mint || !pin.chips.includes(amountUi)) throw new Error('This stake does not match the arcade tables');  if (t.program !== TOKEN_PROGRAM.toBase58() && t.program !== TOKEN_2022.toBase58()) throw new Error('Unsupported token');  if (t.houseAta !== houseAccountFor(t.mint, t.program)) throw new Error('The arcade sent an unknown house account. Stake not sent.');  return sendWithWallet(async (owner) => {    const mint = new PublicKey(t.mint);    const prog = new PublicKey(t.program);    if (!prog.equals(TOKEN_PROGRAM) && !prog.equals(TOKEN_2022)) throw new Error('Unsupported token');    const src = PublicKey.findProgramAddressSync([owner.toBuffer(), prog.toBuffer(), mint.toBuffer()], ASSOCIATED_TOKEN_PROGRAM_ID)[0];    const units = unitsOf(amountUi, t.decimals);    const data = Buffer.alloc(10);    data[0] = 12; // TransferChecked    new DataView(data.buffer, data.byteOffset, 10).setBigUint64(1, units, true);    data[9] = t.decimals;    return [      new TransactionInstruction({        programId: prog,        keys: [          { pubkey: src, isSigner: false, isWritable: true },
  1. Line 75 /** Send one stake. `memo` is `gali1|<game>|<token>|<round>|<choice>`. Returns the transac: Touches staking state: the amount locked and when it was locked.
  2. Line 86 const src = PublicKey.findProgramAddressSync([owner.toBuffer(), prog.toBuffer(), mint.toBu: Derives a PDA from seeds and the program ID and returns the address with its canonical bump.
  3. Line 89 data[0] = 12; // TransferChecked: A checked token transfer, which also verifies the mint and decimals.

How to run, build and test

These commands come from the repo's own manifests (package.json scripts, Anchor.toml, Cargo, go.mod, Makefile). Check the README for prerequisites and environment variables.

DirectoryCommandRuns
.npm run testanchor test
adminnpm run devvite
adminnpm run buildtsc -b && vite build
relaynpm run devwrangler dev
relaynpm run testnode --test test/*.test.ts
relaynpm run deploywrangler deploy
appnpm run startexpo start
appnpm run testnode tests/run.cjs
.anchor buildcompile programs and generate IDLs
.anchor testlocal validator + test suite
.anchor run testnode scripts/run-tests.cjs
.cargo buildRust workspace build
.cargo testRust tests

What you could build from it

Used in lessons