use { anchor_lang::{ bytemuck, cpi::find_program_address, solana_program::instruction::Instruction, Id, InstructionData, ToAccountMetas, }, anchor_v2_testing::{Keypair, LiteSVM, Message, Signer, VersionedMessage, VersionedTransaction}, smart_updown::state::{Bet, Global}, }; const CLOCK_ADDR: &str = "SysvarC1ock11111111111111111111111111111111"; const SYSTEM_ADDR: &str = "11111111111111111111111111111111"; const MIN_AMOUNT: u64 = 1_000_000_000; // 1 SOL const MULTIPLIER_BPS: u64 = 13_000; // 1.3x const EXPIRY_SECONDS: i64 = 15; type Addr = anchor_lang::prelude::Address; fn pid() -> Addr { smart_updown::id() } fn global_pda() -> Addr { find_program_address(&[b"global"], &pid()).0 } fn bet_pda(id: u64) -> Addr { find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0 } fn setup() -> (LiteSVM, Keypair) { let mut svm = LiteSVM::new(); let bytes = include_bytes!("../../../target/deploy/smart_updown.so"); svm.add_program(pid(), bytes).expect("deploy program"); let payer = Keypair::new(); svm.airdrop(&payer.pubkey(), 100_000_000_000).unwrap(); (svm, payer) } fn send(svm: &mut LiteSVM, signers: &[&Keypair], instruction: Instruction) -> Result<(), String> { let blockhash = svm.latest_blockhash(); let msg = Message::new_with_blockhash(&[instruction], Some(&signers[0].pubkey()), &blockhash); let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), signers).unwrap(); let res = svm.send_transaction(tx); res.map(|_| ()).map_err(|e| format!("{:?}", e)) } fn initialize(svm: &mut LiteSVM, admin: &Keypair) { let ix = Instruction::new_with_bytes( pid(), &smart_updown::instruction::Initialize { min_amount: MIN_AMOUNT, max_amount: 1_000_000_000_000, multiplier_bps: MULTIPLIER_BPS, expiry_seconds: EXPIRY_SECONDS, } .data(), smart_updown::accounts::Initialize { admin: admin.pubkey(), global: global_pda(), system_program: SYSTEM_ADDR.parse().unwrap(), } .to_account_metas(None), ); send(svm, &[admin], ix).expect("initialize"); } fn set_clock(svm: &mut LiteSVM, unix: i64) { use solana_clock::Clock; let mut clk = svm.get_sysvar::(); clk.unix_timestamp = unix; svm.set_sysvar(&clk); } fn fund_vault(svm: &mut LiteSVM, lamports: u64) { svm.airdrop(&global_pda(), lamports).unwrap(); } fn read_global(svm: &LiteSVM) -> Global { let acc = svm.get_account(&global_pda()).expect("global account"); *bytemuck::from_bytes::(&acc.data[8..]) } fn read_bet(svm: &LiteSVM, id: u64) -> Bet { let acc = svm.get_account(&bet_pda(id)).expect("bet account"); *bytemuck::from_bytes::(&acc.data[8..]) } fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Addr) -> Instruction { Instruction::new_with_bytes( pid(), &smart_updown::instruction::CreateBet { id, side, amount, entry_price, } .data(), smart_updown::accounts::CreateBet { bettor, global: global_pda(), bet: bet_pda(id), clock: CLOCK_ADDR.parse().unwrap(), system_program: SYSTEM_ADDR.parse().unwrap(), } .to_account_metas(None), ) } fn make_close_ix(id: u64, exit_price: u64, relayer: Addr, bettor: Addr) -> Instruction { Instruction::new_with_bytes( pid(), &smart_updown::instruction::CloseBet { id, exit_price }.data(), smart_updown::accounts::CloseBet { relayer, bet: bet_pda(id), global: global_pda(), bettor, clock: CLOCK_ADDR.parse().unwrap(), } .to_account_metas(None), ) } fn create_up_bet(svm: &mut LiteSVM, admin: &Keypair, side: u64) -> (Keypair, u64, u64) { let bettor = Keypair::new(); svm.airdrop(&bettor.pubkey(), 100_000_000_000).unwrap(); set_clock(svm, 1_700_000_000); let amount = MIN_AMOUNT; let entry_price = 100_000_000u64; let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey()); send(svm, &[&bettor], create_ix).expect("create_bet"); (bettor, amount, entry_price) } #[test] fn initialize_creates_global() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let g = read_global(&svm); assert_eq!(g.admin, admin.pubkey()); assert_eq!(g.relayer, admin.pubkey()); assert_eq!(g.counter, 0); assert_eq!(g.multiplier_bps, MULTIPLIER_BPS); assert_eq!(g.expiry_seconds, EXPIRY_SECONDS); assert_eq!(g.paused, 0); } #[test] fn create_bet_escrows_sol() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0); let vault_balance = svm.get_balance(&global_pda()).unwrap(); assert!(vault_balance >= amount); let b = read_bet(&svm, 0); assert_eq!(b.bettor, bettor.pubkey()); assert_eq!(b.side, 0); assert_eq!(b.amount, amount); assert_eq!(b.entry_price, entry_price); assert_eq!(b.bet_time, 1_700_000_000); assert_eq!(b.expire_time, 1_700_000_000 + EXPIRY_SECONDS); assert_eq!(b.status, 0); assert_eq!(read_global(&svm).counter, 1); } #[test] fn close_bet_user_wins_up() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0); set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1); let exit_price = entry_price + 1_000; let before = svm.get_balance(&bettor.pubkey()).unwrap(); let payout = amount * MULTIPLIER_BPS / 10_000; fund_vault(&mut svm, 10 * amount); let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey()); send(&mut svm, &[&admin], close_ix).expect("close_bet"); assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before + payout); let b = read_bet(&svm, 0); assert_eq!(b.status, 1); assert_eq!(b.exit_price, exit_price); } #[test] fn close_bet_house_wins_down() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 1); set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1); let exit_price = entry_price + 1_000; // side=DOWN but price rose -> lose let before = svm.get_balance(&bettor.pubkey()).unwrap(); let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey()); send(&mut svm, &[&admin], close_ix).expect("close_bet"); assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before); assert_eq!(read_bet(&svm, 0).status, 2); } #[test] fn close_bet_equal_price_stays_open() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let (bettor, amount, entry_price) = create_up_bet(&mut svm, &admin, 0); set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1); let before = svm.get_balance(&bettor.pubkey()).unwrap(); let payout = amount * MULTIPLIER_BPS / 10_000; fund_vault(&mut svm, 10 * amount); let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey()); send(&mut svm, &[&admin], equal_ix).expect("equal close"); assert_eq!(read_bet(&svm, 0).status, 0); assert_eq!(svm.get_balance(&bettor.pubkey()).unwrap(), before); let exit_price = entry_price + 1_000; let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey()); send(&mut svm, &[&admin], second_ix).expect("second close"); assert_eq!( svm.get_balance(&bettor.pubkey()).unwrap(), before + payout ); assert_eq!(read_bet(&svm, 0).status, 1); } #[test] fn close_bet_before_expiry_rejected() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0); let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey()); let res = send(&mut svm, &[&admin], close_ix); assert!(res.is_err(), "expected premature close to be rejected"); assert_eq!(read_bet(&svm, 0).status, 0); } #[test] fn close_bet_wrong_relayer_rejected() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0); let attacker = Keypair::new(); svm.airdrop(&attacker.pubkey(), 100_000_000_000).unwrap(); set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1); let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey()); let res = send(&mut svm, &[&attacker], close_ix); assert!(res.is_err(), "expected wrong relayer to be rejected"); assert_eq!(read_bet(&svm, 0).status, 0); } #[test] fn close_bet_replay_rejected() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let (bettor, _amount, entry_price) = create_up_bet(&mut svm, &admin, 0); set_clock(&mut svm, 1_700_000_000 + EXPIRY_SECONDS + 1); fund_vault(&mut svm, 10 * MIN_AMOUNT); let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey()); send(&mut svm, &[&admin], close_ix).expect("first close"); let res = send(&mut svm, &[&admin], make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey())); assert!(res.is_err(), "expected replay close to be rejected"); } #[test] fn create_bet_rejects_out_of_range() { let (mut svm, admin) = setup(); initialize(&mut svm, &admin); let bettor = Keypair::new(); svm.airdrop(&bettor.pubkey(), 100_000_000_000).unwrap(); set_clock(&mut svm, 1_700_000_000); let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey()); let res = send(&mut svm, &[&bettor], ix); assert!(res.is_err(), "expected below-min bet to be rejected"); }