Program tests

This commit is contained in:
Christian Kamm 2022-10-18 17:38:36 +02:00
parent 93790bfe11
commit 92d1a715e2
9 changed files with 4980 additions and 92 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
node_modules
dist
ts/**/*.js
.vscode
.anchor
target

3730
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,22 @@ no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
test-bpf = []
[dependencies]
anchor-lang = { path = "../../../mango-v4/anchor/lang", features = ["init-if-needed"] }
anchor-spl = { path = "../../../mango-v4/anchor/spl" }
solana-program = "~1.10.29"
static_assertions = "1.1"
static_assertions = "1.1"
[dev-dependencies]
solana-sdk = { version = "~1.10.29", default-features = false }
solana-program-test = "~1.10.29"
solana-logger = "~1.10.29"
async-trait = "0.1.52"
bytemuck = "^1.7.2"
spl-token = { version = "^3.0.0", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "^1.0.3", features = ["no-entrypoint"] }
log = "0.4.14"
env_logger = "0.9.0"
lazy_static = "1.4.0"

View File

@ -0,0 +1,361 @@
#![allow(dead_code)]
use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar;
use anchor_spl::token::Token;
use solana_program_test::BanksClientError;
use solana_sdk::instruction;
use solana_sdk::transport::TransportError;
use std::sync::Arc;
use super::solana::SolanaCookie;
use super::utils::TestKeypair;
use mango_v3_reimbursement::state::*;
#[async_trait::async_trait(?Send)]
pub trait ClientAccountLoader {
async fn load_bytes(&self, pubkey: &Pubkey) -> Option<Vec<u8>>;
async fn load<T: AccountDeserialize>(&self, pubkey: &Pubkey) -> Option<T> {
let bytes = self.load_bytes(pubkey).await?;
AccountDeserialize::try_deserialize(&mut &bytes[..]).ok()
}
}
#[async_trait::async_trait(?Send)]
impl ClientAccountLoader for &SolanaCookie {
async fn load_bytes(&self, pubkey: &Pubkey) -> Option<Vec<u8>> {
self.get_account_data(*pubkey).await
}
}
// TODO: report error outwards etc
pub async fn send_tx<CI: ClientInstruction>(
solana: &SolanaCookie,
ix: CI,
) -> std::result::Result<CI::Accounts, TransportError> {
let (accounts, instruction) = ix.to_instruction(solana).await;
let signers = ix.signers();
let instructions = vec![instruction];
solana
.process_transaction(&instructions, Some(&signers[..]))
.await?;
Ok(accounts)
}
/// Build a transaction from multiple instructions
pub struct ClientTransaction {
solana: Arc<SolanaCookie>,
instructions: Vec<instruction::Instruction>,
signers: Vec<TestKeypair>,
}
impl<'a> ClientTransaction {
pub fn new(solana: &Arc<SolanaCookie>) -> Self {
Self {
solana: solana.clone(),
instructions: vec![],
signers: vec![],
}
}
pub async fn add_instruction<CI: ClientInstruction>(&mut self, ix: CI) -> CI::Accounts {
let solana: &SolanaCookie = &self.solana;
let (accounts, instruction) = ix.to_instruction(solana).await;
self.instructions.push(instruction);
self.signers.extend(ix.signers());
accounts
}
pub fn add_instruction_direct(&mut self, ix: instruction::Instruction) {
self.instructions.push(ix);
}
pub fn add_signer(&mut self, keypair: TestKeypair) {
self.signers.push(keypair);
}
pub async fn send(&self) -> std::result::Result<(), BanksClientError> {
self.solana
.process_transaction(&self.instructions, Some(&self.signers))
.await
}
}
#[async_trait::async_trait(?Send)]
pub trait ClientInstruction {
type Accounts: anchor_lang::ToAccountMetas;
type Instruction: anchor_lang::InstructionData;
async fn to_instruction(
&self,
loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction);
fn signers(&self) -> Vec<TestKeypair>;
}
fn make_instruction(
program_id: Pubkey,
accounts: &impl anchor_lang::ToAccountMetas,
data: impl anchor_lang::InstructionData,
) -> instruction::Instruction {
instruction::Instruction {
program_id,
accounts: anchor_lang::ToAccountMetas::to_account_metas(accounts, None),
data: anchor_lang::InstructionData::data(&data),
}
}
//
// a struct for each instruction along with its
// ClientInstruction impl
//
pub struct CreateGroupInstruction {
pub group_num: u32,
pub claim_transfer_destination: Pubkey,
pub testing: bool,
pub table: Pubkey,
pub payer: TestKeypair,
pub authority: TestKeypair,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for CreateGroupInstruction {
type Accounts = mango_v3_reimbursement::accounts::CreateGroup;
type Instruction = mango_v3_reimbursement::instruction::CreateGroup;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v3_reimbursement::id();
let instruction = Self::Instruction {
group_num: self.group_num,
claim_transfer_destination: self.claim_transfer_destination,
testing: if self.testing { 1 } else { 0 },
};
let group = Pubkey::find_program_address(
&[b"Group".as_ref(), &self.group_num.to_le_bytes()],
&program_id,
)
.0;
let accounts = Self::Accounts {
group,
table: self.table,
payer: self.payer.pubkey(),
authority: self.authority.pubkey(),
system_program: System::id(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.payer, self.authority]
}
}
pub struct CreateVaultInstruction {
pub group: Pubkey,
pub authority: TestKeypair,
pub token_index: usize,
pub mint: Pubkey,
pub payer: TestKeypair,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for CreateVaultInstruction {
type Accounts = mango_v3_reimbursement::accounts::CreateVault;
type Instruction = mango_v3_reimbursement::instruction::CreateVault;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v3_reimbursement::id();
let group: Group = account_loader.load(&self.group).await.unwrap();
let instruction = Self::Instruction {
token_index: self.token_index,
};
let claim_mint = Pubkey::find_program_address(
&[
b"Mint".as_ref(),
self.group.as_ref(),
&self.token_index.to_le_bytes(),
],
&program_id,
)
.0;
let claim_transfer_token_account =
anchor_spl::associated_token::get_associated_token_address(
&group.claim_transfer_destination,
&claim_mint,
);
let vault =
anchor_spl::associated_token::get_associated_token_address(&self.group, &self.mint);
let accounts = Self::Accounts {
group: self.group,
vault,
mint: self.mint,
authority: self.authority.pubkey(),
payer: self.payer.pubkey(),
claim_transfer_token_account,
claim_transfer_destination: group.claim_transfer_destination,
claim_mint,
token_program: Token::id(),
system_program: System::id(),
rent: sysvar::rent::id(),
associated_token_program: anchor_spl::associated_token::ID,
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.authority, self.payer]
}
}
pub struct StartReimbursementInstruction {
pub group: Pubkey,
pub authority: TestKeypair,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for StartReimbursementInstruction {
type Accounts = mango_v3_reimbursement::accounts::StartReimbursement;
type Instruction = mango_v3_reimbursement::instruction::StartReimbursement;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v3_reimbursement::id();
let instruction = Self::Instruction {};
let accounts = Self::Accounts {
group: self.group,
authority: self.authority.pubkey(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.authority]
}
}
pub struct CreateReimbursementAccountInstruction {
pub group: Pubkey,
pub mango_account_owner: Pubkey,
pub payer: TestKeypair,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for CreateReimbursementAccountInstruction {
type Accounts = mango_v3_reimbursement::accounts::CreateReimbursementAccount;
type Instruction = mango_v3_reimbursement::instruction::CreateReimbursementAccount;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v3_reimbursement::id();
let instruction = Self::Instruction {};
let reimbursement_account = Pubkey::find_program_address(
&[
b"ReimbursementAccount".as_ref(),
self.group.as_ref(),
self.mango_account_owner.as_ref(),
],
&program_id,
)
.0;
let accounts = Self::Accounts {
group: self.group,
reimbursement_account,
mango_account_owner: self.mango_account_owner,
payer: self.payer.pubkey(),
system_program: System::id(),
rent: sysvar::rent::id(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.payer]
}
}
pub struct ReimburseInstruction {
pub group: Pubkey,
pub token_index: usize,
pub mango_account_owner: TestKeypair,
pub transfer_claim: bool,
pub token_account: Pubkey,
pub table_index: usize,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for ReimburseInstruction {
type Accounts = mango_v3_reimbursement::accounts::Reimburse;
type Instruction = mango_v3_reimbursement::instruction::Reimburse;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v3_reimbursement::id();
let group: Group = account_loader.load(&self.group).await.unwrap();
let instruction = Self::Instruction {
index_into_table: self.table_index,
token_index: self.token_index,
transfer_claim: self.transfer_claim,
};
let reimbursement_account = Pubkey::find_program_address(
&[
b"ReimbursementAccount".as_ref(),
self.group.as_ref(),
self.mango_account_owner.pubkey().as_ref(),
],
&program_id,
)
.0;
let claim_mint_token_account = anchor_spl::associated_token::get_associated_token_address(
&group.claim_transfer_destination,
&group.claim_mints[self.token_index],
);
let accounts = Self::Accounts {
group: self.group,
vault: group.vaults[self.token_index],
token_account: self.token_account,
reimbursement_account,
mango_account_owner: self.mango_account_owner.pubkey(),
signer: self.mango_account_owner.pubkey(),
claim_mint_token_account,
claim_mint: group.claim_mints[self.token_index],
table: group.table,
token_program: Token::id(),
system_program: System::id(),
rent: sysvar::rent::id(),
};
let instruction = make_instruction(program_id, &accounts, instruction);
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.mango_account_owner]
}
}

View File

@ -0,0 +1,20 @@
use solana_program::pubkey::*;
use crate::utils::*;
#[derive(Debug, Clone, Copy)]
pub struct MintCookie {
pub index: usize,
pub decimals: u8,
pub unit: f64,
pub base_lot: f64,
pub quote_lot: f64,
pub pubkey: Pubkey,
pub authority: TestKeypair,
}
#[derive(Debug, Clone)]
pub struct UserCookie {
pub key: TestKeypair,
pub token_accounts: Vec<Pubkey>,
}

View File

@ -0,0 +1,252 @@
#![allow(dead_code)]
use std::cell::RefCell;
use std::{sync::Arc, sync::RwLock};
use log::*;
use solana_program::{program_option::COption, program_pack::Pack};
use solana_program_test::*;
use solana_sdk::pubkey::Pubkey;
use spl_token::{state::*, *};
pub use client::*;
pub use cookies::*;
pub use solana::*;
pub use utils::*;
pub mod client;
pub mod cookies;
pub mod solana;
pub mod utils;
pub trait AddPacked {
fn add_packable_account<T: Pack>(
&mut self,
pubkey: Pubkey,
amount: u64,
data: &T,
owner: &Pubkey,
);
}
impl AddPacked for ProgramTest {
fn add_packable_account<T: Pack>(
&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);
}
}
struct LoggerWrapper {
inner: env_logger::Logger,
capture: Arc<RwLock<Vec<String>>>,
}
impl Log for LoggerWrapper {
fn enabled(&self, metadata: &log::Metadata) -> bool {
self.inner.enabled(metadata)
}
fn log(&self, record: &log::Record) {
if record
.target()
.starts_with("solana_runtime::message_processor")
{
let msg = record.args().to_string();
if let Some(data) = msg.strip_prefix("Program log: ") {
self.capture.write().unwrap().push(data.into());
} else if let Some(data) = msg.strip_prefix("Program data: ") {
self.capture.write().unwrap().push(data.into());
}
}
self.inner.log(record);
}
fn flush(&self) {}
}
pub struct TestContextBuilder {
test: ProgramTest,
logger_capture: Arc<RwLock<Vec<String>>>,
mint0: Pubkey,
}
lazy_static::lazy_static! {
static ref LOGGER_CAPTURE: Arc<RwLock<Vec<String>>> = Arc::new(RwLock::new(vec![]));
static ref LOGGER_LOCK: Arc<RwLock<()>> = Arc::new(RwLock::new(()));
}
impl TestContextBuilder {
pub fn new() -> Self {
// We need to intercept logs to capture program log output
let log_filter = "solana_rbpf=trace,\
solana_runtime::message_processor=debug,\
solana_runtime::system_instruction_processor=trace,\
solana_program_test=info";
let env_logger =
env_logger::Builder::from_env(env_logger::Env::new().default_filter_or(log_filter))
.format_timestamp_nanos()
.build();
let _ = log::set_boxed_logger(Box::new(LoggerWrapper {
inner: env_logger,
capture: LOGGER_CAPTURE.clone(),
}));
let mut test = ProgramTest::new(
"mango_v3_reimbursement",
mango_v3_reimbursement::id(),
processor!(mango_v3_reimbursement::entry),
);
// intentionally set to as tight as possible, to catch potential problems early
test.set_compute_max_units(200000);
Self {
test,
logger_capture: LOGGER_CAPTURE.clone(),
mint0: Pubkey::new_unique(),
}
}
pub fn test(&mut self) -> &mut ProgramTest {
&mut self.test
}
pub fn create_mints(&mut self) -> Vec<MintCookie> {
let mut mints: Vec<MintCookie> = vec![
MintCookie {
index: 0,
decimals: 6,
unit: 10u64.pow(6) as f64,
base_lot: 100 as f64,
quote_lot: 10 as f64,
pubkey: self.mint0,
authority: TestKeypair::new(),
}, // symbol: "MNGO".to_string()
];
for i in 1..16 {
mints.push(MintCookie {
index: i,
decimals: 6,
unit: 10u64.pow(6) as f64,
base_lot: 0 as f64,
quote_lot: 0 as f64,
pubkey: Pubkey::default(),
authority: TestKeypair::new(),
});
}
// Add mints in loop
for mint_index in 0..mints.len() {
let mint_pk: Pubkey;
if mints[mint_index].pubkey == Pubkey::default() {
mint_pk = Pubkey::new_unique();
} else {
mint_pk = mints[mint_index].pubkey;
}
mints[mint_index].pubkey = mint_pk;
self.test.add_packable_account(
mint_pk,
u32::MAX as u64,
&Mint {
is_initialized: true,
mint_authority: COption::Some(mints[mint_index].authority.pubkey()),
decimals: mints[mint_index].decimals,
..Mint::default()
},
&spl_token::id(),
);
}
mints
}
pub fn create_users(&mut self, mints: &[MintCookie]) -> Vec<UserCookie> {
let num_users = 4;
let mut users = Vec::new();
for _ in 0..num_users {
let user_key = TestKeypair::new();
self.test.add_account(
user_key.pubkey(),
solana_sdk::account::Account::new(
u32::MAX as u64,
0,
&solana_sdk::system_program::id(),
),
);
// give every user 10^18 (< 2^60) of every token
// ~~ 1 trillion in case of 6 decimals
let mut token_accounts = Vec::new();
for mint_index in 0..mints.len() {
let token_key = Pubkey::new_unique();
self.test.add_packable_account(
token_key,
u32::MAX as u64,
&spl_token::state::Account {
mint: mints[mint_index].pubkey,
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(),
);
token_accounts.push(token_key);
}
users.push(UserCookie {
key: user_key,
token_accounts,
});
}
users
}
pub async fn start_default(mut self) -> TestContext {
let mints = self.create_mints();
let users = self.create_users(&mints);
let solana = self.start().await;
TestContext {
solana: solana.clone(),
mints,
users,
}
}
pub async fn start(self) -> Arc<SolanaCookie> {
let mut context = self.test.start_with_context().await;
let rent = context.banks_client.get_rent().await.unwrap();
let solana = Arc::new(SolanaCookie {
context: RefCell::new(context),
rent,
logger_capture: self.logger_capture.clone(),
logger_lock: LOGGER_LOCK.clone(),
last_transaction_log: RefCell::new(vec![]),
});
solana
}
}
pub struct TestContext {
pub solana: Arc<SolanaCookie>,
pub mints: Vec<MintCookie>,
pub users: Vec<UserCookie>,
}
impl TestContext {
pub async fn new() -> Self {
TestContextBuilder::new().start_default().await
}
}

View File

@ -0,0 +1,225 @@
#![allow(dead_code)]
use std::cell::RefCell;
use std::sync::{Arc, RwLock};
use crate::utils::TestKeypair;
use anchor_lang::AccountDeserialize;
use anchor_spl::token::TokenAccount;
use solana_program::{program_pack::Pack, rent::*, system_instruction};
use solana_program_test::*;
use solana_sdk::{
account::ReadableAccount,
instruction::Instruction,
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};
//use spl_token::*;
pub struct SolanaCookie {
pub context: RefCell<ProgramTestContext>,
pub rent: Rent,
pub logger_capture: Arc<RwLock<Vec<String>>>,
pub logger_lock: Arc<RwLock<()>>,
pub last_transaction_log: RefCell<Vec<String>>,
}
impl SolanaCookie {
pub async fn process_transaction(
&self,
instructions: &[Instruction],
signers: Option<&[TestKeypair]>,
) -> Result<(), BanksClientError> {
// The locking in this function is convoluted:
// We capture the program log output by overriding the global logger and capturing
// messages there. This logger is potentially shared among multiple tests that run
// concurrently.
// To allow each independent SolanaCookie to capture only the logs from the transaction
// passed to process_transaction, wo globally hold the "program_log_lock" for the
// duration that the tx needs to process. So only a single one can run at a time.
let tx_log_lock = Arc::new(self.logger_lock.write().unwrap());
self.logger_capture.write().unwrap().clear();
let mut context = self.context.borrow_mut();
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&context.payer.pubkey()));
let mut all_signers = vec![&context.payer];
let signer_keypairs =
signers.map(|signers| signers.iter().map(|s| s.into()).collect::<Vec<Keypair>>());
let signer_keypair_refs = signer_keypairs
.as_ref()
.map(|kps| kps.iter().map(|kp| kp).collect::<Vec<&Keypair>>());
if let Some(signer_keypair_refs) = signer_keypair_refs {
all_signers.extend(signer_keypair_refs.iter());
}
// This fails when warping is involved - https://gitmemory.com/issue/solana-labs/solana/18201/868325078
// let recent_blockhash = self.context.banks_client.get_recent_blockhash().await.unwrap();
transaction.sign(&all_signers, context.last_blockhash);
let result = context
.banks_client
.process_transaction_with_commitment(
transaction,
solana_sdk::commitment_config::CommitmentLevel::Processed,
)
.await;
*self.last_transaction_log.borrow_mut() = self.logger_capture.read().unwrap().clone();
drop(tx_log_lock);
drop(context);
// This makes sure every transaction gets a new blockhash, avoiding issues where sending
// the same transaction again would lead to it being skipped.
self.advance_by_slots(1).await;
result
}
pub async fn get_clock(&self) -> solana_program::clock::Clock {
self.context
.borrow_mut()
.banks_client
.get_sysvar::<solana_program::clock::Clock>()
.await
.unwrap()
}
pub async fn advance_by_slots(&self, slots: u64) {
let clock = self.get_clock().await;
self.context
.borrow_mut()
.warp_to_slot(clock.slot + slots + 1)
.unwrap();
}
pub async fn advance_clock(&self) {
let mut clock = self.get_clock().await;
let old_ts = clock.unix_timestamp;
// just advance enough to ensure we get changes over last_updated in various ix
// if this gets too slow for our tests, remove and replace with manual time offset
// which is configurable
while clock.unix_timestamp <= old_ts {
self.context
.borrow_mut()
.warp_to_slot(clock.slot + 50)
.unwrap();
clock = self.get_clock().await;
}
}
pub async fn get_newest_slot_from_history(&self) -> u64 {
self.context
.borrow_mut()
.banks_client
.get_sysvar::<solana_program::slot_history::SlotHistory>()
.await
.unwrap()
.newest()
}
pub async fn create_account_from_len(&self, owner: &Pubkey, len: usize) -> Pubkey {
let key = TestKeypair::new();
let rent = self.rent.minimum_balance(len);
let create_account_instr = solana_sdk::system_instruction::create_account(
&self.context.borrow().payer.pubkey(),
&key.pubkey(),
rent,
len as u64,
&owner,
);
self.process_transaction(&[create_account_instr], Some(&[key]))
.await
.unwrap();
key.pubkey()
}
pub async fn create_account_for_type<T>(&self, owner: &Pubkey) -> Pubkey {
let key = TestKeypair::new();
let len = 8 + std::mem::size_of::<T>();
let rent = self.rent.minimum_balance(len);
let create_account_instr = solana_sdk::system_instruction::create_account(
&self.context.borrow().payer.pubkey(),
&key.pubkey(),
rent,
len as u64,
&owner,
);
self.process_transaction(&[create_account_instr], Some(&[key]))
.await
.unwrap();
key.pubkey()
}
pub async fn create_token_account(&self, owner: &Pubkey, mint: Pubkey) -> Pubkey {
let keypair = TestKeypair::new();
let rent = self.rent.minimum_balance(spl_token::state::Account::LEN);
let instructions = [
system_instruction::create_account(
&self.context.borrow().payer.pubkey(),
&keypair.pubkey(),
rent,
spl_token::state::Account::LEN as u64,
&spl_token::id(),
),
spl_token::instruction::initialize_account(
&spl_token::id(),
&keypair.pubkey(),
&mint,
owner,
)
.unwrap(),
];
self.process_transaction(&instructions, Some(&[keypair]))
.await
.unwrap();
return keypair.pubkey();
}
pub async fn get_account_data(&self, address: Pubkey) -> Option<Vec<u8>> {
Some(
self.context
.borrow_mut()
.banks_client
.get_account(address)
.await
.unwrap()?
.data()
.to_vec(),
)
}
pub async fn get_account_opt<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
let data = self.get_account_data(address).await?;
let mut data_slice: &[u8] = &data;
AccountDeserialize::try_deserialize(&mut data_slice).ok()
}
// Use when accounts are too big for the stack
pub async fn get_account_boxed<T: AccountDeserialize>(&self, address: Pubkey) -> Box<T> {
let data = self.get_account_data(address).await.unwrap();
let mut data_slice: &[u8] = &data;
Box::new(AccountDeserialize::try_deserialize(&mut data_slice).unwrap())
}
pub async fn get_account<T: AccountDeserialize>(&self, address: Pubkey) -> T {
self.get_account_opt(address).await.unwrap()
}
pub async fn token_account_balance(&self, address: Pubkey) -> u64 {
self.get_account::<TokenAccount>(address).await.amount
}
pub fn program_log(&self) -> Vec<String> {
self.last_transaction_log.borrow().clone()
}
}

View File

@ -0,0 +1,53 @@
#![allow(dead_code)]
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
pub fn clone_keypair(keypair: &Keypair) -> Keypair {
Keypair::from_base58_string(&keypair.to_base58_string())
}
// Add clone() to Keypair, totally safe in tests
pub trait ClonableKeypair {
fn clone(&self) -> Self;
}
impl ClonableKeypair for Keypair {
fn clone(&self) -> Self {
clone_keypair(self)
}
}
/// A Keypair-like struct that's Clone and Copy and can be into()ed to a Keypair
///
/// The regular Keypair is neither Clone nor Copy because the key data is sensitive
/// and should not be copied needlessly. That just makes things difficult for tests.
#[derive(Clone, Copy, Debug)]
pub struct TestKeypair([u8; 64]);
impl TestKeypair {
pub fn new() -> Self {
Keypair::new().into()
}
pub fn to_keypair(&self) -> Keypair {
Keypair::from_bytes(&self.0).unwrap()
}
pub fn pubkey(&self) -> Pubkey {
solana_sdk::signature::Signer::pubkey(&self.to_keypair())
}
}
impl Default for TestKeypair {
fn default() -> Self {
Self([0; 64])
}
}
impl<T: std::borrow::Borrow<Keypair>> From<T> for TestKeypair {
fn from(k: T) -> Self {
Self(k.borrow().to_bytes())
}
}
impl Into<Keypair> for &TestKeypair {
fn into(self) -> Keypair {
self.to_keypair()
}
}

View File

@ -0,0 +1,415 @@
#![cfg(feature = "test-bpf")]
use solana_program_test::{BanksClientError, *};
use mango_v3_reimbursement::state::*;
use program_test::*;
use std::sync::Arc;
mod program_test;
fn make_table(authority: Pubkey, rows: &[Row]) -> solana_sdk::account::Account {
let mut data = vec![0u8; 40 + rows.len() * 160];
data[5..37].copy_from_slice(&authority.to_bytes());
for (i, row) in rows.iter().enumerate() {
data[40 + i * 160..40 + (i + 1) * 160].copy_from_slice(bytemuck::bytes_of(row));
}
let mut acc =
solana_sdk::account::Account::new(u32::MAX as u64, data.len(), &Pubkey::new_unique());
acc.data.copy_from_slice(&data);
acc
}
async fn token_transfer(
solana: &Arc<SolanaCookie>,
from: Pubkey,
to: Pubkey,
authority: TestKeypair,
amount: u64,
) -> std::result::Result<(), BanksClientError> {
let mut tx = ClientTransaction::new(solana);
tx.add_instruction_direct(
spl_token::instruction::transfer(
&spl_token::ID,
&from,
&to,
&authority.pubkey(),
&[&authority.pubkey()],
amount,
)
.unwrap(),
);
tx.add_signer(authority);
tx.send().await
}
async fn create_ata(
solana: &Arc<SolanaCookie>,
authority: Pubkey,
payer: TestKeypair,
mint: Pubkey,
) -> std::result::Result<Pubkey, BanksClientError> {
let mut tx = ClientTransaction::new(solana);
tx.add_instruction_direct(
spl_associated_token_account::instruction::create_associated_token_account(
&payer.pubkey(),
&authority,
&mint,
),
);
tx.add_signer(payer);
tx.send().await?;
Ok(spl_associated_token_account::get_associated_token_address(
&authority, &mint,
))
}
async fn load_reimbursement(
solana: &SolanaCookie,
reimbursement_account: Pubkey,
) -> ([bool; 16], [bool; 16]) {
let data: ReimbursementAccount = solana.get_account(reimbursement_account).await;
let reimb = (0..16).map(|i| data.reimbursed(i)).collect::<Vec<bool>>();
let transf = (0..16)
.map(|i| data.claim_transferred(i))
.collect::<Vec<bool>>();
(reimb.try_into().unwrap(), transf.try_into().unwrap())
}
#[tokio::test]
async fn test_basic() -> Result<()> {
use mango_v3_reimbursement::accounts;
let authority = TestKeypair::new();
let table = Pubkey::new_unique();
let user1 = TestKeypair::new();
let user2 = TestKeypair::new();
let table_rows = vec![
Row {
owner: user1.pubkey(),
balances: (0..16)
.map(|i| 100 + i)
.collect::<Vec<u64>>()
.try_into()
.unwrap(),
},
Row {
owner: user2.pubkey(),
balances: (0..16).collect::<Vec<u64>>().try_into().unwrap(),
},
];
let table_account = make_table(authority.pubkey(), &table_rows);
let mut builder = TestContextBuilder::new();
builder.test().add_account(table, table_account);
let context = builder.start_default().await;
let solana = &context.solana.clone();
let payer = context.users[1].key;
let mut user1_token = vec![];
for i in 0..2 {
user1_token.push(
create_ata(solana, user1.pubkey(), payer, context.mints[i].pubkey)
.await
.unwrap(),
);
}
let mut user2_token = vec![];
for i in 0..2 {
user2_token.push(
create_ata(solana, user2.pubkey(), payer, context.mints[i].pubkey)
.await
.unwrap(),
);
}
let accounts::CreateGroup { group, .. } = send_tx(
solana,
CreateGroupInstruction {
group_num: 0,
testing: false,
claim_transfer_destination: authority.pubkey(),
authority,
payer,
table,
},
)
.await
.unwrap();
let mut claim_accounts = vec![];
for i in [0, 1, 15] {
let accounts::CreateVault {
vault,
claim_transfer_token_account,
..
} = send_tx(
solana,
CreateVaultInstruction {
group,
authority,
payer,
token_index: i,
mint: context.mints[i].pubkey,
},
)
.await
.unwrap();
token_transfer(
solana,
context.users[1].token_accounts[i],
vault,
payer,
1000,
)
.await
.unwrap();
claim_accounts.push(claim_transfer_token_account);
}
//
// Test reimbursing user1
//
let accounts::CreateReimbursementAccount {
reimbursement_account,
..
} = send_tx(
solana,
CreateReimbursementAccountInstruction {
group,
mango_account_owner: user1.pubkey(),
payer,
},
)
.await
.unwrap();
// Creating twice is ok
send_tx(
solana,
CreateReimbursementAccountInstruction {
group,
mango_account_owner: user1.pubkey(),
payer,
},
)
.await
.unwrap();
// Cannot reimburse before start
assert!(send_tx(
solana,
ReimburseInstruction {
group,
token_index: 0,
table_index: 0,
mango_account_owner: user1,
transfer_claim: true,
token_account: user1_token[0],
}
)
.await
.is_err());
send_tx(solana, StartReimbursementInstruction { group, authority })
.await
.unwrap();
// Cannot reimburse a bad table index
assert!(send_tx(
solana,
ReimburseInstruction {
group,
token_index: 0,
table_index: 1,
mango_account_owner: user1,
transfer_claim: true,
token_account: user1_token[0],
}
)
.await
.is_err());
send_tx(
solana,
ReimburseInstruction {
group,
token_index: 0,
table_index: 0,
mango_account_owner: user1,
transfer_claim: false,
token_account: user1_token[0],
},
)
.await
.unwrap();
assert_eq!(solana.token_account_balance(user1_token[0]).await, 100);
assert_eq!(solana.token_account_balance(claim_accounts[0]).await, 0);
{
let (reimb, transf) = load_reimbursement(solana, reimbursement_account).await;
assert_eq!(reimb[0], true);
assert_eq!(reimb[1..], [false; 15]);
assert_eq!(transf, [false; 16]);
}
// does not work again (regardless of transfer_claim)
assert!(send_tx(
solana,
ReimburseInstruction {
group,
token_index: 0,
table_index: 0,
mango_account_owner: user1,
transfer_claim: true,
token_account: user1_token[0],
}
)
.await
.is_err());
assert!(send_tx(
solana,
ReimburseInstruction {
group,
token_index: 0,
table_index: 0,
mango_account_owner: user1,
transfer_claim: false,
token_account: user1_token[0],
}
)
.await
.is_err());
// can claim a second token
send_tx(
solana,
ReimburseInstruction {
group,
token_index: 1,
table_index: 0,
mango_account_owner: user1,
transfer_claim: true,
token_account: user1_token[1],
},
)
.await
.unwrap();
assert_eq!(solana.token_account_balance(user1_token[1]).await, 101);
assert_eq!(solana.token_account_balance(claim_accounts[1]).await, 101);
{
let (reimb, transf) = load_reimbursement(solana, reimbursement_account).await;
assert_eq!(reimb[0..2], [true; 2]);
assert_eq!(reimb[2..], [false; 14]);
assert_eq!(transf[0..2], [false, true]);
assert_eq!(transf[2..], [false; 14]);
}
// can't claim again
assert!(send_tx(
solana,
ReimburseInstruction {
group,
token_index: 1,
table_index: 0,
mango_account_owner: user1,
transfer_claim: false,
token_account: user1_token[1],
}
)
.await
.is_err());
//
// Test reimbursing user2
//
let accounts::CreateReimbursementAccount {
reimbursement_account,
..
} = send_tx(
solana,
CreateReimbursementAccountInstruction {
group,
mango_account_owner: user2.pubkey(),
payer,
},
)
.await
.unwrap();
send_tx(
solana,
ReimburseInstruction {
group,
token_index: 1,
table_index: 1,
mango_account_owner: user2,
transfer_claim: true,
token_account: user2_token[1],
},
)
.await
.unwrap();
// ok to call reimburse, even if amount=0
send_tx(
solana,
ReimburseInstruction {
group,
token_index: 0,
table_index: 1,
mango_account_owner: user2,
transfer_claim: true,
token_account: user2_token[0],
},
)
.await
.unwrap();
assert_eq!(solana.token_account_balance(user2_token[0]).await, 0);
assert_eq!(solana.token_account_balance(user2_token[1]).await, 1);
assert_eq!(solana.token_account_balance(claim_accounts[1]).await, 101 + 1);
{
let (reimb, transf) = load_reimbursement(solana, reimbursement_account).await;
assert_eq!(reimb[0..2], [true; 2]);
assert_eq!(reimb[2..], [false; 14]);
assert_eq!(transf[0..2], [true; 2]);
assert_eq!(transf[2..], [false; 14]);
}
// Can claim the rightmost row entry too
let user2_token15 = create_ata(solana, user2.pubkey(), payer, context.mints[15].pubkey)
.await
.unwrap();
send_tx(
solana,
ReimburseInstruction {
group,
token_index: 15,
table_index: 1,
mango_account_owner: user2,
transfer_claim: false,
token_account: user2_token15,
},
)
.await
.unwrap();
assert_eq!(solana.token_account_balance(user2_token15).await, 15);
assert_eq!(solana.token_account_balance(claim_accounts[2]).await, 0);
{
let (reimb, transf) = load_reimbursement(solana, reimbursement_account).await;
assert_eq!(reimb[0..2], [true; 2]);
assert_eq!(reimb[2..15], [false; 13]);
assert_eq!(reimb[15], true);
assert_eq!(transf[0..2], [true; 2]);
assert_eq!(transf[2..], [false; 14]);
}
Ok(())
}