Skip to content

Instantly share code, notes, and snippets.

@jongan69
Last active March 5, 2025 03:49
Show Gist options
  • Save jongan69/9d1e248febb3587a2331b1017810ddda to your computer and use it in GitHub Desktop.
Save jongan69/9d1e248febb3587a2331b1017810ddda to your computer and use it in GitHub Desktop.
Example Solana Rust Lottery Program
[package]
name = "lottery"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "lottery"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = "0.30.1"
anchor-spl = "0.30.1"
// Import necessary modules from Anchor and Solana
use anchor_lang::prelude::*;
use anchor_lang::system_program;
// Declare the program ID - this is the unique identifier for this Solana program
declare_id!("GUjB8G7uqJ9yhfSfNdWNhyF7gYsrpmJN3dHSEd7J6ZhP");
// Add this after declare_id!()
pub const LOTTERY_SEED: &[u8] = b"lottery";
#[program]
pub mod lottery {
use super::*;
// Initialize a new lottery with an entry fee and end time
// This function is called once to set up the lottery
pub fn initialize(ctx: Context<Initialize>, entry_fee: u64, end_time: i64) -> Result<()> {
let lottery = &mut ctx.accounts.lottery;
// Store the admin's public key (the person who created the lottery)
lottery.admin = ctx.accounts.admin.key();
// Set the cost to enter the lottery
lottery.entry_fee = entry_fee;
// Set when the lottery will end (as a Unix timestamp)
lottery.end_time = end_time;
// Initialize ticket count to 0
lottery.total_tickets = 0;
// No winner yet, so set to None
lottery.winner = None;
msg!("Lottery Initialized!");
Ok(())
}
// Function to purchase a lottery ticket
pub fn buy_ticket(ctx: Context<BuyTicket>) -> Result<()> {
require!(
Clock::get().unwrap().unix_timestamp <= ctx.accounts.lottery.end_time,
LotteryError::LotteryClosed
);
require!(
ctx.accounts.lottery.winner.is_none(),
LotteryError::WinnerAlreadySelected
);
require!(
ctx.accounts.lottery.participants.len() < 100,
LotteryError::MaxParticipantsReached
);
let entry_fee = ctx.accounts.lottery.entry_fee;
// Create a CPI (Cross-Program Invocation) to transfer SOL from player to lottery account
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.player.to_account_info(),
to: ctx.accounts.lottery.to_account_info(),
},
);
// Execute the transfer
system_program::transfer(cpi_context, entry_fee)?;
// Update the lottery state with new participant
let lottery = &mut ctx.accounts.lottery;
// Add player's public key to participants list
lottery.participants.push(ctx.accounts.player.key());
// Increment total ticket count
lottery.total_tickets += 1;
msg!("Ticket purchased by: {:?}", ctx.accounts.player.key());
Ok(())
}
// Function to select a winner after lottery ends
pub fn select_winner(ctx: Context<SelectWinner>) -> Result<()> {
let lottery = &mut ctx.accounts.lottery;
require!(
Clock::get().unwrap().unix_timestamp > lottery.end_time,
LotteryError::LotteryNotEnded
);
require!(lottery.winner.is_none(), LotteryError::WinnerAlreadySelected);
require!(!lottery.participants.is_empty(), LotteryError::NoParticipants);
// Winner selection happens in callback
Ok(())
}
// Function for winner to claim their prize (90% of collected SOL) and reset lottery
pub fn claim_prize(ctx: Context<ClaimPrize>) -> Result<()> {
// Verify winner
require!(
Some(ctx.accounts.player.key()) == ctx.accounts.lottery.winner,
LotteryError::NotWinner
);
// Calculate prize amount (90% of total collected fees)
let total_collected = ctx.accounts.lottery.entry_fee
.checked_mul(ctx.accounts.lottery.total_tickets as u64)
.ok_or(LotteryError::Overflow)?;
let prize_amount = total_collected
.checked_mul(90)
.ok_or(LotteryError::Overflow)?
.checked_div(100)
.ok_or(LotteryError::Overflow)?;
**ctx.accounts.lottery.to_account_info().try_borrow_mut_lamports()? -= prize_amount;
**ctx.accounts.player.to_account_info().try_borrow_mut_lamports()? += prize_amount;
// Reset lottery state after transfer
let lottery = &mut ctx.accounts.lottery;
lottery.total_tickets = 0;
lottery.participants.clear();
lottery.winner = None;
msg!("Prize of {} lamports claimed by: {:?}", prize_amount, ctx.accounts.player.key());
msg!("Lottery has been reset for the next round");
Ok(())
}
}
// Account validation struct for Initialize instruction
#[derive(Accounts)]
pub struct Initialize<'info> {
// Create new lottery account, paid for by admin
#[account(
init,
payer = admin,
space = 8 + LotteryState::LEN,
seeds = [LOTTERY_SEED],
bump
)]
pub lottery: Account<'info, LotteryState>,
// Admin must sign this transaction and pay for account creation
#[account(mut)]
pub admin: Signer<'info>,
// Required for creating new accounts
pub system_program: Program<'info, System>,
}
// Account validation struct for BuyTicket instruction
#[derive(Accounts)]
pub struct BuyTicket<'info> {
// Lottery account to be modified
#[account(
mut,
seeds = [LOTTERY_SEED],
bump
)]
pub lottery: Account<'info, LotteryState>,
// Player must sign transaction and pay entry fee
#[account(mut)]
pub player: Signer<'info>,
// Required for SOL transfers
pub system_program: Program<'info, System>,
}
// Account validation struct for SelectWinner instruction
#[derive(Accounts)]
pub struct SelectWinner<'info> {
// Lottery account to be modified
#[account(
mut,
seeds = [LOTTERY_SEED],
bump
)]
pub lottery: Account<'info, LotteryState>
}
// Account validation struct for ClaimPrize instruction
#[derive(Accounts)]
pub struct ClaimPrize<'info> {
// Lottery account to verify winner and transfer funds from
#[account(
mut,
seeds = [LOTTERY_SEED],
bump,
constraint = lottery.winner.is_some() @ LotteryError::NoWinnerSelected,
constraint = lottery.winner.unwrap() == player.key() @ LotteryError::NotWinner,
)]
pub lottery: Account<'info, LotteryState>,
// Winner must sign to claim prize
#[account(mut)]
pub player: Signer<'info>,
// Required for SOL transfers
pub system_program: Program<'info, System>,
}
// Main state account storing all lottery data
#[account]
pub struct LotteryState {
// Admin's public key
pub admin: Pubkey,
// Cost to enter lottery
pub entry_fee: u64,
// Number of tickets sold
pub total_tickets: u32,
// List of participant public keys
pub participants: Vec<Pubkey>,
// When lottery ends (Unix timestamp)
pub end_time: i64,
// Winner's public key (if selected)
pub winner: Option<Pubkey>,
}
// Calculate required space for LotteryState account
impl LotteryState {
// 32 (pubkey) + 8 (u64) + 4 (u32) + (32 * 100) (vec of pubkeys) + 8 (i64) + 1 (option)
const LEN: usize = 32 + 8 + 4 + (32 * 100) + 8 + 1; // Adjust 100 based on max participants.
}
// Custom error codes for the program
#[error_code]
pub enum LotteryError {
#[msg("The lottery has already ended.")]
LotteryClosed,
#[msg("The lottery has not ended yet.")]
LotteryNotEnded,
#[msg("A winner has already been selected.")]
WinnerAlreadySelected,
#[msg("You are not the winner.")]
NotWinner,
#[msg("Arithmetic overflow occurred.")]
Overflow,
#[msg("No participants in the lottery.")]
NoParticipants,
#[msg("Maximum participants reached.")]
MaxParticipantsReached,
#[msg("No winner selected.")]
NoWinnerSelected,
}
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Lottery } from "../target/types/lottery";
import { Keypair, LAMPORTS_PER_SOL, SystemProgram } from "@solana/web3.js";
import { assert } from "chai";
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
describe("lottery", () => {
// Configure the client to use the local cluster
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Lottery as Program<Lottery>;
const provider = anchor.getProvider();
// Use the default wallet as admin
const admin = {
publicKey: (provider as anchor.AnchorProvider).wallet.publicKey,
signTransaction: (provider as anchor.AnchorProvider).wallet.signTransaction.bind((provider as anchor.AnchorProvider).wallet)
};
// Test accounts - we'll transfer SOL to these from admin
const player1 = Keypair.generate();
const player2 = Keypair.generate();
const player3 = Keypair.generate();
// Test state
const entryFee = new anchor.BN(0.1 * LAMPORTS_PER_SOL); // 0.1 SOL
const [lotteryPDA] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("lottery")],
program.programId
);
// Helper function to get confirmation strategy
const getConfirmationStrategy = async (signature: string) => {
const { blockhash, lastValidBlockHeight } = await provider.connection.getLatestBlockhash();
return {
signature,
blockhash,
lastValidBlockHeight,
};
};
// Update transferSol function
const transferSol = async (from: { publicKey: anchor.web3.PublicKey, signTransaction: (tx: anchor.web3.Transaction) => Promise<anchor.web3.Transaction> },
to: Keypair,
amount: number) => {
const tx = new anchor.web3.Transaction();
const latestBlockhash = await provider.connection.getLatestBlockhash();
tx.recentBlockhash = latestBlockhash.blockhash;
tx.feePayer = from.publicKey;
tx.add(
SystemProgram.transfer({
fromPubkey: from.publicKey,
toPubkey: to.publicKey,
lamports: amount,
})
);
const signedTx = await from.signTransaction(tx);
const txid = await provider.connection.sendRawTransaction(signedTx.serialize());
await provider.connection.confirmTransaction(await getConfirmationStrategy(txid));
};
beforeEach(async () => {
try {
// Only airdrop if admin balance is low
const adminBalance = await provider.connection.getBalance(admin.publicKey);
if (adminBalance < LAMPORTS_PER_SOL) {
await provider.connection.requestAirdrop(admin.publicKey, 2 * LAMPORTS_PER_SOL);
await sleep(1000);
}
// Transfer smaller amounts to test accounts
const FUND_AMOUNT = 0.2 * LAMPORTS_PER_SOL;
for (const account of [player1, player2, player3]) {
const balance = await provider.connection.getBalance(account.publicKey);
if (balance < FUND_AMOUNT) {
await transferSol(admin, account, FUND_AMOUNT);
await sleep(500);
}
}
} catch (error) {
console.error("Setup error:", error);
throw error;
}
});
it("Initializes the lottery", async () => {
// Clear any existing state
try {
await program.account.lotteryState.fetch(lotteryPDA);
// If we get here, the account exists and we should skip initialization
return;
} catch {
// Account doesn't exist, proceed with initialization
const now = Math.floor(Date.now() / 1000);
const endTime = new anchor.BN(now + 10);
const tx = await program.methods
.initialize(entryFee, endTime)
.accounts({
lottery: lotteryPDA,
admin: admin.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([])
.rpc();
await provider.connection.confirmTransaction(await getConfirmationStrategy(tx));
const lotteryAccount = await program.account.lotteryState.fetch(lotteryPDA);
assert.equal(lotteryAccount.admin.toBase58(), admin.publicKey.toBase58());
assert.equal(lotteryAccount.entryFee.toString(), entryFee.toString());
assert.equal(lotteryAccount.totalTickets, 0);
assert.equal(lotteryAccount.participants.length, 0);
}
});
it("Allows players to buy tickets", async () => {
// Clear participants array first
const lotteryBefore = await program.account.lotteryState.fetch(lotteryPDA);
console.log("\nCurrent lottery state:");
console.log("Total tickets:", lotteryBefore.totalTickets);
console.log("Participants:", lotteryBefore.participants.map(p => p.toBase58()));
if (lotteryBefore.totalTickets > 0) {
return; // Skip if tickets already purchased
}
// Get initial balances
const player1InitialBalance = await provider.connection.getBalance(player1.publicKey);
const player2InitialBalance = await provider.connection.getBalance(player2.publicKey);
const lotteryInitialBalance = await provider.connection.getBalance(lotteryPDA);
console.log("\nInitial Balances:");
console.log(Player 1: ${player1InitialBalance / LAMPORTS_PER_SOL} SOL);
console.log(Player 2: ${player2InitialBalance / LAMPORTS_PER_SOL} SOL);
console.log(Lottery: ${lotteryInitialBalance / LAMPORTS_PER_SOL} SOL);
// Player 1 buys a ticket
const tx1 = await program.methods
.buyTicket()
.accounts({
lottery: lotteryPDA,
player: player1.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([player1])
.rpc();
await provider.connection.confirmTransaction(await getConfirmationStrategy(tx1));
const player1BalanceAfterBuy = await provider.connection.getBalance(player1.publicKey);
console.log(\nPlayer 1 balance after buying ticket: ${player1BalanceAfterBuy / LAMPORTS_PER_SOL} SOL);
console.log(Cost of ticket: ${(player1InitialBalance - player1BalanceAfterBuy) / LAMPORTS_PER_SOL} SOL);
// Player 2 buys a ticket
const tx2 = await program.methods
.buyTicket()
.accounts({
lottery: lotteryPDA,
player: player2.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([player2])
.rpc();
await provider.connection.confirmTransaction(await getConfirmationStrategy(tx2));
const player2BalanceAfterBuy = await provider.connection.getBalance(player2.publicKey);
console.log(Player 2 balance after buying ticket: ${player2BalanceAfterBuy / LAMPORTS_PER_SOL} SOL);
console.log(Cost of ticket: ${(player2InitialBalance - player2BalanceAfterBuy) / LAMPORTS_PER_SOL} SOL);
// Verify lottery state
const lotteryAccount = await program.account.lotteryState.fetch(lotteryPDA);
const lotteryFinalBalance = await provider.connection.getBalance(lotteryPDA);
console.log(\nLottery final balance: ${lotteryFinalBalance / LAMPORTS_PER_SOL} SOL);
console.log(Total collected: ${(lotteryFinalBalance - lotteryInitialBalance) / LAMPORTS_PER_SOL} SOL);
assert.equal(lotteryAccount.totalTickets, 2);
assert.equal(lotteryAccount.participants.length, 2);
});
it("Selects a winner after the end time", async () => {
const lotteryBefore = await program.account.lotteryState.fetch(lotteryPDA);
if (lotteryBefore.winner !== null) {
return; // Skip if winner already selected
}
// Wait for the lottery to end (10 seconds + 2 seconds buffer)
await new Promise((resolve) => setTimeout(resolve, 12000));
const tx = await program.methods
.selectWinner()
.accounts({
lottery: lotteryPDA,
})
.rpc();
await provider.connection.confirmTransaction(await getConfirmationStrategy(tx));
const lotteryAccount = await program.account.lotteryState.fetch(lotteryPDA);
assert.isNotNull(lotteryAccount.winner);
});
it("Allows winner to claim prize", async () => {
const lotteryAccount = await program.account.lotteryState.fetch(lotteryPDA);
const winner = lotteryAccount.winner;
if (!winner) {
console.log("No winner selected yet, skipping claim test");
return;
}
// Debug prints
console.log("\nParticipants in lottery:", lotteryAccount.participants.map(p => p.toBase58()));
console.log("Selected winner:", winner.toBase58());
console.log("Player 1:", player1.publicKey.toBase58());
console.log("Player 2:", player2.publicKey.toBase58());
// Find which player is the winner
let winningPlayer;
if (winner.equals(player1.publicKey)) {
winningPlayer = player1;
console.log("Winner is Player 1");
} else if (winner.equals(player2.publicKey)) {
winningPlayer = player2;
console.log("Winner is Player 2");
} else {
console.log("Winner public key matches neither player!");
console.log("Winner:", winner.toBase58());
console.log("Player1:", player1.publicKey.toBase58());
console.log("Player2:", player2.publicKey.toBase58());
throw new Error("Winner is neither player1 nor player2!");
}
// Calculate expected prize (90% of total collected fees)
const totalCollected = entryFee.mul(new anchor.BN(lotteryAccount.totalTickets));
const expectedPrize = totalCollected.mul(new anchor.BN(90)).div(new anchor.BN(100));
console.log(\nTotal collected: ${totalCollected.toNumber() / LAMPORTS_PER_SOL} SOL);
console.log(Expected prize: ${expectedPrize.toNumber() / LAMPORTS_PER_SOL} SOL);
// Get initial balances
const winnerInitialBalance = await provider.connection.getBalance(winningPlayer.publicKey);
const lotteryInitialBalance = await provider.connection.getBalance(lotteryPDA);
console.log(\nInitial balances:);
console.log(Winner: ${winnerInitialBalance / LAMPORTS_PER_SOL} SOL);
console.log(Lottery: ${lotteryInitialBalance / LAMPORTS_PER_SOL} SOL);
// Try with the actual winner
console.log("\nAttempting to claim with winning player...");
await program.methods
.claimPrize()
.accounts({
lottery: lotteryPDA,
player: winningPlayer.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([winningPlayer])
.rpc();
// Verify the prize transfer
const winnerFinalBalance = await provider.connection.getBalance(winningPlayer.publicKey);
const lotteryFinalBalance = await provider.connection.getBalance(lotteryPDA);
console.log(\nFinal balances:);
console.log(Winner: ${winnerFinalBalance / LAMPORTS_PER_SOL} SOL);
console.log(Lottery: ${lotteryFinalBalance / LAMPORTS_PER_SOL} SOL);
console.log(Prize claimed: ${(winnerFinalBalance - winnerInitialBalance) / LAMPORTS_PER_SOL} SOL);
assert.approximately(
winnerFinalBalance - winnerInitialBalance,
expectedPrize.toNumber(),
1000000 // Allow for small difference due to transaction fees
);
assert.approximately(
lotteryInitialBalance - lotteryFinalBalance,
expectedPrize.toNumber(),
1000000
);
// After verifying prize transfer, check that lottery was reset
const lotteryAfterReset = await program.account.lotteryState.fetch(lotteryPDA);
console.log("\nVerifying lottery reset:");
console.log("Total tickets:", lotteryAfterReset.totalTickets);
console.log("Participants:", lotteryAfterReset.participants.length);
console.log("Winner:", lotteryAfterReset.winner);
assert.equal(lotteryAfterReset.totalTickets, 0, "Total tickets should be reset to 0");
assert.equal(lotteryAfterReset.participants.length, 0, "Participants should be cleared");
assert.isNull(lotteryAfterReset.winner, "Winner should be reset to null");
// Try with the losing player (should fail)
const losingPlayer = winningPlayer.publicKey.equals(player1.publicKey) ? player2 : player1;
console.log("\nAttempting to claim with losing player (should fail)...");
try {
await program.methods
.claimPrize()
.accounts({
lottery: lotteryPDA,
player: losingPlayer.publicKey,
systemProgram: SystemProgram.programId,
})
.signers([losingPlayer])
.rpc();
assert.fail("Non-winner was able to claim prize");
} catch (error) {
console.log("Correctly failed for non-winner");
}
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment