Back to Docs

Solana Programs

Program Addresses

ProgramNetworkAddress
AnimeMarketsMainnetANiME...8f4b
MarketResolverMainnetRSLV...2c7e
AnimeMarketsDevnetANiME...test

anime_markets.rs (Anchor)

The main Solana program for creating and managing prediction markets, built with Anchor.

Rust (Anchor)
use anchor_lang::prelude::*;

declare_id!("ANiMExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");

#[program]
pub mod anime_markets {
    use super::*;

    /// Creates a new prediction market
    pub fn create_market(
        ctx: Context<CreateMarket>,
        title: String,
        description: String,
        category: String,
        image_url: String,
        duration: i64,
    ) -> Result<()> {
        require!(duration >= 3600, MarketError::DurationTooShort);
        require!(duration <= 31536000, MarketError::DurationTooLong);
        require!(title.len() <= 200, MarketError::TitleTooLong);

        let market = &mut ctx.accounts.market;
        let clock = Clock::get()?;

        market.authority = ctx.accounts.authority.key();
        market.title = title;
        market.description = description;
        market.category = category;
        market.image_url = image_url;
        market.end_time = clock.unix_timestamp + duration;
        market.yes_pool = 0;
        market.no_pool = 0;
        market.total_volume = 0;
        market.participants = 0;
        market.status = MarketStatus::Active;
        market.outcome = Outcome::None;
        market.bump = ctx.bumps.market;

        emit!(MarketCreated {
            market: market.key(),
            title: market.title.clone(),
            category: market.category.clone(),
            end_time: market.end_time,
        });

        Ok(())
    }

    /// Places a bet on a market outcome
    pub fn place_bet(
        ctx: Context<PlaceBet>,
        outcome: Outcome,
        amount: u64,
    ) -> Result<()> {
        let market = &mut ctx.accounts.market;
        let clock = Clock::get()?;

        require!(market.status == MarketStatus::Active, MarketError::MarketNotActive);
        require!(clock.unix_timestamp < market.end_time, MarketError::MarketEnded);
        require!(amount >= 1_000_000, MarketError::BelowMinimumBet); // 0.001 SOL
        require!(
            outcome == Outcome::Yes || outcome == Outcome::No,
            MarketError::InvalidOutcome
        );

        // Transfer SOL from user to market vault
        let cpi_context = CpiContext::new(
            ctx.accounts.system_program.to_account_info(),
            anchor_lang::system_program::Transfer {
                from: ctx.accounts.user.to_account_info(),
                to: ctx.accounts.market_vault.to_account_info(),
            },
        );
        anchor_lang::system_program::transfer(cpi_context, amount)?;

        // Calculate shares (simplified AMM)
        let shares = calculate_shares(market, outcome, amount);

        // Update market state
        match outcome {
            Outcome::Yes => market.yes_pool += amount,
            Outcome::No => market.no_pool += amount,
            _ => return Err(MarketError::InvalidOutcome.into()),
        }
        market.total_volume += amount;

        // Create or update user position
        let position = &mut ctx.accounts.user_position;
        if position.market == Pubkey::default() {
            position.market = market.key();
            position.user = ctx.accounts.user.key();
            position.bump = ctx.bumps.user_position;
            market.participants += 1;
        }

        match outcome {
            Outcome::Yes => position.yes_shares += shares,
            Outcome::No => position.no_shares += shares,
            _ => {}
        }

        emit!(BetPlaced {
            market: market.key(),
            user: ctx.accounts.user.key(),
            outcome,
            amount,
            shares,
        });

        Ok(())
    }

    /// Claims winnings from a resolved market
    pub fn claim_winnings(ctx: Context<ClaimWinnings>) -> Result<()> {
        let market = &ctx.accounts.market;
        let position = &mut ctx.accounts.user_position;

        require!(market.status == MarketStatus::Resolved, MarketError::NotResolved);
        require!(!position.claimed, MarketError::AlreadyClaimed);

        let (user_shares, winning_pool, losing_pool) = match market.outcome {
            Outcome::Yes => (position.yes_shares, market.yes_pool, market.no_pool),
            Outcome::No => (position.no_shares, market.no_pool, market.yes_pool),
            _ => return Err(MarketError::InvalidOutcome.into()),
        };

        require!(user_shares > 0, MarketError::NoWinningShares);

        // Calculate payout
        let payout = user_shares + (user_shares * losing_pool / winning_pool);
        
        // Deduct 2% protocol fee
        let fee = payout * 200 / 10000;
        let final_payout = payout - fee;

        position.claimed = true;

        // Transfer SOL from vault to user
        let market_seeds = &[
            b"market",
            market.title.as_bytes(),
            &[market.bump],
        ];
        let signer = &[&market_seeds[..]];

        let cpi_context = CpiContext::new_with_signer(
            ctx.accounts.system_program.to_account_info(),
            anchor_lang::system_program::Transfer {
                from: ctx.accounts.market_vault.to_account_info(),
                to: ctx.accounts.user.to_account_info(),
            },
            signer,
        );
        anchor_lang::system_program::transfer(cpi_context, final_payout)?;

        emit!(WinningsClaimed {
            market: market.key(),
            user: ctx.accounts.user.key(),
            payout: final_payout,
        });

        Ok(())
    }

    /// Resolves a market with final outcome (admin only)
    pub fn resolve_market(
        ctx: Context<ResolveMarket>,
        outcome: Outcome,
    ) -> Result<()> {
        let market = &mut ctx.accounts.market;
        let clock = Clock::get()?;

        require!(market.status == MarketStatus::Active, MarketError::NotActive);
        require!(clock.unix_timestamp >= market.end_time, MarketError::NotEnded);
        require!(
            outcome == Outcome::Yes || outcome == Outcome::No,
            MarketError::InvalidOutcome
        );

        market.status = MarketStatus::Resolved;
        market.outcome = outcome;

        emit!(MarketResolved {
            market: market.key(),
            outcome,
            yes_pool: market.yes_pool,
            no_pool: market.no_pool,
        });

        Ok(())
    }
}

fn calculate_shares(market: &Market, outcome: Outcome, amount: u64) -> u64 {
    let pool = match outcome {
        Outcome::Yes => market.yes_pool,
        Outcome::No => market.no_pool,
        _ => 0,
    };

    if pool == 0 {
        return amount;
    }

    // Simplified constant product AMM
    (amount as u128 * 1_000_000_000 / (pool as u128 + amount as u128)) as u64
}

#[derive(Accounts)]
#[instruction(title: String)]
pub struct CreateMarket<'info> {
    #[account(
        init,
        payer = authority,
        space = 8 + Market::INIT_SPACE,
        seeds = [b"market", title.as_bytes()],
        bump
    )]
    pub market: Account<'info, Market>,

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

    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct PlaceBet<'info> {
    #[account(mut)]
    pub market: Account<'info, Market>,

    #[account(
        init_if_needed,
        payer = user,
        space = 8 + UserPosition::INIT_SPACE,
        seeds = [b"position", market.key().as_ref(), user.key().as_ref()],
        bump
    )]
    pub user_position: Account<'info, UserPosition>,

    /// CHECK: Market vault PDA
    #[account(mut, seeds = [b"vault", market.key().as_ref()], bump)]
    pub market_vault: AccountInfo<'info>,

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

    pub system_program: Program<'info, System>,
}

#[account]
#[derive(InitSpace)]
pub struct Market {
    pub authority: Pubkey,
    #[max_len(200)]
    pub title: String,
    #[max_len(1000)]
    pub description: String,
    #[max_len(50)]
    pub category: String,
    #[max_len(200)]
    pub image_url: String,
    pub end_time: i64,
    pub yes_pool: u64,
    pub no_pool: u64,
    pub total_volume: u64,
    pub participants: u32,
    pub status: MarketStatus,
    pub outcome: Outcome,
    pub bump: u8,
}

#[account]
#[derive(InitSpace)]
pub struct UserPosition {
    pub market: Pubkey,
    pub user: Pubkey,
    pub yes_shares: u64,
    pub no_shares: u64,
    pub claimed: bool,
    pub bump: u8,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)]
pub enum MarketStatus {
    Pending,
    Active,
    Paused,
    Resolved,
    Cancelled,
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)]
pub enum Outcome {
    None,
    Yes,
    No,
}

#[error_code]
pub enum MarketError {
    #[msg("Duration too short (minimum 1 hour)")]
    DurationTooShort,
    #[msg("Duration too long (maximum 1 year)")]
    DurationTooLong,
    #[msg("Title too long")]
    TitleTooLong,
    #[msg("Market is not active")]
    MarketNotActive,
    #[msg("Market has ended")]
    MarketEnded,
    #[msg("Below minimum bet amount")]
    BelowMinimumBet,
    #[msg("Invalid outcome")]
    InvalidOutcome,
    #[msg("Market not resolved")]
    NotResolved,
    #[msg("Already claimed")]
    AlreadyClaimed,
    #[msg("No winning shares")]
    NoWinningShares,
    #[msg("Market not active")]
    NotActive,
    #[msg("Market not ended")]
    NotEnded,
}

#[event]
pub struct MarketCreated {
    pub market: Pubkey,
    pub title: String,
    pub category: String,
    pub end_time: i64,
}

#[event]
pub struct BetPlaced {
    pub market: Pubkey,
    pub user: Pubkey,
    pub outcome: Outcome,
    pub amount: u64,
    pub shares: u64,
}

#[event]
pub struct MarketResolved {
    pub market: Pubkey,
    pub outcome: Outcome,
    pub yes_pool: u64,
    pub no_pool: u64,
}

#[event]
pub struct WinningsClaimed {
    pub market: Pubkey,
    pub user: Pubkey,
    pub payout: u64,
}

TypeScript Client

Interact with the program using the Anchor TypeScript client.

TypeScript
import * as anchor from '@coral-xyz/anchor';
import { Program } from '@coral-xyz/anchor';
import { Connection, PublicKey, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { AnimeMarkets } from './idl/anime_markets';

// Initialize connection and provider
const connection = new Connection('https://api.mainnet-beta.solana.com');
const wallet = (window as any).solana;
const provider = new anchor.AnchorProvider(connection, wallet, {});

// Load the program
const programId = new PublicKey('ANiMExxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
const program = new Program<AnimeMarkets>(IDL, programId, provider);

// Derive market PDA
const marketTitle = 'luffy-vs-blackbeard';
const [marketPda] = PublicKey.findProgramAddressSync(
  [Buffer.from('market'), Buffer.from(marketTitle)],
  programId
);

// Derive user position PDA
const [positionPda] = PublicKey.findProgramAddressSync(
  [Buffer.from('position'), marketPda.toBuffer(), wallet.publicKey.toBuffer()],
  programId
);

// Derive vault PDA
const [vaultPda] = PublicKey.findProgramAddressSync(
  [Buffer.from('vault'), marketPda.toBuffer()],
  programId
);

// Place a YES bet
async function placeBet() {
  const tx = await program.methods
    .placeBet(
      { yes: {} },  // Outcome enum
      new anchor.BN(0.5 * LAMPORTS_PER_SOL)  // 0.5 SOL
    )
    .accounts({
      market: marketPda,
      userPosition: positionPda,
      marketVault: vaultPda,
      user: wallet.publicKey,
      systemProgram: anchor.web3.SystemProgram.programId,
    })
    .rpc();

  console.log('Bet placed! Signature:', tx);
  console.log('View on Solscan:', `https://solscan.io/tx/${tx}`);
}

// Fetch market data
async function getMarket() {
  const market = await program.account.market.fetch(marketPda);
  
  console.log('Title:', market.title);
  console.log('YES Pool:', market.yesPool.toNumber() / LAMPORTS_PER_SOL, 'SOL');
  console.log('NO Pool:', market.noPool.toNumber() / LAMPORTS_PER_SOL, 'SOL');
  console.log('Status:', Object.keys(market.status)[0]);
  
  const total = market.yesPool.toNumber() + market.noPool.toNumber();
  const yesPercent = total > 0 ? (market.yesPool.toNumber() / total * 100).toFixed(1) : 50;
  console.log('YES Probability:', yesPercent, '%');
}

// Listen to program events
function subscribeToEvents() {
  const listener = program.addEventListener('BetPlaced', (event, slot) => {
    console.log('New bet detected!');
    console.log('  Market:', event.market.toString());
    console.log('  User:', event.user.toString());
    console.log('  Amount:', event.amount.toNumber() / LAMPORTS_PER_SOL, 'SOL');
    console.log('  Outcome:', Object.keys(event.outcome)[0]);
    console.log('  Slot:', slot);
  });

  // Remove listener when done
  // program.removeEventListener(listener);
}