mango-v4/lib/client/src/swap/sanctum.rs

407 lines
15 KiB
Rust

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<String>,
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<QuoteResponse> {
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<TransactionBuilder> {
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::<Vec<_>>();
let vault_ams = [source_token.first_vault(), target_token.first_vault()]
.into_iter()
.map(util::to_writable_account_meta)
.collect::<Vec<_>>();
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::<Vec<_>>();
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::<u64>()?;
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::<solana_sdk::transaction::VersionedTransaction>(
&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<Instruction> = 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<Instruction> = 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<Signature> {
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<HashSet<Pubkey>> {
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<Account> =
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)
}