diff --git a/bin/cli/src/main.rs b/bin/cli/src/main.rs index 819ec4a2f..14bae6ef0 100644 --- a/bin/cli/src/main.rs +++ b/bin/cli/src/main.rs @@ -146,7 +146,7 @@ async fn main() -> Result<(), anyhow::Error> { Command::CreateAccount(cmd) => { let client = cmd.rpc.client(Some(&cmd.owner))?; let group = pubkey_from_cli(&cmd.group); - let owner = keypair_from_cli(&cmd.owner); + let owner = Arc::new(keypair_from_cli(&cmd.owner)); let account_num = if let Some(num) = cmd.account_num { num @@ -164,9 +164,15 @@ async fn main() -> Result<(), anyhow::Error> { + 1 } }; - let (account, txsig) = - MangoClient::create_account(&client, group, &owner, &owner, account_num, &cmd.name) - .await?; + let (account, txsig) = MangoClient::create_account( + &client, + group, + owner.clone(), + owner.clone(), + account_num, + &cmd.name, + ) + .await?; println!("{}", account); println!("{}", txsig); } @@ -193,6 +199,7 @@ async fn main() -> Result<(), anyhow::Error> { cmd.amount, cmd.slippage_bps, JupiterSwapMode::ExactIn, + false, ) .await?; println!("{}", txsig); diff --git a/bin/liquidator/src/liquidate.rs b/bin/liquidator/src/liquidate.rs index 8387789e4..f94df100e 100644 --- a/bin/liquidator/src/liquidate.rs +++ b/bin/liquidator/src/liquidate.rs @@ -42,6 +42,7 @@ pub async fn jupiter_market_can_buy( quote_amount, slippage, JupiterSwapMode::ExactIn, + false, ) .await .is_ok() @@ -70,6 +71,7 @@ pub async fn jupiter_market_can_sell( quote_amount, slippage, JupiterSwapMode::ExactOut, + false, ) .await .is_ok() diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index 6bd25c1f4..9d012b790 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -176,6 +176,7 @@ impl Rebalancer { input_amount.to_num::(), self.config.slippage_bps, JupiterSwapMode::ExactIn, + false, ) .await?; info!( @@ -208,6 +209,7 @@ impl Rebalancer { amount.to_num::(), self.config.slippage_bps, JupiterSwapMode::ExactIn, + false, ) .await?; info!( diff --git a/bin/liquidator/src/token_swap_info.rs b/bin/liquidator/src/token_swap_info.rs index 5341f4034..ff93830df 100644 --- a/bin/liquidator/src/token_swap_info.rs +++ b/bin/liquidator/src/token_swap_info.rs @@ -104,6 +104,7 @@ impl TokenSwapInfoUpdater { token_amount, slippage, JupiterSwapMode::ExactIn, + false, self.config.mock_jupiter, ) .await?; @@ -114,6 +115,7 @@ impl TokenSwapInfoUpdater { self.config.quote_amount, slippage, JupiterSwapMode::ExactIn, + false, self.config.mock_jupiter, ) .await?; diff --git a/bin/liquidator/src/trigger_tcs.rs b/bin/liquidator/src/trigger_tcs.rs index 7942e7b9a..bb1c5de0f 100644 --- a/bin/liquidator/src/trigger_tcs.rs +++ b/bin/liquidator/src/trigger_tcs.rs @@ -172,6 +172,7 @@ pub async fn maybe_execute_token_conditional_swap_inner( input_amount, slippage, swap_mode, + false, config.mock_jupiter, ) .await?; diff --git a/bin/liquidator/src/util.rs b/bin/liquidator/src/util.rs index 8fa456d2c..c1d611298 100644 --- a/bin/liquidator/src/util.rs +++ b/bin/liquidator/src/util.rs @@ -45,11 +45,19 @@ pub async fn jupiter_route( amount: u64, slippage: u64, swap_mode: JupiterSwapMode, + only_direct_routes: bool, mock: bool, ) -> anyhow::Result { if !mock { return mango_client - .jupiter_route(input_mint, output_mint, amount, slippage, swap_mode) + .jupiter_route( + input_mint, + output_mint, + amount, + slippage, + swap_mode, + only_direct_routes, + ) .await; } diff --git a/bin/settler/src/settle.rs b/bin/settler/src/settle.rs index cced540d1..a46ccd5c4 100644 --- a/bin/settler/src/settle.rs +++ b/bin/settler/src/settle.rs @@ -251,7 +251,7 @@ struct SettleBatchProcessor<'a> { impl<'a> SettleBatchProcessor<'a> { fn transaction(&self) -> anyhow::Result { let client = &self.mango_client.client; - let fee_payer = &*client.fee_payer; + let fee_payer = client.fee_payer.clone(); TransactionBuilder { instructions: self.instructions.clone(), diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index bc5a5ad41..bf00463bc 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -1,3 +1,4 @@ +use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -134,16 +135,16 @@ impl MangoClient { pub async fn find_or_create_account( client: &Client, group: Pubkey, - owner: &Keypair, - payer: &Keypair, // pays the SOL for the new account + owner: Arc, + payer: Arc, // pays the SOL for the new account mango_account_name: &str, ) -> anyhow::Result { let rpc = client.rpc_async(); let program = mango_v4::ID; + let owner_pk = owner.pubkey(); // Mango Account - let mut mango_account_tuples = - fetch_mango_accounts(&rpc, program, group, owner.pubkey()).await?; + let mut mango_account_tuples = fetch_mango_accounts(&rpc, program, group, owner_pk).await?; let mango_account_opt = mango_account_tuples .iter() .find(|(_, account)| account.fixed.name() == mango_account_name); @@ -158,12 +159,18 @@ impl MangoClient { Some(tuple) => tuple.1.fixed.account_num + 1, None => 0u32, }; - Self::create_account(client, group, owner, payer, account_num, mango_account_name) - .await - .context("Failed to create account...")?; + Self::create_account( + client, + group, + owner.clone(), + payer, + account_num, + mango_account_name, + ) + .await + .context("Failed to create account...")?; } - let mango_account_tuples = - fetch_mango_accounts(&rpc, program, group, owner.pubkey()).await?; + let mango_account_tuples = fetch_mango_accounts(&rpc, program, group, owner_pk).await?; let index = mango_account_tuples .iter() .position(|tuple| tuple.1.fixed.name() == mango_account_name) @@ -174,8 +181,8 @@ impl MangoClient { pub async fn create_account( client: &Client, group: Pubkey, - owner: &Keypair, - payer: &Keypair, // pays the SOL for the new account + owner: Arc, + payer: Arc, // pays the SOL for the new account account_num: u32, mango_account_name: &str, ) -> anyhow::Result<(Pubkey, Signature)> { @@ -1390,6 +1397,7 @@ impl MangoClient { amount: u64, slippage: u64, swap_mode: JupiterSwapMode, + only_direct_routes: bool, ) -> anyhow::Result { let response = self .http_client @@ -1398,7 +1406,7 @@ impl MangoClient { ("inputMint", input_mint.to_string()), ("outputMint", output_mint.to_string()), ("amount", format!("{}", amount)), - ("onlyDirectRoutes", "true".into()), + ("onlyDirectRoutes", only_direct_routes.to_string()), ("enforceSingleTx", "true".into()), ("filterTopNResult", "10".into()), ("slippageBps", format!("{}", slippage)), @@ -1429,19 +1437,18 @@ impl MangoClient { Ok(route.clone()) } - pub async fn jupiter_swap( + /// Find the instructions and account lookup tables for a jupiter swap through mango + /// + /// It would be nice if we didn't have to pass input_mint/output_mint - the data is + /// definitely in QueryRoute - but it's unclear how. + pub async fn prepare_jupiter_swap_transaction( &self, input_mint: Pubkey, output_mint: Pubkey, - amount: u64, - slippage: u64, - swap_mode: JupiterSwapMode, - ) -> anyhow::Result { + route: &jupiter::QueryRoute, + ) -> anyhow::Result { let source_token = self.context.token_by_mint(&input_mint)?; let target_token = self.context.token_by_mint(&output_mint)?; - let route = self - .jupiter_route(input_mint, output_mint, amount, slippage, swap_mode) - .await?; let swap_response = self .http_client @@ -1477,22 +1484,25 @@ impl MangoClient { let ata_program = anchor_spl::associated_token::ID; let token_program = anchor_spl::token::ID; let compute_budget_program = solana_sdk::compute_budget::ID; - // these setup instructions are unnecessary since FlashLoan already takes care of it + // these setup instructions should be placed outside of flashloan begin-end let is_setup_ix = |k: Pubkey| -> bool { k == ata_program || k == token_program || k == compute_budget_program }; let (jup_ixs, jup_alts) = self .deserialize_instructions_and_alts(&jup_tx.message) .await?; - let jup_cu_ix = jup_ixs + let jup_action_ix_begin = jup_ixs .iter() - .filter(|ix| ix.program_id == compute_budget_program) - .cloned() - .collect::>(); - let jup_action_ix = jup_ixs - .into_iter() - .filter(|ix| !is_setup_ix(ix.program_id)) - .collect::>(); + .position(|ix| !is_setup_ix(ix.program_id)) + .ok_or_else(|| { + anyhow::anyhow!("jupiter swap response only had setup-like instructions") + })?; + let jup_action_ix_end = jup_ixs.len() + - jup_ixs + .iter() + .rev() + .position(|ix| !is_setup_ix(ix.program_id)) + .unwrap(); let bank_ams = [ source_token.mint_info.first_bank(), @@ -1522,14 +1532,14 @@ impl MangoClient { }) .collect::>(); - let loan_amounts = vec![ - match swap_mode { - JupiterSwapMode::ExactIn => amount, - // in amount + slippage - JupiterSwapMode::ExactOut => u64::from_str(&route.other_amount_threshold).unwrap(), - }, - 0u64, - ]; + let source_loan = if route.swap_mode == "ExactIn" { + u64::from_str(&route.amount).unwrap() + } else if route.swap_mode == "ExactOut" { + u64::from_str(&route.other_amount_threshold).unwrap() + } else { + anyhow::bail!("unknown swap mode: {}", route.swap_mode); + }; + let loan_amounts = vec![source_loan, 0u64]; 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! @@ -1544,25 +1554,9 @@ impl MangoClient { let mut instructions = Vec::new(); - for ix in jup_cu_ix { + for ix in &jup_ixs[..jup_action_ix_begin] { instructions.push(ix.clone()); } - instructions.push( - spl_associated_token_account::instruction::create_associated_token_account_idempotent( - &self.owner.pubkey(), - &self.owner.pubkey(), - &source_token.mint_info.mint, - &Token::id(), - ), - ); - instructions.push( - spl_associated_token_account::instruction::create_associated_token_account_idempotent( - &self.owner.pubkey(), - &self.owner.pubkey(), - &target_token.mint_info.mint, - &Token::id(), - ), - ); instructions.push(Instruction { program_id: mango_v4::id(), accounts: { @@ -1585,7 +1579,7 @@ impl MangoClient { loan_amounts, }), }); - for ix in jup_action_ix { + for ix in &jup_ixs[jup_action_ix_begin..jup_action_ix_end] { instructions.push(ix.clone()); } instructions.push(Instruction { @@ -1610,20 +1604,49 @@ impl MangoClient { flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Swap, }), }); + for ix in &jup_ixs[jup_action_ix_end..] { + instructions.push(ix.clone()); + } - let payer = self.owner.pubkey(); // maybe use fee_payer? but usually it's the same let mut address_lookup_tables = self.mango_address_lookup_tables().await?; address_lookup_tables.extend(jup_alts.into_iter()); - TransactionBuilder { + let payer = self.owner.pubkey(); // maybe use fee_payer? but usually it's the same + + Ok(TransactionBuilder { instructions, address_lookup_tables, payer, - signers: vec![&*self.owner], + signers: vec![self.owner.clone()], config: self.client.transaction_builder_config, - } - .send_and_confirm(&self.client) - .await + }) + } + + pub async fn jupiter_swap( + &self, + input_mint: Pubkey, + output_mint: Pubkey, + amount: u64, + slippage: u64, + swap_mode: JupiterSwapMode, + only_direct_routes: bool, + ) -> anyhow::Result { + let route = self + .jupiter_route( + input_mint, + output_mint, + amount, + slippage, + swap_mode, + only_direct_routes, + ) + .await?; + + let tx_builder = self + .prepare_jupiter_swap_transaction(input_mint, output_mint, &route) + .await?; + + tx_builder.send_and_confirm(&self.client).await } async fn fetch_address_lookup_table( @@ -1707,7 +1730,7 @@ impl MangoClient { instructions, address_lookup_tables: vec![], payer: self.client.fee_payer.pubkey(), - signers: vec![&*self.owner, &*self.client.fee_payer], + signers: vec![self.owner.clone(), self.client.fee_payer.clone()], config: self.client.transaction_builder_config, } .send_and_confirm(&self.client) @@ -1722,7 +1745,7 @@ impl MangoClient { instructions, address_lookup_tables: vec![], payer: self.client.fee_payer.pubkey(), - signers: vec![&*self.client.fee_payer], + signers: vec![self.client.fee_payer.clone()], config: self.client.transaction_builder_config, } .send_and_confirm(&self.client) @@ -1747,17 +1770,17 @@ pub struct TransactionBuilderConfig { pub prioritization_micro_lamports: Option, } -pub struct TransactionBuilder<'a> { +pub struct TransactionBuilder { pub instructions: Vec, pub address_lookup_tables: Vec, - pub signers: Vec<&'a Keypair>, + pub signers: Vec>, pub payer: Pubkey, pub config: TransactionBuilderConfig, } -impl<'a> TransactionBuilder<'a> { +impl TransactionBuilder { pub async fn transaction( - self, + &self, rpc: &RpcClientAsync, ) -> anyhow::Result { let latest_blockhash = rpc.get_latest_blockhash().await?; @@ -1765,11 +1788,12 @@ impl<'a> TransactionBuilder<'a> { } pub fn transaction_with_blockhash( - mut self, + &self, blockhash: Hash, ) -> anyhow::Result { + let mut ix = self.instructions.clone(); if let Some(prio_price) = self.config.prioritization_micro_lamports { - self.instructions.insert( + ix.insert( 0, solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price( prio_price, @@ -1778,15 +1802,16 @@ impl<'a> TransactionBuilder<'a> { } let v0_message = solana_sdk::message::v0::Message::try_compile( &self.payer, - &self.instructions, + &ix, &self.address_lookup_tables, blockhash, )?; let versioned_message = solana_sdk::message::VersionedMessage::V0(v0_message); let signers = self .signers - .into_iter() + .iter() .unique_by(|s| s.pubkey()) + .map(|v| v.deref()) .collect::>(); let tx = solana_sdk::transaction::VersionedTransaction::try_new(versioned_message, &signers)?; @@ -1795,7 +1820,7 @@ impl<'a> TransactionBuilder<'a> { // These two send() functions don't really belong into the transaction builder! - pub async fn send(self, client: &Client) -> anyhow::Result { + pub async fn send(&self, client: &Client) -> anyhow::Result { let rpc = client.rpc_async(); let tx = self.transaction(&rpc).await?; rpc.send_transaction_with_config(&tx, client.rpc_send_transaction_config) @@ -1803,7 +1828,7 @@ impl<'a> TransactionBuilder<'a> { .map_err(prettify_solana_client_error) } - pub async fn send_and_confirm(self, client: &Client) -> anyhow::Result { + pub async fn send_and_confirm(&self, client: &Client) -> anyhow::Result { let rpc = client.rpc_async(); let tx = self.transaction(&rpc).await?; // TODO: Wish we could use client.rpc_send_transaction_config here too! @@ -1811,6 +1836,12 @@ impl<'a> TransactionBuilder<'a> { .await .map_err(prettify_solana_client_error) } + + pub fn transaction_size_ok(&self) -> anyhow::Result { + let tx = self.transaction_with_blockhash(solana_sdk::hash::Hash::default())?; + let bytes = bincode::serialize(&tx)?; + Ok(bytes.len() <= solana_sdk::packet::PACKET_DATA_SIZE) + } } /// Do some manual unpacking on some ClientErrors