rust client: ix cu limits based on health compute cost (#768)

Many instructions now return PreparedInstructions instead of a direct
Instruction or Vec<Instruction>. 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.
This commit is contained in:
Christian Kamm 2023-11-03 11:20:37 +01:00 committed by GitHub
parent 941f945999
commit f690514638
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 935 additions and 351 deletions

View File

@ -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<Option<Signature>> {
let all_perp_base_positions: anyhow::Result<
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
@ -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!(

View File

@ -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)

View File

@ -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<Instruction>,
instructions: PreparedInstructions,
max_batch_size: usize,
blockhash: solana_sdk::hash::Hash,
address_lookup_tables: &'a Vec<AddressLookupTableAccount>,
@ -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<Option<Signature>> {
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);
}

View File

@ -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<Instruction> {
async fn make_start_ix(
&self,
pubkey: &Pubkey,
tcs_id: u64,
) -> anyhow::Result<PreparedInstructions> {
let account = self.account_fetcher.fetch_mango_account(pubkey).unwrap();
self.mango_client
.token_conditional_swap_start_instruction((pubkey, &account), tcs_id)

File diff suppressed because it is too large Load Diff

View File

@ -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<String, PerpMarketIndex>,
pub address_lookup_tables: Vec<Pubkey>,
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<TokenIndex>,
writable_banks: Vec<TokenIndex>,
affected_perp_markets: Vec<PerpMarketIndex>,
) -> anyhow::Result<Vec<AccountMeta>> {
) -> anyhow::Result<(Vec<AccountMeta>, 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<Vec<AccountMeta>> {
) -> anyhow::Result<(Vec<AccountMeta>, 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<bool> {

View File

@ -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<Vec<KeyedAccountSharedData>> = 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()

View File

@ -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],

View File

@ -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],

View File

@ -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<Instruction>,
pub cu: u32,
}
impl PreparedInstructions {
pub fn new() -> Self {
Self {
instructions: vec![],
cu: 0,
}
}
pub fn from_vec(instructions: Vec<Instruction>, 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<Instruction> {
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()
}
}

View File

@ -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::<PerpMarket>(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::<MangoAccount>(account_0).await;

View File

@ -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,

View File

@ -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,