Client: jupiter swap and tx builder improvements

- jupiter swap now supports multiple hops
- tx builder can check resulting tx size

(cherry picked from commit c58ee91356)
This commit is contained in:
Christian Kamm 2023-07-10 10:40:48 +02:00
parent d5472d790e
commit 2be2c29101
6 changed files with 232 additions and 79 deletions

View File

@ -138,7 +138,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
@ -156,9 +156,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);
}
@ -185,6 +191,7 @@ async fn main() -> Result<(), anyhow::Error> {
cmd.amount,
cmd.slippage_bps,
JupiterSwapMode::ExactIn,
false,
)
.await?;
println!("{}", txsig);

View File

@ -41,6 +41,7 @@ pub async fn jupiter_market_can_buy(
quote_amount,
slippage,
JupiterSwapMode::ExactIn,
false,
)
.await
.is_ok()
@ -69,6 +70,7 @@ pub async fn jupiter_market_can_sell(
quote_amount,
slippage,
JupiterSwapMode::ExactOut,
false,
)
.await
.is_ok()

View File

@ -165,6 +165,7 @@ impl Rebalancer {
input_amount.to_num::<u64>(),
self.config.slippage_bps,
JupiterSwapMode::ExactIn,
false,
)
.await?;
log::info!(
@ -197,6 +198,7 @@ impl Rebalancer {
amount.to_num::<u64>(),
self.config.slippage_bps,
JupiterSwapMode::ExactIn,
false,
)
.await?;
log::info!(

View File

@ -1,10 +1,14 @@
use mango_v4::accounts_zerocopy::*;
use mango_v4::state::{Bank, MintInfo, PerpMarket};
use mango_v4::state::{Bank, MangoAccountValue, MintInfo, PerpMarket, TokenIndex};
use anyhow::Context;
use fixed::types::I80F48;
use solana_sdk::account::AccountSharedData;
use solana_sdk::pubkey::Pubkey;
pub use mango_v4_client::snapshot_source::is_mango_account;
use mango_v4_client::{chain_data, JupiterSwapMode, MangoClient};
pub fn is_mango_bank<'a>(account: &'a AccountSharedData, group_id: &Pubkey) -> Option<&'a Bank> {
let bank = account.load::<Bank>().ok()?;
@ -32,3 +36,110 @@ pub fn is_perp_market<'a>(
}
Some(perp_market)
}
/// A wrapper that can mock the response
pub async fn jupiter_route(
mango_client: &MangoClient,
input_mint: Pubkey,
output_mint: Pubkey,
amount: u64,
slippage: u64,
swap_mode: JupiterSwapMode,
only_direct_routes: bool,
mock: bool,
) -> anyhow::Result<mango_v4_client::jupiter::QueryRoute> {
if !mock {
return mango_client
.jupiter_route(
input_mint,
output_mint,
amount,
slippage,
swap_mode,
only_direct_routes,
)
.await;
}
let input_price = mango_client
.bank_oracle_price(mango_client.context.token_by_mint(&input_mint)?.token_index)
.await?;
let output_price = mango_client
.bank_oracle_price(
mango_client
.context
.token_by_mint(&output_mint)?
.token_index,
)
.await?;
let in_amount: u64;
let out_amount: u64;
let other_amount_threshold: u64;
let swap_mode_str;
match swap_mode {
JupiterSwapMode::ExactIn => {
in_amount = amount;
out_amount = (I80F48::from(amount) * input_price / output_price).to_num();
other_amount_threshold = out_amount;
swap_mode_str = "ExactIn".to_string();
}
JupiterSwapMode::ExactOut => {
in_amount = (I80F48::from(amount) * output_price / input_price).to_num();
out_amount = amount;
other_amount_threshold = in_amount;
swap_mode_str = "ExactOut".to_string();
}
}
Ok(mango_v4_client::jupiter::QueryRoute {
in_amount: in_amount.to_string(),
out_amount: out_amount.to_string(),
price_impact_pct: 0.1,
market_infos: vec![],
amount: amount.to_string(),
slippage_bps: 1,
other_amount_threshold: other_amount_threshold.to_string(),
swap_mode: swap_mode_str,
fees: None,
})
}
/// Convenience wrapper for getting max swap amounts for a token pair
pub async fn max_swap_source(
client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
account: &MangoAccountValue,
source: TokenIndex,
target: TokenIndex,
price: I80F48,
min_health_ratio: I80F48,
) -> anyhow::Result<I80F48> {
let mut account = account.clone();
// Ensure the tokens are activated, so they appear in the health cache and
// max_swap_source() will work.
account.ensure_token_position(source)?;
account.ensure_token_position(target)?;
let health_cache =
mango_v4_client::health_cache::new(&client.context, account_fetcher, &account)
.await
.expect("always ok");
let source_bank = client.first_bank(source).await?;
let target_bank = client.first_bank(target).await?;
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
let amount = health_cache
.max_swap_source_for_health_ratio(
&account,
&source_bank,
source_price,
&target_bank,
price,
min_health_ratio,
)
.context("getting max_swap_source")?;
Ok(amount)
}

View File

@ -250,7 +250,7 @@ struct SettleBatchProcessor<'a> {
impl<'a> SettleBatchProcessor<'a> {
fn transaction(&self) -> anyhow::Result<VersionedTransaction> {
let client = &self.mango_client.client;
let fee_payer = &*client.fee_payer;
let fee_payer = client.fee_payer.clone();
TransactionBuilder {
instructions: self.instructions.clone(),

View File

@ -1,3 +1,4 @@
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
@ -133,16 +134,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<Keypair>,
payer: Arc<Keypair>, // pays the SOL for the new account
mango_account_name: &str,
) -> anyhow::Result<Pubkey> {
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);
@ -157,12 +158,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)
@ -173,8 +180,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<Keypair>,
payer: Arc<Keypair>, // pays the SOL for the new account
account_num: u32,
mango_account_name: &str,
) -> anyhow::Result<(Pubkey, Signature)> {
@ -1305,6 +1312,7 @@ impl MangoClient {
amount: u64,
slippage: u64,
swap_mode: JupiterSwapMode,
only_direct_routes: bool,
) -> anyhow::Result<jupiter::QueryRoute> {
let quote = self
.http_client
@ -1313,7 +1321,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)),
@ -1353,19 +1361,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<Signature> {
route: &jupiter::QueryRoute,
) -> anyhow::Result<TransactionBuilder> {
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 = self
.http_client
@ -1400,22 +1407,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::<Vec<_>>();
let jup_action_ix = jup_ixs
.into_iter()
.filter(|ix| !is_setup_ix(ix.program_id))
.collect::<Vec<_>>();
.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(),
@ -1445,14 +1455,14 @@ impl MangoClient {
})
.collect::<Vec<_>>();
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!
@ -1467,25 +1477,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: {
@ -1508,7 +1502,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 {
@ -1533,20 +1527,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<Signature> {
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(
@ -1630,7 +1653,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)
@ -1645,7 +1668,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)
@ -1677,17 +1700,17 @@ pub struct TransactionBuilderConfig {
pub prioritization_micro_lamports: Option<u64>,
}
pub struct TransactionBuilder<'a> {
pub struct TransactionBuilder {
pub instructions: Vec<Instruction>,
pub address_lookup_tables: Vec<AddressLookupTableAccount>,
pub signers: Vec<&'a Keypair>,
pub signers: Vec<Arc<Keypair>>,
pub payer: Pubkey,
pub config: TransactionBuilderConfig,
}
impl<'a> TransactionBuilder<'a> {
impl TransactionBuilder {
pub async fn transaction(
self,
&self,
rpc: &RpcClientAsync,
) -> anyhow::Result<solana_sdk::transaction::VersionedTransaction> {
let latest_blockhash = rpc.get_latest_blockhash().await?;
@ -1695,11 +1718,12 @@ impl<'a> TransactionBuilder<'a> {
}
pub fn transaction_with_blockhash(
mut self,
&self,
blockhash: Hash,
) -> anyhow::Result<solana_sdk::transaction::VersionedTransaction> {
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,
@ -1708,15 +1732,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::<Vec<_>>();
let tx =
solana_sdk::transaction::VersionedTransaction::try_new(versioned_message, &signers)?;
@ -1725,7 +1750,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<Signature> {
pub async fn send(&self, client: &Client) -> anyhow::Result<Signature> {
let rpc = client.rpc_async();
let tx = self.transaction(&rpc).await?;
rpc.send_transaction_with_config(&tx, client.rpc_send_transaction_config)
@ -1733,7 +1758,7 @@ impl<'a> TransactionBuilder<'a> {
.map_err(prettify_solana_client_error)
}
pub async fn send_and_confirm(self, client: &Client) -> anyhow::Result<Signature> {
pub async fn send_and_confirm(&self, client: &Client) -> anyhow::Result<Signature> {
let rpc = client.rpc_async();
let tx = self.transaction(&rpc).await?;
// TODO: Wish we could use client.rpc_send_transaction_config here too!
@ -1741,6 +1766,12 @@ impl<'a> TransactionBuilder<'a> {
.await
.map_err(prettify_solana_client_error)
}
pub fn transaction_size_ok(&self) -> anyhow::Result<bool> {
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