diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 560840b1f..06ab893be 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -1,5 +1,6 @@ use clap::clap_derive::ArgEnum; use clap::{Args, Parser, Subcommand}; +use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use mango_v4::state::{PlaceOrderType, SelfTradeBehavior, Side}; use mango_v4_client::{ keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig, @@ -126,6 +127,63 @@ struct PerpPlaceOrder { rpc: Rpc, } +#[derive(Args, Debug, Clone)] +struct Serum3CreateOpenOrders { + #[clap(long)] + account: String, + + /// also pays for everything + #[clap(short, long)] + owner: String, + + #[clap(long)] + market_name: String, + + #[clap(flatten)] + rpc: Rpc, +} + +#[derive(Args, Debug, Clone)] +struct Serum3CloseOpenOrders { + #[clap(long)] + account: String, + + /// also pays for everything + #[clap(short, long)] + owner: String, + + #[clap(long)] + market_name: String, + + #[clap(flatten)] + rpc: Rpc, +} + +#[derive(Args, Debug, Clone)] +struct Serum3PlaceOrder { + #[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(flatten)] + rpc: Rpc, +} + #[derive(Subcommand, Debug, Clone)] enum Command { CreateAccount(CreateAccount), @@ -167,6 +225,9 @@ enum Command { output: String, }, PerpPlaceOrder(PerpPlaceOrder), + Serum3CloseOpenOrders(Serum3CloseOpenOrders), + Serum3CreateOpenOrders(Serum3CreateOpenOrders), + Serum3PlaceOrder(Serum3PlaceOrder), } impl Rpc { @@ -326,6 +387,65 @@ async fn main() -> Result<(), anyhow::Error> { .await?; println!("{}", txsig); } + Command::Serum3CreateOpenOrders(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 txsig = client.serum3_create_open_orders(&cmd.market_name).await?; + println!("{}", txsig); + } + Command::Serum3CloseOpenOrders(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 txsig = client.serum3_close_open_orders(&cmd.market_name).await?; + println!("{}", txsig); + } + Command::Serum3PlaceOrder(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_index = client.context.serum3_market_index(&cmd.market_name); + let market = client.context.serum3(market_index); + let base_token = client.context.token(market.base_token_index); + let quote_token = client.context.token(market.quote_token_index); + + fn native(x: f64, b: u32) -> u64 { + (x * (10_i64.pow(b)) as f64) as u64 + } + + // coin_lot_size = base lot size ? + // cf priceNumberToLots + let price_lots = native(cmd.price, quote_token.decimals as u32) * market.coin_lot_size + / (native(1.0, base_token.decimals as u32) * market.pc_lot_size); + + // cf baseSizeNumberToLots + let max_base_lots = + native(cmd.quantity, base_token.decimals as u32) / market.coin_lot_size; + + let txsig = client + .serum3_place_order( + &cmd.market_name, + match cmd.side { + CliSide::Bid => Serum3Side::Bid, + CliSide::Ask => Serum3Side::Ask, + }, + price_lots, + max_base_lots as u64, + ((price_lots * max_base_lots) as f64 * 1.01) as u64, + Serum3SelfTradeBehavior::AbortTransaction, + Serum3OrderType::Limit, + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), + 10, + ) + .await?; + println!("{}", txsig); + } }; Ok(()) diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index 2af47dea0..0b4d1b646 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -1,3 +1,4 @@ +use anchor_client::ClientError::AnchorError; use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; @@ -126,6 +127,7 @@ impl ClientBuilder { } } } + pub struct Client { config: ClientConfig, rpc_async: RpcClientAsync, @@ -577,40 +579,40 @@ impl MangoClient { let ixs = PreparedInstructions::from_vec( vec![ - spl_associated_token_account::instruction::create_associated_token_account_idempotent( - &self.owner(), - &self.owner(), - &mint, - &Token::id(), - ), - Instruction { - program_id: mango_v4::id(), - accounts: { - let mut ams = anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::TokenWithdraw { - group: self.group(), - account: self.mango_account_address, - owner: self.owner(), - bank: token.first_bank(), - vault: token.first_vault(), - oracle: token.oracle, - token_account: get_associated_token_address( - &self.owner(), - &token.mint, - ), - token_program: Token::id(), - }, - None, - ); - ams.extend(health_check_metas.into_iter()); - ams + spl_associated_token_account::instruction::create_associated_token_account_idempotent( + &self.owner(), + &self.owner(), + &mint, + &Token::id(), + ), + Instruction { + program_id: mango_v4::id(), + accounts: { + let mut ams = anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::TokenWithdraw { + group: self.group(), + account: self.mango_account_address, + owner: self.owner(), + bank: token.first_bank(), + vault: token.first_vault(), + oracle: token.oracle, + token_account: get_associated_token_address( + &self.owner(), + &token.mint, + ), + token_program: Token::id(), + }, + None, + ); + ams.extend(health_check_metas.into_iter()); + ams + }, + data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenWithdraw { + amount, + allow_borrow, + }), }, - data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenWithdraw { - amount, - allow_borrow, - }), - }, - ], + ], self.instruction_cu(health_cu), ); Ok(ixs) @@ -658,6 +660,45 @@ impl MangoClient { // Serum3 // + pub fn serum3_close_open_orders_instruction( + &self, + market_index: Serum3MarketIndex, + ) -> PreparedInstructions { + let account_pubkey = self.mango_account_address; + let s3 = self.context.serum3(market_index); + + let open_orders = self.serum3_create_open_orders_address(market_index); + + PreparedInstructions::from_single( + Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::Serum3CloseOpenOrders { + group: self.group(), + account: account_pubkey, + serum_market: s3.address, + serum_program: s3.serum_program, + serum_market_external: s3.serum_market_external, + open_orders, + owner: self.owner(), + sol_destination: self.owner(), + }, + None, + ), + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::Serum3CloseOpenOrders {}, + ), + }, + self.context.compute_estimates.cu_per_mango_instruction, + ) + } + + pub async fn serum3_close_open_orders(&self, name: &str) -> anyhow::Result { + let market_index = self.context.serum3_market_index(name); + let ix = self.serum3_close_open_orders_instruction(market_index); + self.send_and_confirm_owner_tx(ix.to_instructions()).await + } + pub fn serum3_create_open_orders_instruction( &self, market_index: Serum3MarketIndex, @@ -665,15 +706,7 @@ impl MangoClient { let account_pubkey = self.mango_account_address; let s3 = self.context.serum3(market_index); - let open_orders = Pubkey::find_program_address( - &[ - b"Serum3OO".as_ref(), - account_pubkey.as_ref(), - s3.address.as_ref(), - ], - &mango_v4::ID, - ) - .0; + let open_orders = self.serum3_create_open_orders_address(market_index); Instruction { program_id: mango_v4::id(), @@ -698,6 +731,23 @@ impl MangoClient { } } + fn serum3_create_open_orders_address(&self, market_index: Serum3MarketIndex) -> Pubkey { + let account_pubkey = self.mango_account_address; + let s3 = self.context.serum3(market_index); + + let open_orders = Pubkey::find_program_address( + &[ + b"Serum3OO".as_ref(), + account_pubkey.as_ref(), + s3.address.as_ref(), + ], + &mango_v4::ID, + ) + .0; + + open_orders + } + pub async fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result { let market_index = self.context.serum3_market_index(name); let ix = self.serum3_create_open_orders_instruction(market_index); @@ -721,20 +771,22 @@ impl MangoClient { 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); - let open_orders = account - .serum3_orders(market_index) - .expect("oo is created") - .open_orders; + let (payer_token, receiver_token) = match side { + Serum3Side::Bid => ("e, &base), + Serum3Side::Ask => (&base, "e), + }; + + let open_orders = account.serum3_orders(market_index).map(|x| x.open_orders)?; let (health_check_metas, health_cu) = self - .derive_health_check_remaining_account_metas(account, vec![], vec![], vec![]) + .derive_health_check_remaining_account_metas( + &account, + vec![], + vec![receiver_token.token_index], + vec![], + ) .await?; - let payer_token = match side { - Serum3Side::Bid => "e, - Serum3Side::Ask => &base, - }; - let ixs = PreparedInstructions::from_single( Instruction { program_id: mango_v4::id(), @@ -766,7 +818,7 @@ impl MangoClient { ams }, data: anchor_lang::InstructionData::data( - &mango_v4::instruction::Serum3PlaceOrder { + &mango_v4::instruction::Serum3PlaceOrderV2 { side, limit_price, max_base_qty, @@ -785,6 +837,205 @@ impl MangoClient { Ok(ixs) } + #[allow(clippy::too_many_arguments)] + pub async fn serum3_create_or_replace_account_instruction( + &self, + mut account: &mut MangoAccountValue, + market_index: Serum3MarketIndex, + side: Serum3Side, + ) -> anyhow::Result { + let mut ixs = PreparedInstructions::new(); + + let base = self.context.serum3_base_token(market_index); + let quote = self.context.serum3_quote_token(market_index); + let (payer_token, receiver_token) = match side { + Serum3Side::Bid => ("e, &base), + Serum3Side::Ask => (&base, "e), + }; + + let open_orders_opt = account + .serum3_orders(market_index) + .map(|x| x.open_orders) + .ok(); + + let mut missing_tokens = false; + + let token_replace_ixs = self + .find_existing_or_try_to_replace_token_positions( + &mut account, + &[payer_token.token_index, receiver_token.token_index], + ) + .await; + match token_replace_ixs { + Ok(res) => { + ixs.append(res); + } + Err(_) => missing_tokens = true, + } + + if open_orders_opt.is_none() { + let has_available_slot = account.all_serum3_orders().any(|p| !p.is_active()); + let should_close_one_open_orders_account = !has_available_slot || missing_tokens; + + if should_close_one_open_orders_account { + ixs.append( + self.deactivate_first_active_unused_serum3_orders(&mut account) + .await?, + ); + } + + // in case of missing token slots + // try again to create, as maybe deactivating the market slot resulted in some token being now unused + // but this time, in case of error, propagate to caller + if missing_tokens { + ixs.append( + self.find_existing_or_try_to_replace_token_positions( + &mut account, + &[payer_token.token_index, receiver_token.token_index], + ) + .await?, + ); + } + + ixs.push( + self.serum3_create_open_orders_instruction(market_index), + self.context.compute_estimates.cu_per_mango_instruction, + ); + + let created_open_orders = self.serum3_create_open_orders_address(market_index); + + account.create_serum3_orders(market_index)?.open_orders = created_open_orders; + } + + Ok(ixs) + } + + async fn deactivate_first_active_unused_serum3_orders( + &self, + account: &mut MangoAccountValue, + ) -> anyhow::Result { + let mut serum3_closable_order_market_index = None; + + for p in account.all_serum3_orders() { + let open_orders_acc = self + .account_fetcher + .fetch_raw_account(&p.open_orders) + .await?; + let open_orders_bytes = open_orders_acc.data(); + let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes( + &open_orders_bytes[5..5 + std::mem::size_of::()], + ); + + let is_closable = open_orders_data.free_slot_bits == u128::MAX + && open_orders_data.native_coin_total == 0 + && open_orders_data.native_pc_total == 0; + + if is_closable { + serum3_closable_order_market_index = Some(p.market_index); + break; + } + } + + let first_closable_slot = + serum3_closable_order_market_index.expect("couldn't find any serum3 slot available"); + + let ixs = self.serum3_close_open_orders_instruction(first_closable_slot); + + let first_closable_market = account.serum3_orders(first_closable_slot)?; + let (tk1, tk2) = ( + first_closable_market.base_token_index, + first_closable_market.quote_token_index, + ); + account.token_position_mut(tk1)?.0.decrement_in_use(); + account.token_position_mut(tk2)?.0.decrement_in_use(); + account.deactivate_serum3_orders(first_closable_slot)?; + + Ok(ixs) + } + + async fn find_existing_or_try_to_replace_token_positions( + &self, + account: &mut MangoAccountValue, + token_indexes: &[TokenIndex], + ) -> anyhow::Result { + let mut ixs = PreparedInstructions::new(); + + for token_index in token_indexes { + let result = self + .find_existing_or_try_to_replace_token_position(account, *token_index) + .await?; + if let Some(ix) = result { + ixs.append(ix); + } + } + + Ok(ixs) + } + + async fn find_existing_or_try_to_replace_token_position( + &self, + account: &mut MangoAccountValue, + token_index: TokenIndex, + ) -> anyhow::Result> { + let token_position_missing = account + .ensure_token_position(token_index) + .is_anchor_error_with_code(MangoError::NoFreeTokenPositionIndex.error_code()); + + if !token_position_missing { + return Ok(None); + } + + let ixs = self.deactivate_first_active_unused_token(account).await?; + account.ensure_token_position(token_index)?; + + Ok(Some(ixs)) + } + + async fn deactivate_first_active_unused_token( + &self, + account: &mut MangoAccountValue, + ) -> anyhow::Result { + let closable_tokens = account + .all_token_positions() + .enumerate() + .filter(|(_, p)| p.is_active() && !p.is_in_use()); + + let mut closable_token_position_raw_index_opt = None; + let mut closable_token_bank_opt = None; + + for (closable_token_position_raw_index, closable_token_position) in closable_tokens { + let bank = self.first_bank(closable_token_position.token_index).await?; + let native_balance = closable_token_position.native(&bank); + + if native_balance < I80F48::ZERO { + continue; + } + if native_balance > I80F48::ONE { + continue; + } + + closable_token_position_raw_index_opt = Some(closable_token_position_raw_index); + closable_token_bank_opt = Some(bank); + break; + } + + if closable_token_bank_opt.is_none() { + return Err(AnchorError(MangoError::NoFreeTokenPositionIndex.into()).into()); + } + + let withdraw_ixs = self + .token_withdraw_instructions( + &account, + closable_token_bank_opt.unwrap().mint, + u64::MAX, + false, + ) + .await?; + + account.deactivate_token_position(closable_token_position_raw_index_opt.unwrap()); + return Ok(withdraw_ixs); + } + #[allow(clippy::too_many_arguments)] pub async fn serum3_place_order( &self, @@ -798,9 +1049,12 @@ impl MangoClient { client_order_id: u64, limit: u16, ) -> anyhow::Result { - let account = self.mango_account().await?; + let mut account = self.mango_account().await?.clone(); let market_index = self.context.serum3_market_index(name); - let ixs = self + let create_or_replace_ixs = self + .serum3_create_or_replace_account_instruction(&mut account, market_index, side) + .await?; + let place_order_ixs = self .serum3_place_order_instruction( &account, market_index, @@ -814,6 +1068,10 @@ impl MangoClient { limit, ) .await?; + + let mut ixs = PreparedInstructions::new(); + ixs.append(create_or_replace_ixs); + ixs.append(place_order_ixs); self.send_and_confirm_owner_tx(ixs.to_instructions()).await } @@ -1960,7 +2218,7 @@ impl MangoClient { #[derive(Debug, thiserror::Error)] pub enum MangoClientError { #[error("Transaction simulation error. Error: {err:?}, Logs: {}", - .logs.iter().join("; ") + .logs.iter().join("; ") )] SendTransactionPreflightFailure { err: Option, @@ -1999,6 +2257,7 @@ pub enum FallbackOracleConfig { /// Every possible fallback oracle (may cause serious issues with the 64 accounts-per-tx limit) All, } + impl Default for FallbackOracleConfig { fn default() -> Self { FallbackOracleConfig::Dynamic