From f6905146383110c361096797074b5fb841c1d903 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 3 Nov 2023 11:20:37 +0100 Subject: [PATCH] rust client: ix cu limits based on health compute cost (#768) Many instructions now return PreparedInstructions instead of a direct Instruction or Vec. That way they can keep track of the expected cu cost of the instructions for the compute limit instruction that gets added once all instructions are made. --- bin/liquidator/src/liquidate.rs | 26 +- bin/liquidator/src/trigger_tcs.rs | 6 +- bin/settler/src/settle.rs | 24 +- bin/settler/src/tcs_start.rs | 21 +- lib/client/src/client.rs | 689 ++++++++++-------- lib/client/src/context.rs | 94 ++- lib/client/src/health_cache.rs | 4 +- lib/client/src/jupiter/v4.rs | 2 +- lib/client/src/jupiter/v6.rs | 2 +- lib/client/src/util.rs | 57 ++ programs/mango-v4/tests/cases/test_perp.rs | 227 ++++++ programs/mango-v4/tests/cases/test_serum.rs | 127 +++- .../tests/program_test/mango_client.rs | 7 +- 13 files changed, 935 insertions(+), 351 deletions(-) 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,