Back to Docs
Solana Programs
Program Addresses
| Program | Network | Address | |
|---|---|---|---|
| AnimeMarkets | Mainnet | ANiME...8f4b | |
| MarketResolver | Mainnet | RSLV...2c7e | |
| AnimeMarkets | Devnet | ANiME...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);
}