From 07d9a644de05f05b7de01cd4fa97506d907efd5f Mon Sep 17 00:00:00 2001 From: skrrb Date: Tue, 16 Jan 2024 12:22:14 +0100 Subject: [PATCH 1/4] jup test against program simulation --- Cargo.lock | 1 + lib/client/Cargo.toml | 9 +- lib/client/src/jup.rs | 149 +++++++++++++++++++++++++-- programs/openbook-v2/fuzz/Cargo.lock | 12 +++ 4 files changed, 163 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 46ba1c9d9..d1e88066a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3534,6 +3534,7 @@ dependencies = [ "shellexpand", "solana-account-decoder", "solana-client", + "solana-program-test", "solana-rpc", "solana-rpc-client", "solana-rpc-client-api", diff --git a/lib/client/Cargo.toml b/lib/client/Cargo.toml index 585a2e948..861cc7cb4 100644 --- a/lib/client/Cargo.toml +++ b/lib/client/Cargo.toml @@ -8,6 +8,10 @@ version = "0.3.0" [lib] doctest = false +[features] +default = ["openbook-v2/client"] +enable-gpl = ["openbook-v2/enable-gpl"] + [dependencies] anchor-client = {workspace = true} anchor-lang = {workspace = true} @@ -24,7 +28,7 @@ itertools = "0.10.3" jsonrpc-core = "18.0.0" jsonrpc-core-client = {version = "18.0.0", features = ["ws", "http", "tls"]} log = "0.4" -openbook-v2 = {path = "../../programs/openbook-v2", features = ["client"]} +openbook-v2 = {path = "../../programs/openbook-v2"} pyth-sdk-solana = {workspace = true} reqwest = "0.11.11" serde = "1.0.141" @@ -41,3 +45,6 @@ thiserror = "1.0.31" tokio = {version = "1", features = ["full"]} tokio-stream = {version = "0.1.9"} jupiter-amm-interface = "0.1.1" + +[dev-dependencies] +solana-program-test = { workspace = true } diff --git a/lib/client/src/jup.rs b/lib/client/src/jup.rs index 49803e9cf..eb8577c04 100644 --- a/lib/client/src/jup.rs +++ b/lib/client/src/jup.rs @@ -281,12 +281,26 @@ impl Amm for OpenBookMarket { #[cfg(test)] mod test { use super::*; - use solana_client::rpc_client::RpcClient; + use anchor_spl::token::spl_token::{ + self, + state::{Account as TokenAccount, AccountState}, + }; + use solana_client::nonblocking::rpc_client::RpcClient; + use solana_program_test::{processor, ProgramTest}; + use solana_sdk::{ + account::{Account, WritableAccount}, + instruction::Instruction, + program_pack::Pack, + signature::Signer, + signer::keypair::Keypair, + stake_history::Epoch, + transaction::Transaction, + }; use std::str::FromStr; - #[test] + #[tokio::test] // TODO replace std::env by mainnet market after audit deploy - fn test_jupiter_local() -> Result<()> { + async fn test_jupiter_local() -> Result<()> { let market = match std::env::var("MARKET_PUBKEY") { Ok(key) => Pubkey::from_str(&key)?, Err(_) => { @@ -295,8 +309,8 @@ mod test { } }; - let rpc = RpcClient::new("http://127.0.0.1:8899"); - let account = rpc.get_account(&market)?; + let rpc = RpcClient::new("http://127.0.0.1:8899".to_string()); + let account = rpc.get_account(&market).await?; let market_account = KeyedAccount { key: market, @@ -309,7 +323,7 @@ mod test { let pubkeys = openbook.get_accounts_to_update(); let accounts: AccountMap = pubkeys .iter() - .zip(rpc.get_multiple_accounts(&pubkeys)?) + .zip(rpc.get_multiple_accounts(&pubkeys).await?) .map(|(key, acc)| (*key, acc.unwrap())) .collect(); @@ -320,8 +334,10 @@ mod test { (reserves[0], reserves[1]) }; + let in_amount = 1_000_000_000; + let quote_params = QuoteParams { - in_amount: 80, + in_amount, input_mint: base_mint, output_mint: quote_mint, }; @@ -337,6 +353,125 @@ mod test { println!("{:#?}", quote_params); println!("{:#?}", quote); + let mut pt = ProgramTest::new( + "openbook_v2", + openbook_v2::id(), + processor!(openbook_v2::entry), + ); + + pt.add_account(market, market_account.account.clone()); + for (pubkey, account) in accounts.into_iter() { + pt.add_account(pubkey, account); + } + + let initial_amount = 1_000_000_000_000_000; + + let mut add_token_account = |pubkey, owner, mint| { + let mut data = vec![0_u8; TokenAccount::LEN]; + let account = TokenAccount { + state: AccountState::Initialized, + mint, + owner, + amount: initial_amount, + ..TokenAccount::default() + }; + TokenAccount::pack(account, &mut data).unwrap(); + pt.add_account( + pubkey, + Account::create( + Rent::default().minimum_balance(data.len()), + data, + spl_token::ID, + false, + Epoch::default(), + ), + ); + }; + + let user = Keypair::new(); + let user_base_account = Pubkey::new_unique(); + let user_quote_account = Pubkey::new_unique(); + + let market_data = Market::try_deserialize(&mut market_account.account.data.as_slice())?; + + add_token_account(user_base_account, user.pubkey(), market_data.base_mint); + add_token_account(user_quote_account, user.pubkey(), market_data.quote_mint); + + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + // This replicates the above logic in quote() so the asme amounts are used + let (max_base_lots, max_quote_lots_including_fees) = { + ( + i64::try_from(in_amount).unwrap() / market_data.base_lot_size, + market_data.max_quote_lots(), + ) + }; + + let ix = Instruction { + program_id: openbook_v2::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &openbook_v2::accounts::PlaceTakeOrder { + signer: user.pubkey(), + penalty_payer: user.pubkey(), + market: market, + user_base_account, + user_quote_account, + market_authority: market_data.market_authority, + bids: market_data.bids, + asks: market_data.asks, + market_base_vault: market_data.market_base_vault, + market_quote_vault: market_data.market_quote_vault, + event_heap: market_data.event_heap, + oracle_a: Option::from(market_data.oracle_a), + oracle_b: Option::from(market_data.oracle_b), + token_program: Token::id(), + system_program: System::id(), + open_orders_admin: None, + }, + None, + ), + data: anchor_lang::InstructionData::data(&openbook_v2::instruction::PlaceTakeOrder { + args: openbook_v2::PlaceTakeOrderArgs { + side: openbook_v2::state::Side::Ask, + price_lots: i64::MAX, + max_base_lots, + max_quote_lots_including_fees, + order_type: openbook_v2::state::PlaceOrderType::Market, + limit: u8::MAX, + }, + }), + }; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &user], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + let base_account = banks_client + .get_account(user_base_account) + .await + .unwrap() + .unwrap(); + + let quote_account = banks_client + .get_account(user_quote_account) + .await + .unwrap() + .unwrap(); + + let get_amount = + |account: Account| -> u64 { TokenAccount::unpack(&account.data).unwrap().amount }; + + let base_amount = get_amount(base_account); + let quote_amount = get_amount(quote_account); + println!("{}", base_amount); + println!("{}", quote_amount); + + assert_eq!(quote_amount - initial_amount, quote.out_amount); + Ok(()) } } diff --git a/programs/openbook-v2/fuzz/Cargo.lock b/programs/openbook-v2/fuzz/Cargo.lock index 256fa1b5b..cab94afd9 100644 --- a/programs/openbook-v2/fuzz/Cargo.lock +++ b/programs/openbook-v2/fuzz/Cargo.lock @@ -1005,6 +1005,17 @@ dependencies = [ "rayon", ] +[[package]] +name = "default-env" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f753eb82d29277e79efc625e84aecacfd4851ee50e05a8573a4740239a77bfd3" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "derivation-path" version = "0.2.0" @@ -2239,6 +2250,7 @@ dependencies = [ "arbitrary", "arrayref", "bytemuck", + "default-env", "derivative", "fixed", "itertools 0.10.5", From a215cc7d9e2d231ecb781c118da796d239bfa6a5 Mon Sep 17 00:00:00 2001 From: skrrb Date: Tue, 16 Jan 2024 16:54:00 +0100 Subject: [PATCH 2/4] update ts --- lib/client/src/jup.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/client/src/jup.rs b/lib/client/src/jup.rs index eb8577c04..ed2da5e45 100644 --- a/lib/client/src/jup.rs +++ b/lib/client/src/jup.rs @@ -115,6 +115,8 @@ impl Amm for OpenBookMarket { clock.slot, )?; + self.timestamp = clock.unix_timestamp.try_into().unwrap(); + Ok(()) } @@ -321,13 +323,13 @@ mod test { let mut openbook = OpenBookMarket::from_keyed_account(&market_account).unwrap(); let pubkeys = openbook.get_accounts_to_update(); - let accounts: AccountMap = pubkeys + let accounts_map: AccountMap = pubkeys .iter() .zip(rpc.get_multiple_accounts(&pubkeys).await?) .map(|(key, acc)| (*key, acc.unwrap())) .collect(); - openbook.update(&accounts)?; + openbook.update(&accounts_map)?; let (base_mint, quote_mint) = { let reserves = openbook.get_reserve_mints(); @@ -360,7 +362,7 @@ mod test { ); pt.add_account(market, market_account.account.clone()); - for (pubkey, account) in accounts.into_iter() { + for (pubkey, account) in accounts_map.into_iter() { pt.add_account(pubkey, account); } @@ -413,7 +415,7 @@ mod test { &openbook_v2::accounts::PlaceTakeOrder { signer: user.pubkey(), penalty_payer: user.pubkey(), - market: market, + market, user_base_account, user_quote_account, market_authority: market_data.market_authority, From 14a899ca4c754df2d823fda0173261cadf6fb787 Mon Sep 17 00:00:00 2001 From: skrrb Date: Tue, 16 Jan 2024 16:54:53 +0100 Subject: [PATCH 3/4] fix quote amount --- lib/client/src/jup.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/client/src/jup.rs b/lib/client/src/jup.rs index ed2da5e45..595a167f9 100644 --- a/lib/client/src/jup.rs +++ b/lib/client/src/jup.rs @@ -134,8 +134,7 @@ impl Amm for OpenBookMarket { let (max_base_lots, max_quote_lots_including_fees) = match side { Side::Bid => ( self.market.max_base_lots(), - input_amount / self.market.quote_lot_size - + input_amount % self.market.quote_lot_size, + input_amount / self.market.quote_lot_size, ), Side::Ask => ( input_amount / self.market.base_lot_size, From 055d366f4f52a8c934364e054f10f6fad17e5349 Mon Sep 17 00:00:00 2001 From: skrrb Date: Tue, 16 Jan 2024 17:19:24 +0100 Subject: [PATCH 4/4] update comment --- lib/client/src/jup.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/src/jup.rs b/lib/client/src/jup.rs index 595a167f9..d95d16114 100644 --- a/lib/client/src/jup.rs +++ b/lib/client/src/jup.rs @@ -300,7 +300,7 @@ mod test { use std::str::FromStr; #[tokio::test] - // TODO replace std::env by mainnet market after audit deploy + // TODO replace with local accounts async fn test_jupiter_local() -> Result<()> { let market = match std::env::var("MARKET_PUBKEY") { Ok(key) => Pubkey::from_str(&key)?,