use std::collections::HashSet; use std::str::FromStr; use anchor_lang::{system_program, Id}; use anchor_spl::token::Token; use anyhow::Context; use bincode::Options; use mango_v4::accounts_zerocopy::AccountReader; use serde::{Deserialize, Serialize}; use solana_address_lookup_table_program::state::AddressLookupTable; use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::account::Account; use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Signature}; use std::time::Duration; use crate::gpa::fetch_multiple_accounts_in_chunks; use crate::swap::sanctum_state; use crate::{util, MangoClient, TransactionBuilder}; use borsh::BorshDeserialize; #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct QuoteResponse { pub in_amount: Option, pub out_amount: String, pub fee_amount: String, pub fee_mint: String, pub fee_pct: String, pub swap_src: String, } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct SwapRequest { pub amount: String, pub quoted_amount: String, pub input: String, pub mode: String, pub output_lst_mint: String, pub signer: String, pub swap_src: String, } #[derive(Deserialize, Serialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct SanctumSwapResponse { pub tx: String, } pub struct Sanctum<'a> { pub mango_client: &'a MangoClient, pub timeout_duration: Duration, } impl<'a> Sanctum<'a> { pub async fn quote( &self, input_mint: Pubkey, output_mint: Pubkey, amount: u64, ) -> anyhow::Result { if input_mint == output_mint { anyhow::bail!("Need two distinct mint to swap"); } let mut account = self.mango_client.mango_account().await?; let input_token_index = self .mango_client .context .token_by_mint(&input_mint)? .token_index; let output_token_index = self .mango_client .context .token_by_mint(&output_mint)? .token_index; account.ensure_token_position(input_token_index)?; account.ensure_token_position(output_token_index)?; let query_args = vec![ ("input", input_mint.to_string()), ("outputLstMint", output_mint.to_string()), ("amount", format!("{}", amount)), ]; let config = self.mango_client.client.config(); let response = self .mango_client .http_client .get(format!("{}/swap/quote", config.sanctum_url)) .query(&query_args) .timeout(self.timeout_duration) .send() .await .context("quote request to sanctum")?; let quote: QuoteResponse = util::http_error_handling(response).await.with_context(|| { format!("error requesting sanctum route between {input_mint} and {output_mint} (using url: {})", config.sanctum_url) })?; Ok(quote) } /// Find the instructions and account lookup tables for a sanctum swap through mango pub async fn prepare_swap_transaction( &self, input_mint: Pubkey, output_mint: Pubkey, max_slippage_bps: u64, quote: &QuoteResponse, ) -> anyhow::Result { tracing::info!("swapping using sanctum"); let source_token = self.mango_client.context.token_by_mint(&input_mint)?; let target_token = self.mango_client.context.token_by_mint(&output_mint)?; let bank_ams = [source_token.first_bank(), target_token.first_bank()] .into_iter() .map(util::to_writable_account_meta) .collect::>(); let vault_ams = [source_token.first_vault(), target_token.first_vault()] .into_iter() .map(util::to_writable_account_meta) .collect::>(); let authority = self.mango_client.authority(); let account = &self.mango_client.mango_account().await?; let token_ams = [source_token.mint, target_token.mint] .into_iter() .map(|mint| { util::to_writable_account_meta( anchor_spl::associated_token::get_associated_token_address(&authority, &mint), ) }) .collect::>(); let source_loan = quote .in_amount .as_ref() .map(|v| u64::from_str(v).unwrap()) .unwrap_or(0); 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! let (health_ams, _health_cu) = self .mango_client .derive_health_check_remaining_account_metas( account, vec![source_token.token_index, target_token.token_index], vec![source_token.token_index, target_token.token_index], vec![], ) .await .context("building health accounts")?; let config = self.mango_client.client.config(); let in_amount = quote .in_amount .clone() .expect("sanctum require a in amount"); let quote_amount_u64 = quote.out_amount.parse::()?; let out_amount = ((quote_amount_u64 as f64) * (1.0 - (max_slippage_bps as f64) / 10_000.0)) .ceil() as u64; let swap_response = self .mango_client .http_client .post(format!("{}/swap", config.sanctum_url)) .json(&SwapRequest { amount: in_amount.clone(), quoted_amount: out_amount.to_string(), input: input_mint.to_string(), mode: "ExactIn".to_string(), output_lst_mint: output_mint.to_string(), signer: authority.to_string(), swap_src: quote.swap_src.clone(), }) .timeout(self.timeout_duration) .send() .await .context("swap transaction request to sanctum")?; let swap_r: SanctumSwapResponse = util::http_error_handling(swap_response) .await .context("error requesting sanctum swap")?; let tx = bincode::options() .with_fixint_encoding() .reject_trailing_bytes() .deserialize::( &base64::decode(&swap_r.tx).context("base64 decoding sanctum transaction")?, ) .context("parsing sanctum transaction")?; let (sanctum_ixs_orig, sanctum_alts) = self .mango_client .deserialize_instructions_and_alts(&tx.message) .await?; let system_program = system_program::ID; let ata_program = anchor_spl::associated_token::ID; let token_program = anchor_spl::token::ID; let compute_budget_program: Pubkey = solana_sdk::compute_budget::ID; // 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 sync_native_pack = anchor_spl::token::spl_token::instruction::TokenInstruction::SyncNative.pack(); // Remove auto wrapping of SOL->wSOL let sanctum_ixs: Vec = sanctum_ixs_orig .clone() .into_iter() .filter(|ix| { !(ix.program_id == system_program) && !(ix.program_id == token_program && ix.data == sync_native_pack) }) .collect(); let sanctum_action_ix_begin = sanctum_ixs .iter() .position(|ix| !is_setup_ix(ix.program_id)) .ok_or_else(|| { anyhow::anyhow!("sanctum swap response only had setup-like instructions") })?; let sanctum_action_ix_end = sanctum_ixs.len() - sanctum_ixs .iter() .rev() .position(|ix| !is_setup_ix(ix.program_id)) .unwrap(); let mut instructions: Vec = Vec::new(); for ix in &sanctum_ixs[..sanctum_action_ix_begin] { instructions.push(ix.clone()); } // Ensure the source token account is created (sanctum takes care of the output account) instructions.push( spl_associated_token_account::instruction::create_associated_token_account_idempotent( &authority, &authority, &source_token.mint, &Token::id(), ), ); instructions.push(Instruction { program_id: mango_v4::id(), accounts: { let mut ams = anchor_lang::ToAccountMetas::to_account_metas( &mango_v4::accounts::FlashLoanBegin { account: self.mango_client.mango_account_address, owner: authority, token_program: Token::id(), instructions: solana_sdk::sysvar::instructions::id(), }, None, ); ams.extend(bank_ams); ams.extend(vault_ams.clone()); ams.extend(token_ams.clone()); ams.push(util::to_readonly_account_meta(self.mango_client.group())); ams }, data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanBegin { loan_amounts, }), }); for ix in &sanctum_ixs[sanctum_action_ix_begin..sanctum_action_ix_end] { instructions.push(ix.clone()); } instructions.push(Instruction { program_id: mango_v4::id(), accounts: { let mut ams = anchor_lang::ToAccountMetas::to_account_metas( &mango_v4::accounts::FlashLoanEnd { account: self.mango_client.mango_account_address, owner: authority, token_program: Token::id(), }, None, ); ams.extend(health_ams); ams.extend(vault_ams); ams.extend(token_ams); ams.push(util::to_readonly_account_meta(self.mango_client.group())); ams }, data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanEndV2 { num_loans, flash_loan_type: mango_v4::accounts_ix::FlashLoanType::Swap, }), }); for ix in &sanctum_ixs[sanctum_action_ix_end..] { instructions.push(ix.clone()); } let mut address_lookup_tables = self.mango_client.mango_address_lookup_tables().await?; address_lookup_tables.extend(sanctum_alts.into_iter()); let payer = authority; // maybe use fee_payer? but usually it's the same Ok(TransactionBuilder { instructions, address_lookup_tables, payer, signers: vec![self.mango_client.authority.clone()], config: self .mango_client .client .config() .transaction_builder_config .clone(), }) } pub async fn swap( &self, input_mint: Pubkey, output_mint: Pubkey, max_slippage_bps: u64, amount: u64, ) -> anyhow::Result { let route = self.quote(input_mint, output_mint, amount).await?; let tx_builder = self .prepare_swap_transaction(input_mint, output_mint, max_slippage_bps, &route) .await?; tx_builder.send_and_confirm(&self.mango_client.client).await } } pub async fn load_supported_token_mints( live_rpc_client: &RpcClient, ) -> anyhow::Result> { let address = Pubkey::from_str("EhWxBHdmQ3yDmPzhJbKtGMM9oaZD42emt71kSieghy5")?; let lookup_table_data = live_rpc_client.get_account(&address).await?; let lookup_table = AddressLookupTable::deserialize(&lookup_table_data.data())?; let accounts: Vec = fetch_multiple_accounts_in_chunks(live_rpc_client, &lookup_table.addresses, 100, 1) .await? .into_iter() .map(|x| x.1) .collect(); let mut lst_mints = HashSet::new(); for account in accounts { let account = Account::from(account); let mut account_data = account.data(); let t = sanctum_state::StakePool::deserialize(&mut account_data); if let Ok(d) = t { lst_mints.insert(d.pool_mint); } } // taken from https://github.com/igneous-labs/sanctum-lst-list/blob/master/sanctum-lst-list.toml let hardcoded_lst_mints = [ "pathdXw4He1Xk3eX84pDdDZnGKEme3GivBamGCVPZ5a", // pathSOL "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v", // JupSOL "BgYgFYq4A9a2o5S1QbWkmYVFBh7LBQL8YvugdhieFg38", // juicingJupSOL "pWrSoLAhue6jUxUkbWgmEy5rD9VJzkFmvfTDV5KgNuu", // pwrSOL "suPer8CPwxoJPQ7zksGMwFvjBQhjAHwUMmPV4FVatBw", // superSOL "jucy5XJ76pHVvtPZb5TKRcGQExkwit2P5s4vY8UzmpC", // jucySOL "BonK1YhkXEGLZzwtcvRTip3gAL9nCeQD7ppZBLXhtTs", // bonkSOL "Dso1bDeDjCQxTrWHqUUi63oBvV7Mdm6WaobLbQ7gnPQ", // dSOL "Comp4ssDzXcLeu2MnLuGNNFC4cmLPMng8qWHPvzAMU1h", // compassSOL "picobAEvs6w7QEknPce34wAE4gknZA9v5tTonnmHYdX", // picoSOL "GRJQtWwdJmp5LLpy8JWjPgn5FnLyqSJGNhn5ZnCTFUwM", // clockSOL "HUBsveNpjo5pWqNkH57QzxjQASdTVXcSK7bVKTSZtcSX", // hubSOL "strng7mqqc1MBJJV6vMzYbEqnwVGvKKGKedeCvtktWA", // strongSOL "LnTRntk2kTfWEY6cVB8K9649pgJbt6dJLS1Ns1GZCWg", // lanternSOL "st8QujHLPsX3d6HG9uQg9kJ91jFxUgruwsb1hyYXSNd", // stakeSOL "pumpkinsEq8xENVZE6QgTS93EN4r9iKvNxNALS1ooyp", // pumpkinSOL "CgnTSoL3DgY9SFHxcLj6CgCgKKoTBr6tp4CPAEWy25DE", // cgntSOL "LAinEtNLgpmCP9Rvsf5Hn8W6EhNiKLZQti1xfWMLy6X", // laineSOL "vSoLxydx6akxyMD9XEcPvGYNGq6Nn66oqVb3UkGkei7", // vSOL "bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1", // bSOL "GEJpt3Wjmr628FqXxTgxMce1pLntcPV4uFi8ksxMyPQh", // daoSOL "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn", // JitoSOL "7Q2afV64in6N6SeZsAAB81TJzwDoD6zpqmHkzi9Dcavn", // JSOL "LSTxxxnJzKDFSLr4dUkPcmCf5VyryEqzPLz5j4bpxFp", // LST "Zippybh3S5xYYam2nvL6hVJKz1got6ShgV4DyD1XQYF", // zippySOL "edge86g9cVz87xcpKpy3J77vbp4wYd9idEV562CCntt", // edgeSOL "So11111111111111111111111111111111111111112", // SOL "5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm", // INF "7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj", // stSOL "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", // mSOL ]; // Hardcoded for now lst_mints.extend(hardcoded_lst_mints.map(|x| Pubkey::from_str(x).expect("invalid mint"))); Ok(lst_mints) }