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 81525ed139
commit 053b3d0f29
8 changed files with 132 additions and 79 deletions

View File

@ -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);

View File

@ -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()

View File

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

View File

@ -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?;

View File

@ -172,6 +172,7 @@ pub async fn maybe_execute_token_conditional_swap_inner(
input_amount,
slippage,
swap_mode,
false,
config.mock_jupiter,
)
.await?;

View File

@ -45,11 +45,19 @@ pub async fn jupiter_route(
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)
.jupiter_route(
input_mint,
output_mint,
amount,
slippage,
swap_mode,
only_direct_routes,
)
.await;
}

View File

@ -251,7 +251,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;
@ -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<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);
@ -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<Keypair>,
payer: Arc<Keypair>, // 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<jupiter::QueryRoute> {
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<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_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::<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(),
@ -1522,14 +1532,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!
@ -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<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(
@ -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<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?;
@ -1765,11 +1788,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,
@ -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::<Vec<_>>();
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<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)
@ -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<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!
@ -1811,6 +1836,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