use { bincode::deserialize, solana_program_test::{processor, BanksClientError, ProgramTest, ProgramTestContext}, solana_sdk::{ clock::{Clock, UnixTimestamp}, commitment_config::CommitmentLevel, instruction::{Instruction, InstructionError}, program_option::COption, program_pack::Pack, pubkey::Pubkey, rent::*, signature::{Keypair, Signer}, system_instruction, sysvar, transaction::{Transaction, TransactionError}, }, spl_associated_token_account::get_associated_token_address_with_program_id, spl_token::{state::Mint, ID as SPL_TOKEN_PROGRAM_ID}, stablebond_sdk::{ errors::StablebondError, types::{PaymentFeedInfo, PaymentFeedType}, STABLEBOND_ID, }, std::{ borrow::Borrow, time::{SystemTime, UNIX_EPOCH}, }, }; const RUST_LOG_DEFAULT: &str = "solana_rbpf::vm=info,\ solana_program_runtime::stable_log=debug,\ solana_runtime::message_processor=debug,\ solana_runtime::system_instruction_processor=info,\ solana_program_test=info,\ solana_bpf_loader_program=debug"; pub trait AddPacked { fn add_packable_account( &mut self, pubkey: Pubkey, amount: u64, data: &T, owner: &Pubkey, ); } impl AddPacked for ProgramTest { fn add_packable_account( &mut self, pubkey: Pubkey, amount: u64, data: &T, owner: &Pubkey, ) { let mut account = solana_sdk::account::Account::new(amount, T::get_packed_len(), owner); data.pack_into_slice(&mut account.data); self.add_account(pubkey, account); } } #[derive(Copy, Clone)] pub struct BondCookie { pub index: usize, pub unit: f64, pub payment_mint: Pubkey, } impl Default for BondCookie { fn default() -> Self { Self { index: 0, unit: 10u64.pow(6) as f64, payment_mint: Keypair::new().pubkey(), } } } pub struct StablebondProgramTest { pub context: ProgramTestContext, pub rent: Rent, pub admin: Keypair, pub delegate: Keypair, pub nft_collection_mint: Keypair, pub mints: Vec, pub bond_mints: Vec, pub num_mints: usize, pub num_users: usize, pub users: Vec, pub payment_token_accounts: Vec, pub usdc_mxn_payment_feed: PaymentFeedInfo, pub usdc_usd_payment_feed: PaymentFeedInfo, pub stub_payment_feed: PaymentFeedInfo, } impl StablebondProgramTest { #[allow(dead_code)] pub async fn start_new() -> Self { // Predefined mints, maybe can even add symbols to them let mints: Vec = vec![ BondCookie { index: 0, unit: 10u64.pow(6) as f64, ..Default::default() }, BondCookie { index: 1, unit: 10u64.pow(6) as f64, ..Default::default() }, BondCookie { index: 2, unit: 10u64.pow(6) as f64, ..Default::default() }, BondCookie { index: 3, unit: 10u64.pow(6) as f64, ..Default::default() }, ]; solana_logger::setup_with_default(RUST_LOG_DEFAULT); let mut pt = ProgramTest::new("stablebond", STABLEBOND_ID, None); pt.add_program( "spl_token_2022", spl_token_2022::id(), processor!(spl_token_2022::processor::Processor::process), ); pt.add_program( "spl_associated_token_account", spl_associated_token_account::id(), processor!(spl_associated_token_account::processor::process_instruction), ); pt.add_program( "spl_token", SPL_TOKEN_PROGRAM_ID, processor!(spl_token::processor::Processor::process), ); pt.add_program("mpl_token_metadata", mpl_token_metadata::ID, None); pt.set_compute_max_units(650_000); pt.prefer_bpf(false); // Generate bond mint keypairs let mut bond_mints = Vec::new(); for _ in 0..mints.len() { let bond_mint = Keypair::new(); bond_mints.push(bond_mint); } // Add payment mints to the ProgramTest for mint in mints.iter() { pt.add_packable_account( mint.payment_mint, u32::MAX as u64, &Mint { is_initialized: true, mint_authority: COption::Some(Pubkey::new_unique()), decimals: 6, ..Mint::default() }, &spl_token::id(), ); } let admin = Keypair::new(); let delegate = Keypair::new(); let nft_collection_mint = Keypair::new(); let num_users = 10 as usize; let num_mints = mints.len(); let mut users = Vec::new(); let mut payment_token_accounts = Vec::new(); for _ in 0..num_users { let user_key = Keypair::new(); pt.add_account( user_key.pubkey(), solana_sdk::account::Account::new( u32::MAX as u64, 0, &solana_sdk::system_program::id(), ), ); // Create token accounts for each payment mint on each user for mint_index in 0..num_mints { let mint = &mints[mint_index].payment_mint; let token_account = get_associated_token_address_with_program_id( &user_key.pubkey(), &mint, &spl_token::id(), ); pt.add_packable_account( token_account, u32::MAX as u64, &spl_token::state::Account { mint: mint.clone(), owner: user_key.pubkey(), amount: 1_000_000_000_000_000_000, state: spl_token::state::AccountState::Initialized, ..spl_token::state::Account::default() }, &spl_token::id(), ); payment_token_accounts.push(token_account); //add the token accounts to the admin as well for payouts and payments let token_account = get_associated_token_address_with_program_id( &admin.pubkey(), &mint, &spl_token::id(), ); pt.add_packable_account( token_account, u32::MAX as u64, &spl_token::state::Account { mint: mint.clone(), owner: admin.pubkey(), amount: 1_000_000_000_000_000_000, state: spl_token::state::AccountState::Initialized, ..spl_token::state::Account::default() }, &spl_token::id(), ); } users.push(user_key); } pt.add_account( admin.pubkey(), solana_sdk::account::Account::new( u32::MAX as u64, 0, &solana_sdk::system_program::id(), ), ); // TODO: Add multiple bonds and issuances let mut context = pt.start_with_context().await; let rent: Rent = context.banks_client.get_rent().await.unwrap(); let usdc_mint = mints[0].payment_mint; let usdc_mxn_payment_feed = PaymentFeedInfo { payment_mint: usdc_mint, base_price_feed: Keypair::new().pubkey(), quote_price_feed: Keypair::new().pubkey(), payment_feed_type: PaymentFeedType::UsdcMxn, }; let usdc_usd_payment_feed = PaymentFeedInfo { payment_mint: usdc_mint, base_price_feed: Keypair::new().pubkey(), quote_price_feed: Keypair::new().pubkey(), payment_feed_type: PaymentFeedType::UsdcUsd, }; let stub_payment_feed = PaymentFeedInfo { payment_mint: usdc_mint, base_price_feed: Keypair::new().pubkey(), quote_price_feed: Pubkey::default(), payment_feed_type: PaymentFeedType::Stub, }; Self { context, rent, mints, bond_mints, nft_collection_mint, admin, delegate, num_users, num_mints, users, payment_token_accounts, usdc_mxn_payment_feed, usdc_usd_payment_feed, stub_payment_feed, } } #[allow(dead_code)] pub async fn process_transaction( &mut self, instructions: &[Instruction], signers: Option<&[&Keypair]>, ) -> Result<(), BanksClientError> { let mut transaction = Transaction::new_with_payer(&instructions, Some(&self.context.payer.pubkey())); let mut all_signers = vec![&self.context.payer]; if let Some(signers) = signers { all_signers.extend_from_slice(signers); } // This fails when warping is involved let recent_blockhash = self .context .banks_client .get_latest_blockhash() .await .unwrap(); transaction.sign(&all_signers, recent_blockhash); self.context .banks_client .process_transaction_with_commitment(transaction, CommitmentLevel::Processed) .await } #[allow(dead_code)] pub async fn process_transaction_expect_error( &mut self, instructions: &[Instruction], signers: Option<&[&Keypair]>, error_code: StablebondError, ) { let mut transaction = Transaction::new_with_payer(&instructions, Some(&self.context.payer.pubkey())); let mut all_signers = vec![&self.context.payer]; if let Some(signers) = signers { all_signers.extend_from_slice(signers); } // This fails when warping is involved let recent_blockhash = self .context .banks_client .get_latest_blockhash() .await .unwrap(); transaction.sign(&all_signers, recent_blockhash); let error = self .context .banks_client .process_transaction_with_commitment(transaction, CommitmentLevel::Processed) .await .err() .unwrap() .unwrap(); match error { TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { let program_error = error_code as u32; assert_eq!(error_index, program_error); } _ => { panic!( "Wrong error occurs while testing fail_purchase_bond_after_starting_new_issuances" ); } } } #[allow(dead_code)] pub async fn get_account(&mut self, address: Pubkey) -> solana_sdk::account::Account { return self .context .banks_client .get_account(address) .await .unwrap() .unwrap(); } #[allow(dead_code)] pub fn get_payer_pk(&mut self) -> Pubkey { return self.context.payer.pubkey(); } #[allow(dead_code)] pub async fn get_lamport_balance(&mut self, address: Pubkey) -> u64 { self.context .banks_client .get_account(address) .await .unwrap() .unwrap() .lamports } #[allow(dead_code)] pub async fn get_token_balance(&mut self, address: Pubkey) -> u64 { let token = self .context .banks_client .get_account(address) .await .unwrap() .unwrap(); return spl_token::state::Account::unpack(&token.data[..]) .unwrap() .amount; } #[allow(dead_code)] pub async fn create_account(&mut self, size: usize, owner: &Pubkey) -> Pubkey { let keypair = Keypair::new(); let rent = self.rent.minimum_balance(size); let instructions = [system_instruction::create_account( &self.context.payer.pubkey(), &keypair.pubkey(), rent as u64, size as u64, owner, )]; self.process_transaction(&instructions, Some(&[&keypair])) .await .unwrap(); return keypair.pubkey(); } #[allow(dead_code)] pub async fn create_mint(&mut self, mint_authority: &Pubkey) -> Pubkey { let keypair = Keypair::new(); let rent = self.rent.minimum_balance(Mint::LEN); let instructions = [ system_instruction::create_account( &self.context.payer.pubkey(), &keypair.pubkey(), rent, Mint::LEN as u64, &spl_token::id(), ), spl_token::instruction::initialize_mint( &spl_token::id(), &keypair.pubkey(), &mint_authority, None, 0, ) .unwrap(), ]; self.process_transaction(&instructions, Some(&[&keypair])) .await .unwrap(); return keypair.pubkey(); } #[allow(dead_code)] pub fn with_mint(&mut self, mint_index: usize) -> BondCookie { return self.mints[mint_index]; } #[allow(dead_code)] pub fn with_user_payment_token_account( &mut self, user_index: usize, mint_index: usize, ) -> Pubkey { return self.payment_token_accounts[(user_index * self.num_mints) + mint_index]; } #[allow(dead_code)] pub fn get_current_time() -> i64 { let now = SystemTime::now(); now.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 } #[allow(dead_code)] pub async fn get_bincode_account( &mut self, address: &Pubkey, ) -> T { self.context .banks_client .get_account(*address) .await .unwrap() .map(|a| deserialize::(&a.data.borrow()).unwrap()) .expect(format!("GET-TEST-ACCOUNT-ERROR: Account {}", address).as_str()) } #[allow(dead_code)] pub async fn get_clock(&mut self) -> Clock { self.get_bincode_account::(&sysvar::clock::id()) .await } #[allow(dead_code)] pub async fn advance_clock_by_slots(&mut self, slots: u64) { let mut clock: Clock = self.get_clock().await; println!("clock slot before: {}", clock.slot); self.context.warp_to_slot(clock.slot + slots).unwrap(); clock = self.get_clock().await; println!("clock slot after: {}", clock.slot); } #[allow(dead_code)] pub async fn advance_clock_past_timestamp(&mut self, unix_timestamp: UnixTimestamp) { let mut clock: Clock = self.get_clock().await; let mut n = 1; while clock.unix_timestamp <= unix_timestamp { // Since the exact time is not deterministic keep wrapping by arbitrary 400 // slots until we pass the requested timestamp self.context.warp_to_slot(clock.slot + 400).unwrap(); n = n + 1; clock = self.get_clock().await; } } }