use { anchor_lang::{ solana_program::{ instruction::Instruction, pubkey::Pubkey, system_instruction, }, AccountDeserialize, InstructionData, ToAccountMetas, }, smart_updown::state::{Bet, Global}, solana_clock::Clock, solana_keypair::Keypair, solana_program_test::{BanksClient, ProgramTest, ProgramTestContext}, solana_signer::Signer, solana_transaction::Transaction, }; 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; const START_TIME: i64 = 1_700_000_000; fn pid() -> Pubkey { smart_updown::id() } fn global_pda() -> Pubkey { Pubkey::find_program_address(&[b"global"], &pid()).0 } fn bet_pda(id: u64) -> Pubkey { Pubkey::find_program_address(&[b"bet", &id.to_le_bytes()], &pid()).0 } async fn setup() -> (ProgramTestContext, Keypair) { let mut pt = ProgramTest::default(); pt.add_program("smart_updown", pid(), None); // Fund the deploy keypair separately in the test; the validator in CI funds it. let context = pt.start_with_context().await; let admin = Keypair::new(); context.banks_client.process_transaction(Transaction::new_signed_with_payer( &[system_instruction::transfer( &context.payer.pubkey(), &admin.pubkey(), 100_000_000_000, )], Some(&context.payer.pubkey()), &[&context.payer], context.last_blockhash, )).await.unwrap(); (context, admin) } async fn set_clock(ctx: &mut ProgramTestContext, unix: i64) { let mut clk: Clock = ctx.banks_client.get_sysvar().await.unwrap(); clk.unix_timestamp = unix; ctx.set_sysvar(&clk); } async fn read_account_data( bank: &BanksClient, addr: &Pubkey, ) -> Vec { bank.get_account(*addr).await.unwrap().expect("account exists").data } fn read_global_typed(data: &[u8]) -> Global { Global::try_deserialize(&mut &data[..]).unwrap() } fn read_bet_typed(data: &[u8]) -> Bet { Bet::try_deserialize(&mut &data[..]).unwrap() } fn make_initialize_ix(admin: Pubkey) -> Instruction { 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, global: global_pda(), system_program: SYSTEM_ADDR.parse().unwrap(), } .to_account_metas(None), ) } fn make_create_ix(id: u64, side: u64, amount: u64, entry_price: u64, bettor: Pubkey) -> 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: Pubkey, bettor: Pubkey) -> 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), ) } async fn run_ix( ctx: &mut ProgramTestContext, signer: &Keypair, ix: Instruction, ) -> Result<(), String> { let tx = Transaction::new_signed_with_payer( &[ix], Some(&signer.pubkey()), &[signer], ctx.last_blockhash, ); ctx.banks_client .process_transaction(tx) .await .map(|_| ()) .map_err(|e| format!("{:?}", e)) } async fn initialize(ctx: &mut ProgramTestContext, admin: &Keypair) { run_ix(ctx, admin, make_initialize_ix(admin.pubkey())) .await .expect("initialize"); } async fn airdrop_to(ctx: &mut ProgramTestContext, to: &Pubkey, lamports: u64) { let tx = Transaction::new_signed_with_payer( &[system_instruction::transfer( &ctx.payer.pubkey(), to, lamports, )], Some(&ctx.payer.pubkey()), &[&ctx.payer], ctx.last_blockhash, ); ctx.banks_client.process_transaction(tx).await.unwrap(); } async fn fund_vault(ctx: &mut ProgramTestContext, lamports: u64) { airdrop_to(ctx, &global_pda(), lamports).await; } async fn create_up_bet( ctx: &mut ProgramTestContext, side: u64, ) -> (Keypair, u64, u64) { let bettor = Keypair::new(); airdrop_to(ctx, &bettor.pubkey(), 100_000_000_000).await; set_clock(ctx, START_TIME).await; let amount = MIN_AMOUNT; let entry_price = 100_000_000u64; let create_ix = make_create_ix(0, side, amount, entry_price, bettor.pubkey()); run_ix(ctx, &bettor, create_ix).await.expect("create_bet"); (bettor, amount, entry_price) } #[tokio::test] async fn initialize_creates_global() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let acc = read_account_data(&ctx.banks_client, &global_pda()).await; let g = read_global_typed(&acc); 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); } #[tokio::test] async fn create_bet_escrows_sol() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await; let vault_balance = ctx.banks_client.get_balance(global_pda()).await.unwrap(); assert!(vault_balance >= amount); let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await); 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, START_TIME); assert_eq!(b.expire_time, START_TIME + EXPIRY_SECONDS); assert_eq!(b.status, 0); assert_eq!(read_global_typed(&read_account_data(&ctx.banks_client, &global_pda()).await).counter, 1); } #[tokio::test] async fn close_bet_user_wins_up() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await; set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await; let exit_price = entry_price + 1_000; let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(); let payout = amount * MULTIPLIER_BPS / 10_000; fund_vault(&mut ctx, 10 * amount).await; let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey()); run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet"); assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before + payout); let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await); assert_eq!(b.status, 1); assert_eq!(b.exit_price, exit_price); } #[tokio::test] async fn close_bet_house_wins_down() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 1).await; set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await; let exit_price = entry_price + 1_000; // side=DOWN but price rose -> lose let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(); let close_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey()); run_ix(&mut ctx, &admin, close_ix).await.expect("close_bet"); assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before); assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 2); } #[tokio::test] async fn close_bet_equal_price_stays_open() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await; set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await; let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(); let payout = amount * MULTIPLIER_BPS / 10_000; fund_vault(&mut ctx, 10 * amount).await; let equal_ix = make_close_ix(0, entry_price, admin.pubkey(), bettor.pubkey()); run_ix(&mut ctx, &admin, equal_ix).await.expect("equal close"); assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0); assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before); let exit_price = entry_price + 1_000; let second_ix = make_close_ix(0, exit_price, admin.pubkey(), bettor.pubkey()); run_ix(&mut ctx, &admin, second_ix).await.expect("second close"); assert_eq!( ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before + payout ); assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 1); } #[tokio::test] async fn close_bet_already_finalized() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, amount, entry_price) = create_up_bet(&mut ctx, 0).await; set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await; let win_price = entry_price + 500; fund_vault(&mut ctx, 10 * amount).await; run_ix(&mut ctx, &admin, make_close_ix(0, win_price, admin.pubkey(), bettor.pubkey())) .await .expect("first close"); let res = run_ix( &mut ctx, &admin, make_close_ix(0, win_price + 1, admin.pubkey(), bettor.pubkey()), ) .await; assert!(res.is_err(), "expected close after finalization to be rejected"); let b = read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await); assert_eq!(b.status, 1); assert_eq!(b.exit_price, win_price); assert_eq!(b.status, 1); assert_eq!(b.exit_price, win_price); } #[tokio::test] async fn close_bet_before_expiry_rejected() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await; let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey()); let res = run_ix(&mut ctx, &admin, close_ix).await; assert!(res.is_err(), "expected premature close to be rejected"); assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0); } #[tokio::test] async fn close_bet_wrong_relayer_rejected() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await; let attacker = Keypair::new(); airdrop_to(&mut ctx, &attacker.pubkey(), 100_000_000_000).await; set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await; let close_ix = make_close_ix(0, entry_price + 500, attacker.pubkey(), bettor.pubkey()); let res = run_ix(&mut ctx, &attacker, close_ix).await; assert!(res.is_err(), "expected wrong relayer to be rejected"); assert_eq!(read_bet_typed(&read_account_data(&ctx.banks_client, &bet_pda(0)).await).status, 0); } #[tokio::test] async fn close_bet_replay_rejected() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let (bettor, _amount, entry_price) = create_up_bet(&mut ctx, 0).await; set_clock(&mut ctx, START_TIME + EXPIRY_SECONDS + 1).await; fund_vault(&mut ctx, 10 * MIN_AMOUNT).await; let close_ix = make_close_ix(0, entry_price + 500, admin.pubkey(), bettor.pubkey()); run_ix(&mut ctx, &admin, close_ix).await.expect("first close"); let before = ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(); let res = run_ix( &mut ctx, &admin, make_close_ix(0, entry_price + 501, admin.pubkey(), bettor.pubkey()), ) .await; assert!(res.is_err(), "expected replay close to be rejected"); assert_eq!(ctx.banks_client.get_balance(bettor.pubkey()).await.unwrap(), before); } #[tokio::test] async fn create_bet_rejects_out_of_range() { let (mut ctx, admin) = setup().await; initialize(&mut ctx, &admin).await; let bettor = Keypair::new(); airdrop_to(&mut ctx, &bettor.pubkey(), 100_000_000_000).await; set_clock(&mut ctx, START_TIME).await; let ix = make_create_ix(0, 0, 1, 100_000_000u64, bettor.pubkey()); let res = run_ix(&mut ctx, &bettor, ix).await; assert!(res.is_err(), "expected below-min bet to be rejected"); }