diff --git a/bin/liquidator/src/liquidate.rs b/bin/liquidator/src/liquidate.rs index 3097c6ecb..de7bd31e5 100644 --- a/bin/liquidator/src/liquidate.rs +++ b/bin/liquidator/src/liquidate.rs @@ -104,12 +104,6 @@ impl<'a> LiquidateHelper<'a> { Ok(Some(txsig)) } - fn liq_compute_limit_instruction(&self) -> solana_sdk::instruction::Instruction { - solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit( - self.config.compute_limit_for_liq_ix, - ) - } - async fn perp_liq_base_or_positive_pnl(&self) -> anyhow::Result> { let all_perp_base_positions: anyhow::Result< Vec>, @@ -208,7 +202,7 @@ impl<'a> LiquidateHelper<'a> { "computed transfer maximums" ); - let liq_ix = self + let mut liq_ixs = self .client .perp_liq_base_or_positive_pnl_instruction( (self.pubkey, &self.liqee), @@ -218,9 +212,10 @@ impl<'a> LiquidateHelper<'a> { ) .await .context("creating perp_liq_base_or_positive_pnl_instruction")?; + liq_ixs.cu = liq_ixs.cu.max(self.config.compute_limit_for_liq_ix); let txsig = self .client - .send_and_confirm_owner_tx(vec![self.liq_compute_limit_instruction(), liq_ix]) + .send_and_confirm_owner_tx(liq_ixs.to_instructions()) .await .context("sending perp_liq_base_or_positive_pnl_instruction")?; info!( @@ -253,7 +248,7 @@ impl<'a> LiquidateHelper<'a> { } let (perp_market_index, _) = perp_negative_pnl.first().unwrap(); - let liq_ix = self + let mut liq_ixs = self .client .perp_liq_negative_pnl_or_bankruptcy_instruction( (self.pubkey, &self.liqee), @@ -263,9 +258,10 @@ impl<'a> LiquidateHelper<'a> { ) .await .context("creating perp_liq_negative_pnl_or_bankruptcy_instruction")?; + liq_ixs.cu = liq_ixs.cu.max(self.config.compute_limit_for_liq_ix); let txsig = self .client - .send_and_confirm_owner_tx(vec![self.liq_compute_limit_instruction(), liq_ix]) + .send_and_confirm_owner_tx(liq_ixs.to_instructions()) .await .context("sending perp_liq_negative_pnl_or_bankruptcy_instruction")?; info!( @@ -374,7 +370,7 @@ impl<'a> LiquidateHelper<'a> { // TODO: log liqor's assets in UI form // TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side // - let liq_ix = self + let mut liq_ixs = self .client .token_liq_with_token_instruction( (self.pubkey, &self.liqee), @@ -384,9 +380,10 @@ impl<'a> LiquidateHelper<'a> { ) .await .context("creating liq_token_with_token ix")?; + liq_ixs.cu = liq_ixs.cu.max(self.config.compute_limit_for_liq_ix); let txsig = self .client - .send_and_confirm_owner_tx(vec![self.liq_compute_limit_instruction(), liq_ix]) + .send_and_confirm_owner_tx(liq_ixs.to_instructions()) .await .context("sending liq_token_with_token")?; info!( @@ -433,7 +430,7 @@ impl<'a> LiquidateHelper<'a> { .max_token_liab_transfer(liab_token_index, quote_token_index) .await?; - let liq_ix = self + let mut liq_ixs = self .client .token_liq_bankruptcy_instruction( (self.pubkey, &self.liqee), @@ -442,9 +439,10 @@ impl<'a> LiquidateHelper<'a> { ) .await .context("creating liq_token_bankruptcy")?; + liq_ixs.cu = liq_ixs.cu.max(self.config.compute_limit_for_liq_ix); let txsig = self .client - .send_and_confirm_owner_tx(vec![self.liq_compute_limit_instruction(), liq_ix]) + .send_and_confirm_owner_tx(liq_ixs.to_instructions()) .await .context("sending liq_token_with_token")?; info!( diff --git a/bin/liquidator/src/trigger_tcs.rs b/bin/liquidator/src/trigger_tcs.rs index 36ad39713..8f1047529 100644 --- a/bin/liquidator/src/trigger_tcs.rs +++ b/bin/liquidator/src/trigger_tcs.rs @@ -1122,7 +1122,7 @@ impl Context { }; let liqee = self.account_fetcher.fetch_mango_account(&pending.pubkey)?; - let trigger_ix = self + let mut trigger_ixs = self .mango_client .token_conditional_swap_trigger_instruction( (&pending.pubkey, &liqee), @@ -1134,7 +1134,9 @@ impl Context { &allowed_tokens, ) .await?; - tx_builder.instructions.push(trigger_ix); + tx_builder + .instructions + .append(&mut trigger_ixs.instructions); let txsig = tx_builder .send_and_confirm(&self.mango_client.client) diff --git a/bin/settler/src/settle.rs b/bin/settler/src/settle.rs index a46ccd5c4..9f6c42eaf 100644 --- a/bin/settler/src/settle.rs +++ b/bin/settler/src/settle.rs @@ -6,11 +6,11 @@ use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::health::HealthType; use mango_v4::state::{PerpMarket, PerpMarketIndex}; use mango_v4_client::{ - chain_data, health_cache, prettify_solana_client_error, MangoClient, TransactionBuilder, + chain_data, health_cache, prettify_solana_client_error, MangoClient, PreparedInstructions, + TransactionBuilder, }; use solana_sdk::address_lookup_table_account::AddressLookupTableAccount; use solana_sdk::commitment_config::CommitmentConfig; -use solana_sdk::instruction::Instruction; use solana_sdk::signature::Signature; use solana_sdk::signer::Signer; @@ -180,7 +180,7 @@ impl SettlementState { mango_client, account_fetcher, perp_market_index, - instructions: Vec::new(), + instructions: PreparedInstructions::new(), max_batch_size: 8, // the 1.4M max CU limit if we assume settle ix can be up to around 150k blockhash: mango_client .client @@ -242,7 +242,7 @@ struct SettleBatchProcessor<'a> { mango_client: &'a MangoClient, account_fetcher: &'a chain_data::AccountFetcher, perp_market_index: PerpMarketIndex, - instructions: Vec, + instructions: PreparedInstructions, max_batch_size: usize, blockhash: solana_sdk::hash::Hash, address_lookup_tables: &'a Vec, @@ -254,7 +254,7 @@ impl<'a> SettleBatchProcessor<'a> { let fee_payer = client.fee_payer.clone(); TransactionBuilder { - instructions: self.instructions.clone(), + instructions: self.instructions.clone().to_instructions(), address_lookup_tables: self.address_lookup_tables.clone(), payer: fee_payer.pubkey(), signers: vec![fee_payer], @@ -296,15 +296,19 @@ impl<'a> SettleBatchProcessor<'a> { ) -> anyhow::Result> { let a_value = self.account_fetcher.fetch_mango_account(&account_a)?; let b_value = self.account_fetcher.fetch_mango_account(&account_b)?; - let ix = self.mango_client.perp_settle_pnl_instruction( + let new_ixs = self.mango_client.perp_settle_pnl_instruction( self.perp_market_index, (&account_a, &a_value), (&account_b, &b_value), )?; - self.instructions.push(ix); + let previous = self.instructions.clone(); + self.instructions.append(new_ixs.clone()); // if we exceed the batch limit or tx size limit, send a batch without the new ix - let needs_send = if self.instructions.len() > self.max_batch_size { + let max_cu_per_tx = 1_400_000; + let needs_send = if self.instructions.len() > self.max_batch_size + || self.instructions.cu >= max_cu_per_tx + { true } else { let tx = self.transaction()?; @@ -321,9 +325,9 @@ impl<'a> SettleBatchProcessor<'a> { too_big }; if needs_send { - let ix = self.instructions.pop().unwrap(); + self.instructions = previous; let txsig = self.send().await?; - self.instructions.push(ix); + self.instructions.append(new_ixs); return Ok(txsig); } diff --git a/bin/settler/src/tcs_start.rs b/bin/settler/src/tcs_start.rs index bcbaf395a..a48877d6d 100644 --- a/bin/settler/src/tcs_start.rs +++ b/bin/settler/src/tcs_start.rs @@ -4,8 +4,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use itertools::Itertools; use mango_v4::error::{IsAnchorErrorWithCode, MangoError}; use mango_v4::state::*; +use mango_v4_client::PreparedInstructions; use mango_v4_client::{chain_data, error_tracking::ErrorTracking, MangoClient}; -use solana_sdk::instruction::Instruction; use tracing::*; use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; @@ -93,11 +93,11 @@ impl State { } for startable_chunk in startable.chunks(8) { - let mut instructions = vec![]; + let mut instructions = PreparedInstructions::new(); let mut ix_targets = vec![]; let mut liqor_account = mango_client.mango_account().await?; for (pubkey, tcs_id, incentive_token_index) in startable_chunk { - let ix = match self.make_start_ix(pubkey, *tcs_id).await { + let ixs = match self.make_start_ix(pubkey, *tcs_id).await { Ok(v) => v, Err(e) => { self.errors.record_error( @@ -108,7 +108,7 @@ impl State { continue; } }; - instructions.push(ix); + instructions.append(ixs); ix_targets.push((*pubkey, *tcs_id)); liqor_account.ensure_token_position(*incentive_token_index)?; } @@ -116,7 +116,7 @@ impl State { // Clear newly created token positions, so the liqor account is mostly empty for token_index in startable_chunk.iter().map(|(_, _, ti)| *ti).unique() { let mint = mango_client.context.token(token_index).mint_info.mint; - instructions.append(&mut mango_client.token_withdraw_instructions( + instructions.append(mango_client.token_withdraw_instructions( &liqor_account, mint, u64::MAX, @@ -124,7 +124,10 @@ impl State { )?); } - let txsig = match mango_client.send_and_confirm_owner_tx(instructions).await { + let txsig = match mango_client + .send_and_confirm_owner_tx(instructions.to_instructions()) + .await + { Ok(v) => v, Err(e) => { warn!("error sending transaction: {e:?}"); @@ -154,7 +157,11 @@ impl State { Ok(()) } - async fn make_start_ix(&self, pubkey: &Pubkey, tcs_id: u64) -> anyhow::Result { + async fn make_start_ix( + &self, + pubkey: &Pubkey, + tcs_id: u64, + ) -> anyhow::Result { let account = self.account_fetcher.fetch_mango_account(pubkey).unwrap(); self.mango_client .token_conditional_swap_start_instruction((pubkey, &account), tcs_id) diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index 238062faa..3da8e7ad7 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -28,6 +28,7 @@ use solana_client::rpc_config::RpcSendTransactionConfig; use solana_client::rpc_response::RpcSimulateTransactionResult; use solana_sdk::address_lookup_table_account::AddressLookupTableAccount; use solana_sdk::commitment_config::CommitmentLevel; +use solana_sdk::compute_budget::ComputeBudgetInstruction; use solana_sdk::hash::Hash; use solana_sdk::signer::keypair; use solana_sdk::transaction::TransactionError; @@ -35,6 +36,7 @@ use solana_sdk::transaction::TransactionError; use crate::account_fetcher::*; use crate::context::MangoGroupContext; use crate::gpa::{fetch_anchor_account, fetch_mango_accounts}; +use crate::util::PreparedInstructions; use crate::{jupiter, util}; use anyhow::Context; @@ -302,7 +304,7 @@ impl MangoClient { affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, - ) -> anyhow::Result> { + ) -> anyhow::Result<(Vec, u32)> { let account = self.mango_account().await?; self.context.derive_health_check_remaining_account_metas( &account, @@ -317,7 +319,7 @@ impl MangoClient { liqee: &MangoAccountValue, affected_tokens: &[TokenIndex], writable_banks: &[TokenIndex], - ) -> anyhow::Result> { + ) -> anyhow::Result<(Vec, u32)> { let account = self.mango_account().await?; self.context .derive_health_check_remaining_account_metas_two_accounts( @@ -338,36 +340,42 @@ impl MangoClient { let token_index = token.token_index; let mint_info = token.mint_info; - let health_check_metas = self + let (health_check_metas, health_cu) = self .derive_health_check_remaining_account_metas(vec![token_index], vec![], vec![]) .await?; - let ix = Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::TokenDeposit { - group: self.group(), - account: self.mango_account_address, - owner: self.owner(), - bank: mint_info.first_bank(), - vault: mint_info.first_vault(), - oracle: mint_info.oracle, - token_account: get_associated_token_address(&self.owner(), &mint_info.mint), - token_authority: self.owner(), - token_program: Token::id(), - }, - None, - ); - ams.extend(health_check_metas.into_iter()); - ams + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::TokenDeposit { + group: self.group(), + account: self.mango_account_address, + owner: self.owner(), + bank: mint_info.first_bank(), + vault: mint_info.first_vault(), + oracle: mint_info.oracle, + token_account: get_associated_token_address( + &self.owner(), + &mint_info.mint, + ), + token_authority: self.owner(), + token_program: Token::id(), + }, + None, + ); + ams.extend(health_check_metas.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenDeposit { + amount, + reduce_only, + }), }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenDeposit { - amount, - reduce_only, - }), - }; - self.send_and_confirm_owner_tx(vec![ix]).await + self.instruction_cu(health_cu), + ); + self.send_and_confirm_owner_tx(ixs.to_instructions()).await } /// Creates token withdraw instructions for the MangoClient's account/owner. @@ -379,19 +387,21 @@ impl MangoClient { mint: Pubkey, amount: u64, allow_borrow: bool, - ) -> anyhow::Result> { + ) -> anyhow::Result { let token = self.context.token_by_mint(&mint)?; let token_index = token.token_index; let mint_info = token.mint_info; - let health_check_metas = self.context.derive_health_check_remaining_account_metas( - account, - vec![token_index], - vec![], - vec![], - )?; + let (health_check_metas, health_cu) = + self.context.derive_health_check_remaining_account_metas( + account, + vec![token_index], + vec![], + vec![], + )?; - Ok(vec![ + let ixs = PreparedInstructions::from_vec( + vec![ spl_associated_token_account::instruction::create_associated_token_account_idempotent( &self.owner(), &self.owner(), @@ -425,7 +435,10 @@ impl MangoClient { allow_borrow, }), }, - ]) + ], + self.instruction_cu(health_cu), + ); + Ok(ixs) } pub async fn token_withdraw( @@ -436,7 +449,7 @@ impl MangoClient { ) -> anyhow::Result { let account = self.mango_account().await?; let ixs = self.token_withdraw_instructions(&account, mint, amount, allow_borrow)?; - self.send_and_confirm_owner_tx(ixs).await + self.send_and_confirm_owner_tx(ixs.to_instructions()).await } pub async fn bank_oracle_price(&self, token_index: TokenIndex) -> anyhow::Result { @@ -532,7 +545,7 @@ impl MangoClient { order_type: Serum3OrderType, client_order_id: u64, limit: u16, - ) -> anyhow::Result { + ) -> anyhow::Result { let s3 = self.context.serum3(market_index); let base = self.context.serum3_base_token(market_index); let quote = self.context.serum3_quote_token(market_index); @@ -541,60 +554,63 @@ impl MangoClient { .expect("oo is created") .open_orders; - let health_check_metas = self.context.derive_health_check_remaining_account_metas( - account, - vec![], - vec![], - vec![], - )?; + let (health_check_metas, health_cu) = self + .context + .derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; let payer_mint_info = match side { Serum3Side::Bid => quote.mint_info, Serum3Side::Ask => base.mint_info, }; - let ix = Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3PlaceOrder { - group: self.group(), - account: self.mango_account_address, - open_orders, - payer_bank: payer_mint_info.first_bank(), - payer_vault: payer_mint_info.first_vault(), - payer_oracle: payer_mint_info.oracle, - serum_market: s3.address, - serum_program: s3.market.serum_program, - serum_market_external: s3.market.serum_market_external, - market_bids: s3.bids, - market_asks: s3.asks, - market_event_queue: s3.event_q, - market_request_queue: s3.req_q, - market_base_vault: s3.coin_vault, - market_quote_vault: s3.pc_vault, - market_vault_signer: s3.vault_signer, - owner: self.owner(), - token_program: Token::id(), + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3PlaceOrder { + group: self.group(), + account: self.mango_account_address, + open_orders, + payer_bank: payer_mint_info.first_bank(), + payer_vault: payer_mint_info.first_vault(), + payer_oracle: payer_mint_info.oracle, + serum_market: s3.address, + serum_program: s3.market.serum_program, + serum_market_external: s3.market.serum_market_external, + market_bids: s3.bids, + market_asks: s3.asks, + market_event_queue: s3.event_q, + market_request_queue: s3.req_q, + market_base_vault: s3.coin_vault, + market_quote_vault: s3.pc_vault, + market_vault_signer: s3.vault_signer, + owner: self.owner(), + token_program: Token::id(), + }, + None, + ); + ams.extend(health_check_metas.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::Serum3PlaceOrder { + side, + limit_price, + max_base_qty, + max_native_quote_qty_including_fees, + self_trade_behavior, + order_type, + client_order_id, + limit, }, - None, - ); - ams.extend(health_check_metas.into_iter()); - ams + ), }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::Serum3PlaceOrder { - side, - limit_price, - max_base_qty, - max_native_quote_qty_including_fees, - self_trade_behavior, - order_type, - client_order_id, - limit, - }), - }; + self.instruction_cu(health_cu) + + self.context.compute_estimates.cu_per_serum3_order_match * limit as u32, + ); - Ok(ix) + Ok(ixs) } #[allow(clippy::too_many_arguments)] @@ -612,7 +628,7 @@ impl MangoClient { ) -> anyhow::Result { let account = self.mango_account().await?; let market_index = self.context.serum3_market_index(name); - let ix = self.serum3_place_order_instruction( + let ixs = self.serum3_place_order_instruction( &account, market_index, side, @@ -624,7 +640,7 @@ impl MangoClient { client_order_id, limit, )?; - self.send_and_confirm_owner_tx(vec![ix]).await + self.send_and_confirm_owner_tx(ixs.to_instructions()).await } pub async fn serum3_settle_funds(&self, name: &str) -> anyhow::Result { @@ -676,33 +692,37 @@ impl MangoClient { account: &MangoAccountValue, market_index: Serum3MarketIndex, limit: u8, - ) -> anyhow::Result { + ) -> anyhow::Result { let s3 = self.context.serum3(market_index); let open_orders = account.serum3_orders(market_index)?.open_orders; - let ix = Instruction { - program_id: mango_v4::id(), - accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3CancelAllOrders { - group: self.group(), - account: self.mango_account_address, - open_orders, - market_bids: s3.bids, - market_asks: s3.asks, - market_event_queue: s3.event_q, - serum_market: s3.address, - serum_program: s3.market.serum_program, - serum_market_external: s3.market.serum_market_external, - owner: self.owner(), - }, - None, - ), - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3CancelAllOrders { limit }, - ), - }; + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3CancelAllOrders { + group: self.group(), + account: self.mango_account_address, + open_orders, + market_bids: s3.bids, + market_asks: s3.asks, + market_event_queue: s3.event_q, + serum_market: s3.address, + serum_program: s3.market.serum_program, + serum_market_external: s3.market.serum_market_external, + owner: self.owner(), + }, + None, + ), + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::Serum3CancelAllOrders { limit }, + ), + }, + self.instruction_cu(0) + + self.context.compute_estimates.cu_per_serum3_order_cancel * limit as u32, + ); - Ok(ix) + Ok(ixs) } pub async fn serum3_cancel_all_orders( @@ -746,44 +766,50 @@ impl MangoClient { let base = self.context.serum3_base_token(market_index); let quote = self.context.serum3_quote_token(market_index); - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .context .derive_health_check_remaining_account_metas(liqee.1, vec![], vec![], vec![]) .unwrap(); - let ix = Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::Serum3LiqForceCancelOrders { - group: self.group(), - account: *liqee.0, - open_orders: *open_orders, - serum_market: s3.address, - serum_program: s3.market.serum_program, - serum_market_external: s3.market.serum_market_external, - market_bids: s3.bids, - market_asks: s3.asks, - market_event_queue: s3.event_q, - market_base_vault: s3.coin_vault, - market_quote_vault: s3.pc_vault, - market_vault_signer: s3.vault_signer, - quote_bank: quote.mint_info.first_bank(), - quote_vault: quote.mint_info.first_vault(), - base_bank: base.mint_info.first_bank(), - base_vault: base.mint_info.first_vault(), - token_program: Token::id(), - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams + let limit = 5; + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3LiqForceCancelOrders { + group: self.group(), + account: *liqee.0, + open_orders: *open_orders, + serum_market: s3.address, + serum_program: s3.market.serum_program, + serum_market_external: s3.market.serum_market_external, + market_bids: s3.bids, + market_asks: s3.asks, + market_event_queue: s3.event_q, + market_base_vault: s3.coin_vault, + market_quote_vault: s3.pc_vault, + market_vault_signer: s3.vault_signer, + quote_bank: quote.mint_info.first_bank(), + quote_vault: quote.mint_info.first_vault(), + base_bank: base.mint_info.first_bank(), + base_vault: base.mint_info.first_vault(), + token_program: Token::id(), + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::Serum3LiqForceCancelOrders { limit }, + ), }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3LiqForceCancelOrders { limit: 5 }, - ), - }; - self.send_and_confirm_permissionless_tx(vec![ix]).await + self.instruction_cu(health_cu) + + self.context.compute_estimates.cu_per_serum3_order_cancel * limit as u32, + ); + self.send_and_confirm_permissionless_tx(ixs.to_instructions()) + .await } pub async fn serum3_cancel_order( @@ -844,49 +870,56 @@ impl MangoClient { expiry_timestamp: u64, limit: u8, self_trade_behavior: SelfTradeBehavior, - ) -> anyhow::Result { + ) -> anyhow::Result { let perp = self.context.perp(market_index); - let health_remaining_metas = self.context.derive_health_check_remaining_account_metas( - account, - vec![], - vec![], - vec![market_index], - )?; + let (health_remaining_metas, health_cu) = + self.context.derive_health_check_remaining_account_metas( + account, + vec![], + vec![], + vec![market_index], + )?; - let ix = Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpPlaceOrder { - group: self.group(), - account: self.mango_account_address, - owner: self.owner(), - perp_market: perp.address, - bids: perp.market.bids, - asks: perp.market.asks, - event_queue: perp.market.event_queue, - oracle: perp.market.oracle, + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpPlaceOrder { + group: self.group(), + account: self.mango_account_address, + owner: self.owner(), + perp_market: perp.address, + bids: perp.market.bids, + asks: perp.market.asks, + event_queue: perp.market.event_queue, + oracle: perp.market.oracle, + }, + None, + ); + ams.extend(health_remaining_metas.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpPlaceOrderV2 { + side, + price_lots, + max_base_lots, + max_quote_lots, + client_order_id, + order_type, + reduce_only, + expiry_timestamp, + limit, + self_trade_behavior, }, - None, - ); - ams.extend(health_remaining_metas.into_iter()); - ams + ), }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpPlaceOrderV2 { - side, - price_lots, - max_base_lots, - max_quote_lots, - client_order_id, - order_type, - reduce_only, - expiry_timestamp, - limit, - self_trade_behavior, - }), - }; + self.instruction_cu(health_cu) + + self.context.compute_estimates.cu_per_perp_order_match * limit as u32, + ); - Ok(ix) + Ok(ixs) } #[allow(clippy::too_many_arguments)] @@ -905,7 +938,7 @@ impl MangoClient { self_trade_behavior: SelfTradeBehavior, ) -> anyhow::Result { let account = self.mango_account().await?; - let ix = self.perp_place_order_instruction( + let ixs = self.perp_place_order_instruction( &account, market_index, side, @@ -919,36 +952,40 @@ impl MangoClient { limit, self_trade_behavior, )?; - self.send_and_confirm_owner_tx(vec![ix]).await + self.send_and_confirm_owner_tx(ixs.to_instructions()).await } pub fn perp_cancel_all_orders_instruction( &self, market_index: PerpMarketIndex, limit: u8, - ) -> anyhow::Result { + ) -> anyhow::Result { let perp = self.context.perp(market_index); - let ix = Instruction { - program_id: mango_v4::id(), - accounts: { - anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpCancelAllOrders { - group: self.group(), - account: self.mango_account_address, - owner: self.owner(), - perp_market: perp.address, - bids: perp.market.bids, - asks: perp.market.asks, - }, - None, - ) + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpCancelAllOrders { + group: self.group(), + account: self.mango_account_address, + owner: self.owner(), + perp_market: perp.address, + bids: perp.market.bids, + asks: perp.market.asks, + }, + None, + ) + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpCancelAllOrders { limit }, + ), }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpCancelAllOrders { - limit, - }), - }; - Ok(ix) + self.instruction_cu(0) + + self.context.compute_estimates.cu_per_perp_order_cancel * limit as u32, + ); + Ok(ixs) } pub async fn perp_deactivate_position( @@ -957,30 +994,33 @@ impl MangoClient { ) -> anyhow::Result { let perp = self.context.perp(market_index); - let health_check_metas = self + let (health_check_metas, health_cu) = self .derive_health_check_remaining_account_metas(vec![], vec![], vec![]) .await?; - let ix = Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpDeactivatePosition { - group: self.group(), - account: self.mango_account_address, - owner: self.owner(), - perp_market: perp.address, - }, - None, - ); - ams.extend(health_check_metas.into_iter()); - ams + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpDeactivatePosition { + group: self.group(), + account: self.mango_account_address, + owner: self.owner(), + perp_market: perp.address, + }, + None, + ); + ams.extend(health_check_metas.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpDeactivatePosition {}, + ), }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::PerpDeactivatePosition {}, - ), - }; - self.send_and_confirm_owner_tx(vec![ix]).await + self.instruction_cu(health_cu), + ); + self.send_and_confirm_owner_tx(ixs.to_instructions()).await } pub fn perp_settle_pnl_instruction( @@ -988,11 +1028,11 @@ impl MangoClient { market_index: PerpMarketIndex, account_a: (&Pubkey, &MangoAccountValue), account_b: (&Pubkey, &MangoAccountValue), - ) -> anyhow::Result { + ) -> anyhow::Result { let perp = self.context.perp(market_index); let settlement_token = self.context.token(perp.market.settle_token_index); - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .context .derive_health_check_remaining_account_metas_two_accounts( account_a.1, @@ -1002,28 +1042,32 @@ impl MangoClient { ) .unwrap(); - Ok(Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpSettlePnl { - group: self.group(), - settler: self.mango_account_address, - settler_owner: self.owner(), - perp_market: perp.address, - account_a: *account_a.0, - account_b: *account_b.0, - oracle: perp.market.oracle, - settle_bank: settlement_token.mint_info.first_bank(), - settle_oracle: settlement_token.mint_info.oracle, - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpSettlePnl { + group: self.group(), + settler: self.mango_account_address, + settler_owner: self.owner(), + perp_market: perp.address, + account_a: *account_a.0, + account_b: *account_b.0, + oracle: perp.market.oracle, + settle_bank: settlement_token.mint_info.first_bank(), + settle_oracle: settlement_token.mint_info.oracle, + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl {}), }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl {}), - }) + self.instruction_cu(health_cu), + ); + Ok(ixs) } pub async fn perp_settle_pnl( @@ -1032,8 +1076,9 @@ impl MangoClient { account_a: (&Pubkey, &MangoAccountValue), account_b: (&Pubkey, &MangoAccountValue), ) -> anyhow::Result { - let ix = self.perp_settle_pnl_instruction(market_index, account_a, account_b)?; - self.send_and_confirm_permissionless_tx(vec![ix]).await + let ixs = self.perp_settle_pnl_instruction(market_index, account_a, account_b)?; + self.send_and_confirm_permissionless_tx(ixs.to_instructions()) + .await } pub async fn perp_liq_force_cancel_orders( @@ -1043,32 +1088,38 @@ impl MangoClient { ) -> anyhow::Result { let perp = self.context.perp(market_index); - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .context .derive_health_check_remaining_account_metas(liqee.1, vec![], vec![], vec![]) .unwrap(); - let ix = Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::PerpLiqForceCancelOrders { - group: self.group(), - account: *liqee.0, - perp_market: perp.address, - bids: perp.market.bids, - asks: perp.market.asks, - }, - None, - ); - ams.extend(health_remaining_ams.into_iter()); - ams + let limit = 5; + let ixs = PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::PerpLiqForceCancelOrders { + group: self.group(), + account: *liqee.0, + perp_market: perp.address, + bids: perp.market.bids, + asks: perp.market.asks, + }, + None, + ); + ams.extend(health_remaining_ams.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::PerpLiqForceCancelOrders { limit }, + ), }, - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::PerpLiqForceCancelOrders { limit: 5 }, - ), - }; - self.send_and_confirm_permissionless_tx(vec![ix]).await + self.instruction_cu(health_cu) + + self.context.compute_estimates.cu_per_perp_order_cancel * limit as u32, + ); + self.send_and_confirm_permissionless_tx(ixs.to_instructions()) + .await } pub async fn perp_liq_base_or_positive_pnl_instruction( @@ -1077,11 +1128,11 @@ impl MangoClient { market_index: PerpMarketIndex, max_base_transfer: i64, max_pnl_transfer: u64, - ) -> anyhow::Result { + ) -> anyhow::Result { let perp = self.context.perp(market_index); let settle_token_info = self.context.token(perp.market.settle_token_index); - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .derive_liquidation_health_check_remaining_account_metas(liqee.1, &[], &[]) .await .unwrap(); @@ -1113,7 +1164,10 @@ impl MangoClient { }, ), }; - Ok(ix) + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(health_cu), + )) } pub async fn perp_liq_negative_pnl_or_bankruptcy_instruction( @@ -1121,7 +1175,7 @@ impl MangoClient { liqee: (&Pubkey, &MangoAccountValue), market_index: PerpMarketIndex, max_liab_transfer: u64, - ) -> anyhow::Result { + ) -> anyhow::Result { let group = account_fetcher_fetch_anchor_account::( &*self.account_fetcher, &self.context.group, @@ -1132,7 +1186,7 @@ impl MangoClient { let settle_token_info = self.context.token(perp.market.settle_token_index); let insurance_token_info = self.context.token(INSURANCE_TOKEN_INDEX); - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, &[INSURANCE_TOKEN_INDEX], @@ -1170,7 +1224,10 @@ impl MangoClient { &mango_v4::instruction::PerpLiqNegativePnlOrBankruptcyV2 { max_liab_transfer }, ), }; - Ok(ix) + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(health_cu), + )) } // @@ -1183,8 +1240,8 @@ impl MangoClient { asset_token_index: TokenIndex, liab_token_index: TokenIndex, max_liab_transfer: I80F48, - ) -> anyhow::Result { - let health_remaining_ams = self + ) -> anyhow::Result { + let (health_remaining_ams, health_cu) = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, &[], @@ -1214,7 +1271,10 @@ impl MangoClient { max_liab_transfer, }), }; - Ok(ix) + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(health_cu), + )) } pub async fn token_liq_bankruptcy_instruction( @@ -1222,7 +1282,7 @@ impl MangoClient { liqee: (&Pubkey, &MangoAccountValue), liab_token_index: TokenIndex, max_liab_transfer: I80F48, - ) -> anyhow::Result { + ) -> anyhow::Result { let quote_token_index = 0; let quote_info = self.context.token(quote_token_index); @@ -1235,7 +1295,7 @@ impl MangoClient { .map(|bank_pubkey| util::to_writable_account_meta(*bank_pubkey)) .collect::>(); - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, &[INSURANCE_TOKEN_INDEX], @@ -1274,7 +1334,10 @@ impl MangoClient { max_liab_transfer, }), }; - Ok(ix) + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(health_cu), + )) } pub async fn token_conditional_swap_trigger_instruction( @@ -1286,7 +1349,7 @@ impl MangoClient { min_buy_token: u64, min_taker_price: f32, extra_affected_tokens: &[TokenIndex], - ) -> anyhow::Result { + ) -> anyhow::Result { let (tcs_index, tcs) = liqee .1 .token_conditional_swap_by_id(token_conditional_swap_id)?; @@ -1296,7 +1359,7 @@ impl MangoClient { .chain(&[tcs.buy_token_index, tcs.sell_token_index]) .copied() .collect_vec(); - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .derive_liquidation_health_check_remaining_account_metas( liqee.1, &affected_tokens, @@ -1331,20 +1394,23 @@ impl MangoClient { }, ), }; - Ok(ix) + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(health_cu), + )) } pub async fn token_conditional_swap_start_instruction( &self, account: (&Pubkey, &MangoAccountValue), token_conditional_swap_id: u64, - ) -> anyhow::Result { + ) -> anyhow::Result { let (tcs_index, tcs) = account .1 .token_conditional_swap_by_id(token_conditional_swap_id)?; let affected_tokens = vec![tcs.buy_token_index, tcs.sell_token_index]; - let health_remaining_ams = self + let (health_remaining_ams, health_cu) = self .derive_health_check_remaining_account_metas(vec![], affected_tokens, vec![]) .await .unwrap(); @@ -1371,7 +1437,10 @@ impl MangoClient { }, ), }; - Ok(ix) + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(health_cu), + )) } // health region @@ -1382,13 +1451,14 @@ impl MangoClient { affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, - ) -> anyhow::Result { - let health_remaining_metas = self.context.derive_health_check_remaining_account_metas( - account, - affected_tokens, - writable_banks, - affected_perp_markets, - )?; + ) -> anyhow::Result { + let (health_remaining_metas, _health_cu) = + self.context.derive_health_check_remaining_account_metas( + account, + affected_tokens, + writable_banks, + affected_perp_markets, + )?; let ix = Instruction { program_id: mango_v4::id(), @@ -1407,7 +1477,11 @@ impl MangoClient { data: anchor_lang::InstructionData::data(&mango_v4::instruction::HealthRegionBegin {}), }; - Ok(ix) + // There's only a single health computation in End + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(0), + )) } pub fn health_region_end_instruction( @@ -1416,13 +1490,14 @@ impl MangoClient { affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, - ) -> anyhow::Result { - let health_remaining_metas = self.context.derive_health_check_remaining_account_metas( - account, - affected_tokens, - writable_banks, - affected_perp_markets, - )?; + ) -> anyhow::Result { + let (health_remaining_metas, health_cu) = + self.context.derive_health_check_remaining_account_metas( + account, + affected_tokens, + writable_banks, + affected_perp_markets, + )?; let ix = Instruction { program_id: mango_v4::id(), @@ -1439,7 +1514,10 @@ impl MangoClient { data: anchor_lang::InstructionData::data(&mango_v4::instruction::HealthRegionEnd {}), }; - Ok(ix) + Ok(PreparedInstructions::from_single( + ix, + self.instruction_cu(health_cu), + )) } // jupiter @@ -1538,6 +1616,10 @@ impl MangoClient { Ok((compiled_ix, address_lookup_tables)) } + fn instruction_cu(&self, health_cu: u32) -> u32 { + self.context.compute_estimates.cu_per_mango_instruction + health_cu + } + pub async fn send_and_confirm_owner_tx( &self, instructions: Vec, @@ -1644,14 +1726,13 @@ impl TransactionBuilder { } fn instructions_with_cu_budget(&self) -> Vec { - use solana_sdk::compute_budget::{self, ComputeBudgetInstruction}; let mut ixs = self.instructions.clone(); let mut has_compute_unit_price = false; let mut has_compute_unit_limit = false; let mut cu_instructions = 0; for ix in ixs.iter() { - if ix.program_id != compute_budget::id() { + if ix.program_id != solana_sdk::compute_budget::id() { continue; } cu_instructions += 1; diff --git a/lib/client/src/context.rs b/lib/client/src/context.rs index 4a2172e11..132867438 100644 --- a/lib/client/src/context.rs +++ b/lib/client/src/context.rs @@ -57,6 +57,55 @@ pub struct PerpMarketContext { pub market: PerpMarket, } +pub struct ComputeEstimates { + pub cu_per_mango_instruction: u32, + pub health_cu_per_token: u32, + pub health_cu_per_perp: u32, + pub health_cu_per_serum: u32, + pub cu_per_serum3_order_match: u32, + pub cu_per_serum3_order_cancel: u32, + pub cu_per_perp_order_match: u32, + pub cu_per_perp_order_cancel: u32, +} + +impl Default for ComputeEstimates { + fn default() -> Self { + Self { + cu_per_mango_instruction: 100_000, + health_cu_per_token: 5000, + health_cu_per_perp: 8000, + health_cu_per_serum: 6000, + // measured around 1.5k, see test_serum_compute + cu_per_serum3_order_match: 3_000, + // measured around 11k, see test_serum_compute + cu_per_serum3_order_cancel: 20_000, + // measured around 3.5k, see test_perp_compute + cu_per_perp_order_match: 7_000, + // measured around 3.5k, see test_perp_compute + cu_per_perp_order_cancel: 7_000, + } + } +} + +impl ComputeEstimates { + pub fn health_for_counts(&self, tokens: usize, perps: usize, serums: usize) -> u32 { + let tokens: u32 = tokens.try_into().unwrap(); + let perps: u32 = perps.try_into().unwrap(); + let serums: u32 = serums.try_into().unwrap(); + tokens * self.health_cu_per_token + + perps * self.health_cu_per_perp + + serums * self.health_cu_per_serum + } + + pub fn health_for_account(&self, account: &MangoAccountValue) -> u32 { + self.health_for_counts( + account.active_token_positions().count(), + account.active_perp_positions().count(), + account.active_serum3_orders().count(), + ) + } +} + pub struct MangoGroupContext { pub group: Pubkey, @@ -70,6 +119,8 @@ pub struct MangoGroupContext { pub perp_market_indexes_by_name: HashMap, pub address_lookup_tables: Vec, + + pub compute_estimates: ComputeEstimates, } impl MangoGroupContext { @@ -235,6 +286,7 @@ impl MangoGroupContext { perp_markets, perp_market_indexes_by_name, address_lookup_tables, + compute_estimates: ComputeEstimates::default(), }) } @@ -244,7 +296,7 @@ impl MangoGroupContext { affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, - ) -> anyhow::Result> { + ) -> anyhow::Result<(Vec, u32)> { let mut account = account.clone(); for affected_token_index in affected_tokens.iter().chain(writable_banks.iter()) { account.ensure_token_position(*affected_token_index)?; @@ -283,7 +335,7 @@ impl MangoGroupContext { is_signer: false, }; - Ok(banks + let accounts = banks .iter() .map(|&(pubkey, is_writable)| AccountMeta { pubkey, @@ -294,7 +346,11 @@ impl MangoGroupContext { .chain(perp_markets.map(to_account_meta)) .chain(perp_oracles.map(to_account_meta)) .chain(serum_oos.map(to_account_meta)) - .collect()) + .collect(); + + let cu = self.compute_estimates.health_for_account(&account); + + Ok((accounts, cu)) } pub fn derive_health_check_remaining_account_metas_two_accounts( @@ -303,7 +359,7 @@ impl MangoGroupContext { account2: &MangoAccountValue, affected_tokens: &[TokenIndex], writable_banks: &[TokenIndex], - ) -> anyhow::Result> { + ) -> anyhow::Result<(Vec, u32)> { // figure out all the banks/oracles that need to be passed for the health check let mut banks = vec![]; let mut oracles = vec![]; @@ -345,7 +401,7 @@ impl MangoGroupContext { is_signer: false, }; - Ok(banks + let accounts = banks .iter() .map(|(pubkey, is_writable)| AccountMeta { pubkey: *pubkey, @@ -356,7 +412,33 @@ impl MangoGroupContext { .chain(perp_markets.map(to_account_meta)) .chain(perp_oracles.map(to_account_meta)) .chain(serum_oos.map(to_account_meta)) - .collect()) + .collect(); + + // Since health is likely to be computed separately for both accounts, we don't use the + // unique'd counts to estimate health cu cost. + let account1_token_count = account1 + .active_token_positions() + .map(|ta| ta.token_index) + .chain(affected_tokens.iter().copied()) + .unique() + .count(); + let account2_token_count = account2 + .active_token_positions() + .map(|ta| ta.token_index) + .chain(affected_tokens.iter().copied()) + .unique() + .count(); + let cu = self.compute_estimates.health_for_counts( + account1_token_count, + account1.active_perp_positions().count(), + account1.active_serum3_orders().count(), + ) + self.compute_estimates.health_for_counts( + account2_token_count, + account2.active_perp_positions().count(), + account2.active_serum3_orders().count(), + ); + + Ok((accounts, cu)) } pub async fn new_tokens_listed(&self, rpc: &RpcClientAsync) -> anyhow::Result { diff --git a/lib/client/src/health_cache.rs b/lib/client/src/health_cache.rs index 3b8ebacd6..a54055176 100644 --- a/lib/client/src/health_cache.rs +++ b/lib/client/src/health_cache.rs @@ -13,7 +13,7 @@ pub async fn new( let active_token_len = account.active_token_positions().count(); let active_perp_len = account.active_perp_positions().count(); - let metas = + let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; let accounts: anyhow::Result> = stream::iter(metas.iter()) .then(|meta| async { @@ -44,7 +44,7 @@ pub fn new_sync( let active_token_len = account.active_token_positions().count(); let active_perp_len = account.active_perp_positions().count(); - let metas = + let (metas, _health_cu) = context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; let accounts = metas .iter() diff --git a/lib/client/src/jupiter/v4.rs b/lib/client/src/jupiter/v4.rs index eaa51cdfb..d5c2b24d7 100644 --- a/lib/client/src/jupiter/v4.rs +++ b/lib/client/src/jupiter/v4.rs @@ -249,7 +249,7 @@ impl<'a> JupiterV4<'a> { let num_loans: u8 = loan_amounts.len().try_into().unwrap(); // This relies on the fact that health account banks will be identical to the first_bank above! - let health_ams = self + let (health_ams, _health_cu) = self .mango_client .derive_health_check_remaining_account_metas( vec![source_token.token_index, target_token.token_index], diff --git a/lib/client/src/jupiter/v6.rs b/lib/client/src/jupiter/v6.rs index dc8e3a065..ce0d32a9d 100644 --- a/lib/client/src/jupiter/v6.rs +++ b/lib/client/src/jupiter/v6.rs @@ -256,7 +256,7 @@ impl<'a> JupiterV6<'a> { let num_loans: u8 = loan_amounts.len().try_into().unwrap(); // This relies on the fact that health account banks will be identical to the first_bank above! - let health_ams = self + let (health_ams, _health_cu) = self .mango_client .derive_health_check_remaining_account_metas( vec![source_token.token_index, target_token.token_index], diff --git a/lib/client/src/util.rs b/lib/client/src/util.rs index 44782fe06..4ccf9c566 100644 --- a/lib/client/src/util.rs +++ b/lib/client/src/util.rs @@ -1,6 +1,8 @@ use solana_client::{ client_error::Result as ClientResult, rpc_client::RpcClient, rpc_request::RpcError, }; +use solana_sdk::compute_budget::ComputeBudgetInstruction; +use solana_sdk::instruction::Instruction; use solana_sdk::transaction::Transaction; use solana_sdk::{ clock::Slot, commitment_config::CommitmentConfig, signature::Signature, @@ -143,3 +145,58 @@ pub fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta { is_signer: false, } } + +#[derive(Default, Clone)] +pub struct PreparedInstructions { + pub instructions: Vec, + pub cu: u32, +} + +impl PreparedInstructions { + pub fn new() -> Self { + Self { + instructions: vec![], + cu: 0, + } + } + + pub fn from_vec(instructions: Vec, cu: u32) -> Self { + Self { instructions, cu } + } + + pub fn from_single(instruction: Instruction, cu: u32) -> Self { + Self { + instructions: vec![instruction], + cu, + } + } + + pub fn push(&mut self, ix: Instruction, cu: u32) { + self.instructions.push(ix); + self.cu += cu; + } + + pub fn append(&mut self, mut other: Self) { + self.instructions.append(&mut other.instructions); + self.cu += other.cu; + } + + pub fn to_instructions(self) -> Vec { + let mut ixs = self.instructions; + ixs.insert(0, ComputeBudgetInstruction::set_compute_unit_limit(self.cu)); + ixs + } + + pub fn is_empty(&self) -> bool { + self.instructions.is_empty() + } + + pub fn clear(&mut self) { + self.instructions.clear(); + self.cu = 0; + } + + pub fn len(&self) -> usize { + self.instructions.len() + } +} diff --git a/programs/mango-v4/tests/cases/test_perp.rs b/programs/mango-v4/tests/cases/test_perp.rs index 6c47a8a52..414fd84a9 100644 --- a/programs/mango-v4/tests/cases/test_perp.rs +++ b/programs/mango-v4/tests/cases/test_perp.rs @@ -217,6 +217,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { account: account_0, perp_market, owner, + limit: 10, }, ) .await @@ -268,6 +269,7 @@ async fn test_perp_fixed() -> Result<(), TransportError> { account: account_0, perp_market, owner, + limit: 10, }, ) .await @@ -1212,6 +1214,231 @@ async fn test_perp_reducing_when_liquidatable() -> Result<(), TransportError> { Ok(()) } +#[tokio::test] +async fn test_perp_compute() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = TestKeypair::new(); + let owner = context.users[0].key; + let payer = context.users[1].key; + let mints = &context.mints[0..2]; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints: mints.to_vec(), + ..GroupWithTokensConfig::default() + } + .create(solana) + .await; + + let deposit_amount = 100_000; + let account_0 = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + let account_1 = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + + // + // TEST: Create a perp market + // + let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx( + solana, + PerpCreateMarketInstruction { + group, + admin, + payer, + perp_market_index: 0, + quote_lot_size: 10, + base_lot_size: 100, + maint_base_asset_weight: 0.975, + init_base_asset_weight: 0.95, + maint_base_liab_weight: 1.025, + init_base_liab_weight: 1.05, + base_liquidation_fee: 0.012, + maker_fee: 0.0000, + taker_fee: 0.0000, + settle_pnl_limit_factor: -1.0, + settle_pnl_limit_window_size_ts: 24 * 60 * 60, + ..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await + }, + ) + .await + .unwrap(); + + let perp_market_data = solana.get_account::(perp_market).await; + let price_lots = perp_market_data.native_price_to_lot(I80F48::from(1000)); + set_perp_stub_oracle_price(solana, group, perp_market, &tokens[1], admin, 1000.0).await; + + // + // TEST: check compute per order match + // + + for limit in 1..6 { + for bid in price_lots..price_lots + 6 { + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots: bid, + max_base_lots: 1, + client_order_id: 5, + ..PerpPlaceOrderInstruction::default() + }, + ) + .await + .unwrap(); + } + let result = send_tx_get_metadata( + solana, + PerpPlaceOrderInstruction { + account: account_1, + perp_market, + owner, + side: Side::Ask, + price_lots, + max_base_lots: 10, + client_order_id: 6, + limit, + ..PerpPlaceOrderInstruction::default() + }, + ) + .await + .unwrap(); + println!( + "CU for perp_place_order matching {limit} orders in sequence: {}", + result.metadata.unwrap().compute_units_consumed + ); + + send_tx( + solana, + PerpCancelAllOrdersInstruction { + account: account_0, + perp_market, + owner, + limit: 10, + }, + ) + .await + .unwrap(); + send_tx( + solana, + PerpCancelAllOrdersInstruction { + account: account_1, + perp_market, + owner, + limit: 10, + }, + ) + .await + .unwrap(); + + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0, account_1], + }, + ) + .await + .unwrap(); + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0, account_1], + }, + ) + .await + .unwrap(); + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0, account_1], + }, + ) + .await + .unwrap(); + } + + // + // TEST: check compute per order cancel + // + + for count in 1..6 { + for bid in price_lots..price_lots + count { + send_tx( + solana, + PerpPlaceOrderInstruction { + account: account_0, + perp_market, + owner, + side: Side::Bid, + price_lots: bid, + max_base_lots: 1, + client_order_id: 5, + ..PerpPlaceOrderInstruction::default() + }, + ) + .await + .unwrap(); + } + let result = send_tx_get_metadata( + solana, + PerpCancelAllOrdersInstruction { + account: account_0, + perp_market, + owner, + limit: 10, + }, + ) + .await + .unwrap(); + println!( + "CU for perp_cancel_all_orders matching {count} orders: {}", + result.metadata.unwrap().compute_units_consumed + ); + + send_tx( + solana, + PerpConsumeEventsInstruction { + perp_market, + mango_accounts: vec![account_0], + }, + ) + .await + .unwrap(); + } + + Ok(()) +} + async fn assert_no_perp_orders(solana: &SolanaCookie, account_0: Pubkey) { let mango_account_0 = solana.get_account::(account_0).await; diff --git a/programs/mango-v4/tests/cases/test_serum.rs b/programs/mango-v4/tests/cases/test_serum.rs index 5be4145d8..9ad854515 100644 --- a/programs/mango-v4/tests/cases/test_serum.rs +++ b/programs/mango-v4/tests/cases/test_serum.rs @@ -138,10 +138,11 @@ impl SerumOrderPlacer { let open_orders = self.serum.load_open_orders(self.open_orders).await; let orders = open_orders.orders; for (idx, order_id) in orders.iter().enumerate() { - if *order_id == 0 { + let mask = 1u128 << idx; + if open_orders.free_slot_bits & mask != 0 { continue; } - let side = if open_orders.is_bid_bits & (1u128 << idx) == 0 { + let side = if open_orders.is_bid_bits & mask == 0 { Serum3Side::Ask } else { Serum3Side::Bid @@ -1358,6 +1359,128 @@ async fn test_serum_track_reserved_deposits() -> Result<(), TransportError> { Ok(()) } +#[tokio::test] +async fn test_serum_compute() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(150_000); // Serum3PlaceOrder needs lots + let context = test_builder.start_default().await; + let solana = &context.solana; + + // + // SETUP: Create a group, accounts, market etc + // + let deposit_amount = 100000; + let CommonSetup { + serum_market_cookie, + mut order_placer, + order_placer2, + .. + } = common_setup(&context, deposit_amount).await; + + // + // TEST: check compute per serum match + // + + for limit in 1..6 { + order_placer.bid_maker(1.0, 100).await.unwrap(); + order_placer.bid_maker(1.1, 100).await.unwrap(); + order_placer.bid_maker(1.2, 100).await.unwrap(); + order_placer.bid_maker(1.3, 100).await.unwrap(); + order_placer.bid_maker(1.4, 100).await.unwrap(); + + let result = send_tx_get_metadata( + solana, + Serum3PlaceOrderInstruction { + side: Serum3Side::Ask, + limit_price: (1.0 * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100) + max_base_qty: 500 / 100, // in base lot (100) + max_native_quote_qty_including_fees: (1.0 * (500 as f64)) as u64, + self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction, + order_type: Serum3OrderType::Limit, + client_order_id: 0, + limit, + account: order_placer2.account, + owner: order_placer2.owner, + serum_market: order_placer2.serum_market, + }, + ) + .await + .unwrap(); + println!( + "CU for serum_place_order matching {limit} orders in sequence: {}", + result.metadata.unwrap().compute_units_consumed + ); + + // many events need processing + context + .serum + .consume_spot_events( + &serum_market_cookie, + &[order_placer.open_orders, order_placer2.open_orders], + ) + .await; + context + .serum + .consume_spot_events( + &serum_market_cookie, + &[order_placer.open_orders, order_placer2.open_orders], + ) + .await; + context + .serum + .consume_spot_events( + &serum_market_cookie, + &[order_placer.open_orders, order_placer2.open_orders], + ) + .await; + order_placer.cancel_all().await; + order_placer2.cancel_all().await; + context + .serum + .consume_spot_events( + &serum_market_cookie, + &[order_placer.open_orders, order_placer2.open_orders], + ) + .await; + } + + // + // TEST: check compute per serum cancel + // + + for limit in 1..6 { + for i in 0..limit { + order_placer.bid_maker(1.0 + i as f64, 100).await.unwrap(); + } + + let result = send_tx_get_metadata( + solana, + Serum3CancelAllOrdersInstruction { + account: order_placer.account, + owner: order_placer.owner, + serum_market: order_placer.serum_market, + limit: 10, + }, + ) + .await + .unwrap(); + println!( + "CU for serum_cancel_all_order for {limit} orders: {}", + result.metadata.unwrap().compute_units_consumed + ); + + context + .serum + .consume_spot_events( + &serum_market_cookie, + &[order_placer.open_orders, order_placer2.open_orders], + ) + .await; + } + + Ok(()) +} + struct CommonSetup { group_with_tokens: GroupWithTokens, serum_market_cookie: SpotMarketCookie, diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 00bf0fc32..848e9990e 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -3506,6 +3506,7 @@ pub struct PerpPlaceOrderInstruction { pub reduce_only: bool, pub client_order_id: u64, pub self_trade_behavior: SelfTradeBehavior, + pub limit: u8, } impl Default for PerpPlaceOrderInstruction { fn default() -> Self { @@ -3520,6 +3521,7 @@ impl Default for PerpPlaceOrderInstruction { reduce_only: false, client_order_id: 0, self_trade_behavior: SelfTradeBehavior::DecrementTake, + limit: 10, } } } @@ -3542,7 +3544,7 @@ impl ClientInstruction for PerpPlaceOrderInstruction { self_trade_behavior: self.self_trade_behavior, reduce_only: self.reduce_only, expiry_timestamp: 0, - limit: 10, + limit: self.limit, }; let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); @@ -3728,6 +3730,7 @@ pub struct PerpCancelAllOrdersInstruction { pub account: Pubkey, pub perp_market: Pubkey, pub owner: TestKeypair, + pub limit: u8, } #[async_trait::async_trait(?Send)] impl ClientInstruction for PerpCancelAllOrdersInstruction { @@ -3738,7 +3741,7 @@ impl ClientInstruction for PerpCancelAllOrdersInstruction { account_loader: impl ClientAccountLoader + 'async_trait, ) -> (Self::Accounts, instruction::Instruction) { let program_id = mango_v4::id(); - let instruction = Self::Instruction { limit: 5 }; + let instruction = Self::Instruction { limit: self.limit }; let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap(); let accounts = Self::Accounts { group: perp_market.group,