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:
parent
81525ed139
commit
053b3d0f29
|
@ -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);
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -172,6 +172,7 @@ pub async fn maybe_execute_token_conditional_swap_inner(
|
|||
input_amount,
|
||||
slippage,
|
||||
swap_mode,
|
||||
false,
|
||||
config.mock_jupiter,
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue