diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 90b3a74f6..70c4e95d5 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -1,10 +1,13 @@ +use clap::clap_derive::ArgEnum; use clap::{Args, Parser, Subcommand}; +use mango_v4::state::{PlaceOrderType, SelfTradeBehavior, Side}; use mango_v4_client::{ keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig, }; use solana_sdk::pubkey::Pubkey; use std::str::FromStr; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; mod save_snapshot; mod test_oracles; @@ -88,6 +91,41 @@ struct JupiterSwap { rpc: Rpc, } +#[derive(ArgEnum, Clone, Debug)] +#[repr(u8)] +pub enum CliSide { + Bid = 0, + Ask = 1, +} + +#[derive(Args, Debug, Clone)] +struct PerpPlaceOrder { + #[clap(long)] + account: String, + + /// also pays for everything + #[clap(short, long)] + owner: String, + + #[clap(long)] + market_name: String, + + #[clap(long, value_enum)] + side: CliSide, + + #[clap(short, long)] + price: f64, + + #[clap(long)] + quantity: f64, + + #[clap(long)] + expiry: u64, + + #[clap(flatten)] + rpc: Rpc, +} + #[derive(Subcommand, Debug, Clone)] enum Command { CreateAccount(CreateAccount), @@ -128,6 +166,7 @@ enum Command { #[clap(short, long)] output: String, }, + PerpPlaceOrder(PerpPlaceOrder), } impl Rpc { @@ -248,6 +287,52 @@ async fn main() -> Result<(), anyhow::Error> { let client = rpc.client(None)?; save_snapshot::save_snapshot(mango_group, client, output).await? } + Command::PerpPlaceOrder(cmd) => { + let client = cmd.rpc.client(Some(&cmd.owner))?; + let account = pubkey_from_cli(&cmd.account); + let owner = Arc::new(keypair_from_cli(&cmd.owner)); + let client = MangoClient::new_for_existing_account(client, account, owner).await?; + let market = client + .context + .perp_markets + .iter() + .find(|p| p.1.name == cmd.market_name) + .unwrap() + .1; + + fn native(x: f64, b: u32) -> i64 { + (x * (10_i64.pow(b)) as f64) as i64 + } + + let price_lots = native(cmd.price, 6) * market.base_lot_size + / (market.quote_lot_size * 10_i64.pow(market.base_decimals.into())); + let max_base_lots = + native(cmd.quantity, market.base_decimals.into()) / market.base_lot_size; + + let txsig = client + .perp_place_order( + market.perp_market_index, + match cmd.side { + CliSide::Bid => Side::Bid, + CliSide::Ask => Side::Ask, + }, + price_lots, + max_base_lots, + i64::max_value(), + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), + PlaceOrderType::Limit, + false, + if cmd.expiry > 0 { + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + cmd.expiry + } else { + 0 + }, + 10, + SelfTradeBehavior::AbortTransaction, + ) + .await?; + println!("{}", txsig); + } }; Ok(()) diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index b1584d197..c3795cc01 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -45,6 +45,7 @@ use solana_sdk::signer::keypair; use solana_sdk::transaction::TransactionError; use anyhow::Context; +use mango_v4::error::{IsAnchorErrorWithCode, MangoError}; use solana_sdk::account::ReadableAccount; use solana_sdk::instruction::{AccountMeta, Instruction}; use solana_sdk::signature::{Keypair, Signature}; @@ -1058,51 +1059,64 @@ impl MangoClient { limit: u8, self_trade_behavior: SelfTradeBehavior, ) -> anyhow::Result { + let mut ixs = PreparedInstructions::new(); + let perp = self.context.perp(market_index); + let mut account = account.clone(); + + let close_perp_ixs_opt = self + .replace_perp_market_if_needed(&account, market_index) + .await?; + + if let Some((close_perp_ixs, modified_account)) = close_perp_ixs_opt { + account = modified_account; + ixs.append(close_perp_ixs); + } + let (health_remaining_metas, health_cu) = self .derive_health_check_remaining_account_metas( - account, + &account, vec![], vec![], vec![market_index], ) .await?; - 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.bids, - asks: perp.asks, - event_queue: perp.event_queue, - oracle: perp.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, + 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.bids, + asks: perp.asks, + event_queue: perp.event_queue, + oracle: perp.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, + }), + }; + + ixs.push( + ix, self.instruction_cu(health_cu) + self.context.compute_estimates.cu_per_perp_order_match * limit as u32, ); @@ -1110,6 +1124,44 @@ impl MangoClient { Ok(ixs) } + async fn replace_perp_market_if_needed( + &self, + account: &MangoAccountValue, + perk_market_index: PerpMarketIndex, + ) -> anyhow::Result> { + let context = &self.context; + let settle_token_index = context.perp(perk_market_index).settle_token_index; + + let mut account = account.clone(); + let enforce_position_result = + account.ensure_perp_position(perk_market_index, settle_token_index); + + if !enforce_position_result + .is_anchor_error_with_code(MangoError::NoFreePerpPositionIndex.error_code()) + { + return Ok(None); + } + + let perp_position_to_close_opt = account.find_first_active_unused_perp_position(); + match perp_position_to_close_opt { + Some(perp_position_to_close) => { + let close_ix = self + .perp_deactivate_position_instruction(perp_position_to_close.market_index) + .await?; + + let previous_market = context.perp(perp_position_to_close.market_index); + account.deactivate_perp_position( + perp_position_to_close.market_index, + previous_market.settle_token_index, + )?; + account.ensure_perp_position(perk_market_index, settle_token_index)?; + + Ok(Some((close_ix, account))) + } + None => anyhow::bail!("No perp market slot available"), + } + } + #[allow(clippy::too_many_arguments)] pub async fn perp_place_order( &self, @@ -1182,18 +1234,23 @@ impl MangoClient { &self, market_index: PerpMarketIndex, ) -> anyhow::Result { - let perp = self.context.perp(market_index); - let mango_account = &self.mango_account().await?; - - let (health_check_metas, health_cu) = self - .derive_health_check_remaining_account_metas(mango_account, vec![], vec![], vec![]) + let ixs = self + .perp_deactivate_position_instruction(market_index) .await?; + self.send_and_confirm_owner_tx(ixs.to_instructions()).await + } + + async fn perp_deactivate_position_instruction( + &self, + market_index: PerpMarketIndex, + ) -> anyhow::Result { + let perp = self.context.perp(market_index); let ixs = PreparedInstructions::from_single( Instruction { program_id: mango_v4::id(), accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + let ams = anchor_lang::ToAccountMetas::to_account_metas( &mango_v4::accounts::PerpDeactivatePosition { group: self.group(), account: self.mango_account_address, @@ -1202,16 +1259,15 @@ impl MangoClient { }, None, ); - ams.extend(health_check_metas.into_iter()); ams }, data: anchor_lang::InstructionData::data( &mango_v4::instruction::PerpDeactivatePosition {}, ), }, - self.instruction_cu(health_cu), + self.context.compute_estimates.cu_per_mango_instruction, ); - self.send_and_confirm_owner_tx(ixs.to_instructions()).await + Ok(ixs) } pub async fn perp_settle_pnl_instruction( diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 9a759b250..ac8c215ef 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -1155,6 +1155,7 @@ impl< } } + // Only used in unit tests pub fn deactivate_perp_position( &mut self, perp_market_index: PerpMarketIndex, @@ -1196,6 +1197,19 @@ impl< Ok(()) } + pub fn find_first_active_unused_perp_position(&self) -> Option<&PerpPosition> { + let first_unused_position_opt = self.all_perp_positions().find(|p| { + p.is_active() + && p.base_position_lots == 0 + && p.quote_position_native == 0 + && p.bids_base_lots == 0 + && p.asks_base_lots == 0 + && p.taker_base_lots == 0 + && p.taker_quote_lots == 0 + }); + first_unused_position_opt + } + pub fn add_perp_order( &mut self, perp_market_index: PerpMarketIndex, @@ -2808,4 +2822,34 @@ mod tests { Ok(()) } + + #[test] + fn test_perp_auto_close_first_unused() { + let mut account = make_test_account(); + + // Fill all perp slots + assert_eq!(account.header.perp_count, 4); + account.ensure_perp_position(1, 0).unwrap(); + account.ensure_perp_position(2, 0).unwrap(); + account.ensure_perp_position(3, 0).unwrap(); + account.ensure_perp_position(4, 0).unwrap(); + assert_eq!(account.active_perp_positions().count(), 4); + + // Force usage of some perp slot (leaves 3 unused) + account.perp_position_mut(1).unwrap().taker_base_lots = 10; + account.perp_position_mut(2).unwrap().base_position_lots = 10; + account.perp_position_mut(4).unwrap().quote_position_native = I80F48::from_num(10); + assert!(account.perp_position(3).ok().is_some()); + + // Should not succeed anymore + { + let e = account.ensure_perp_position(5, 0); + assert!(e.is_anchor_error_with_code(MangoError::NoFreePerpPositionIndex.error_code())); + } + + // Act + let to_be_closed_account_opt = account.find_first_active_unused_perp_position(); + + assert_eq!(to_be_closed_account_opt.unwrap().market_index, 3) + } }