client/liquidator: jupiter v6 (#684)
Add rust client functions for v6 API that are usuable in parallel to the v4 ones.
This commit is contained in:
parent
6e2363c86f
commit
0f10cb4d92
|
@ -1,7 +1,6 @@
|
|||
use clap::{Args, Parser, Subcommand};
|
||||
use mango_v4_client::{
|
||||
keypair_from_cli, pubkey_from_cli, Client, JupiterSwapMode, MangoClient,
|
||||
TransactionBuilderConfig,
|
||||
keypair_from_cli, pubkey_from_cli, Client, MangoClient, TransactionBuilderConfig,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::str::FromStr;
|
||||
|
@ -193,14 +192,8 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||
let output_mint = pubkey_from_cli(&cmd.output_mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let txsig = client
|
||||
.jupiter_swap(
|
||||
input_mint,
|
||||
output_mint,
|
||||
cmd.amount,
|
||||
cmd.slippage_bps,
|
||||
JupiterSwapMode::ExactIn,
|
||||
false,
|
||||
)
|
||||
.jupiter_v6()
|
||||
.swap(input_mint, output_mint, cmd.amount, cmd.slippage_bps, false)
|
||||
.await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ more advanced parameters
|
|||
- `SNAPSHOT_INTERVAL_SECS` - how frequently to request a full on-chain snapshot (default 5min)
|
||||
- `PARALLEL_RPC_REQUESTS` - number of allowed parallel rpc calls (default 10)
|
||||
- `TELEMETRY` - report the liquidator's existence and pubkey occasionally (default true)
|
||||
- `MOCK_JUPITER` - replace jupiter queries with mocks (for devnet testing only)
|
||||
- `JUPITER_VERSION` - choose between v4 and v6 jupiter (or mock, for devnet testing only)
|
||||
|
||||
```shell
|
||||
cargo run --bin liquidator
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::time::Duration;
|
|||
use itertools::Itertools;
|
||||
use mango_v4::health::{HealthCache, HealthType};
|
||||
use mango_v4::state::{MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX};
|
||||
use mango_v4_client::{chain_data, health_cache, JupiterSwapMode, MangoClient};
|
||||
use mango_v4_client::{chain_data, health_cache, MangoClient};
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
|
@ -18,68 +18,9 @@ use crate::util;
|
|||
pub struct Config {
|
||||
pub min_health_ratio: f64,
|
||||
pub refresh_timeout: Duration,
|
||||
pub mock_jupiter: bool,
|
||||
pub compute_limit_for_liq_ix: u32,
|
||||
}
|
||||
|
||||
pub async fn jupiter_market_can_buy(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
) -> bool {
|
||||
if token == quote_token {
|
||||
return true;
|
||||
}
|
||||
let token_mint = mango_client.context.token(token).mint_info.mint;
|
||||
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
|
||||
|
||||
// Consider a market alive if we can swap $10 worth at 1% slippage
|
||||
// TODO: configurable
|
||||
// TODO: cache this, no need to recheck often
|
||||
let quote_amount = 10_000_000u64;
|
||||
let slippage = 100;
|
||||
mango_client
|
||||
.jupiter_route(
|
||||
quote_token_mint,
|
||||
token_mint,
|
||||
quote_amount,
|
||||
slippage,
|
||||
JupiterSwapMode::ExactIn,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn jupiter_market_can_sell(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
) -> bool {
|
||||
if token == quote_token {
|
||||
return true;
|
||||
}
|
||||
let token_mint = mango_client.context.token(token).mint_info.mint;
|
||||
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
|
||||
|
||||
// Consider a market alive if we can swap $10 worth at 1% slippage
|
||||
// TODO: configurable
|
||||
// TODO: cache this, no need to recheck often
|
||||
let quote_amount = 10_000_000u64;
|
||||
let slippage = 100;
|
||||
mango_client
|
||||
.jupiter_route(
|
||||
token_mint,
|
||||
quote_token_mint,
|
||||
quote_amount,
|
||||
slippage,
|
||||
JupiterSwapMode::ExactOut,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
struct LiquidateHelper<'a> {
|
||||
client: &'a MangoClient,
|
||||
account_fetcher: &'a chain_data::AccountFetcher,
|
||||
|
|
|
@ -7,8 +7,9 @@ use anchor_client::Cluster;
|
|||
use clap::Parser;
|
||||
use mango_v4::state::{PerpMarketIndex, TokenIndex};
|
||||
use mango_v4_client::{
|
||||
account_update_stream, chain_data, keypair_from_cli, snapshot_source, websocket_source, Client,
|
||||
MangoClient, MangoClientError, MangoGroupContext, TransactionBuilderConfig,
|
||||
account_update_stream, chain_data, jupiter, keypair_from_cli, snapshot_source,
|
||||
websocket_source, Client, MangoClient, MangoClientError, MangoGroupContext,
|
||||
TransactionBuilderConfig,
|
||||
};
|
||||
|
||||
use itertools::Itertools;
|
||||
|
@ -49,6 +50,23 @@ enum BoolArg {
|
|||
False,
|
||||
}
|
||||
|
||||
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum JupiterVersionArg {
|
||||
Mock,
|
||||
V4,
|
||||
V6,
|
||||
}
|
||||
|
||||
impl From<JupiterVersionArg> for jupiter::Version {
|
||||
fn from(a: JupiterVersionArg) -> Self {
|
||||
match a {
|
||||
JupiterVersionArg::Mock => jupiter::Version::Mock,
|
||||
JupiterVersionArg::V4 => jupiter::Version::V4,
|
||||
JupiterVersionArg::V6 => jupiter::Version::V6,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
struct Cli {
|
||||
|
@ -98,11 +116,9 @@ struct Cli {
|
|||
#[clap(long, env, default_value = "300000")]
|
||||
compute_limit_for_tcs: u32,
|
||||
|
||||
/// use a jupiter mock instead of actual queries
|
||||
///
|
||||
/// This is required for devnet testing.
|
||||
#[clap(long, env, value_enum, default_value = "false")]
|
||||
mock_jupiter: BoolArg,
|
||||
/// control which version of jupiter to use
|
||||
#[clap(long, env, value_enum, default_value = "v6")]
|
||||
jupiter_version: JupiterVersionArg,
|
||||
|
||||
/// report liquidator's existence and pubkey
|
||||
#[clap(long, env, value_enum, default_value = "true")]
|
||||
|
@ -243,7 +259,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
let token_swap_info_config = token_swap_info::Config {
|
||||
quote_index: 0, // USDC
|
||||
quote_amount: 1_000_000_000, // TODO: config, $1000, should be >= tcs_config.max_trigger_quote_amount
|
||||
mock_jupiter: cli.mock_jupiter == BoolArg::True,
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
};
|
||||
|
||||
let token_swap_info_updater = Arc::new(token_swap_info::TokenSwapInfoUpdater::new(
|
||||
|
@ -253,7 +269,6 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let liq_config = liquidate::Config {
|
||||
min_health_ratio: cli.min_health_ratio,
|
||||
mock_jupiter: cli.mock_jupiter == BoolArg::True,
|
||||
compute_limit_for_liq_ix: cli.compute_limit_for_liquidation,
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
|
@ -262,7 +277,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
let tcs_config = trigger_tcs::Config {
|
||||
min_health_ratio: cli.min_health_ratio,
|
||||
max_trigger_quote_amount: 1_000_000_000, // TODO: config, $1000
|
||||
mock_jupiter: cli.mock_jupiter == BoolArg::True,
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
compute_limit_for_trigger: cli.compute_limit_for_tcs,
|
||||
// TODO: config
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
|
@ -275,6 +290,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
// TODO: config
|
||||
borrow_settle_excess: 1.05,
|
||||
refresh_timeout: Duration::from_secs(30),
|
||||
jupiter_version: cli.jupiter_version.into(),
|
||||
};
|
||||
|
||||
let rebalancer = Arc::new(rebalance::Rebalancer {
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
use itertools::Itertools;
|
||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||
use mango_v4::state::{
|
||||
Bank, BookSide, MangoAccountValue, PerpPosition, PlaceOrderType, Side, TokenIndex,
|
||||
TokenPosition, QUOTE_TOKEN_INDEX,
|
||||
Bank, BookSide, MangoAccountValue, PerpPosition, PlaceOrderType, Side, QUOTE_TOKEN_INDEX,
|
||||
};
|
||||
use mango_v4_client::{
|
||||
chain_data, jupiter::QueryRoute, perp_pnl, AnyhowWrap, JupiterSwapMode, MangoClient,
|
||||
PerpMarketContext, TokenContext, TransactionBuilder,
|
||||
chain_data, jupiter, perp_pnl, MangoClient, PerpMarketContext, TokenContext,
|
||||
TransactionBuilder, TransactionSize,
|
||||
};
|
||||
|
||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
use solana_sdk::signature::Signature;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use std::time::Duration;
|
||||
use tracing::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
@ -27,55 +25,14 @@ pub struct Config {
|
|||
/// If this is 1.05, then it'll swap borrow_value * 1.05 quote token into borrow token.
|
||||
pub borrow_settle_excess: f64,
|
||||
pub refresh_timeout: Duration,
|
||||
pub jupiter_version: jupiter::Version,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TokenState {
|
||||
price: I80F48,
|
||||
native_position: I80F48,
|
||||
in_use: bool,
|
||||
}
|
||||
|
||||
impl TokenState {
|
||||
fn new_position(
|
||||
token: &TokenContext,
|
||||
position: &TokenPosition,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<Self> {
|
||||
let bank = Self::bank(token, account_fetcher)?;
|
||||
Ok(Self {
|
||||
price: Self::fetch_price(token, &bank, account_fetcher)?,
|
||||
native_position: position.native(&bank),
|
||||
in_use: position.is_in_use(),
|
||||
})
|
||||
}
|
||||
|
||||
fn bank(
|
||||
token: &TokenContext,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<Bank> {
|
||||
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
|
||||
}
|
||||
|
||||
fn fetch_price(
|
||||
token: &TokenContext,
|
||||
bank: &Bank,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<I80F48> {
|
||||
let oracle = account_fetcher.fetch_raw(&token.mint_info.oracle)?;
|
||||
bank.oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
None,
|
||||
)
|
||||
.map_err_anyhow()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct WrappedJupRoute {
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
route: QueryRoute,
|
||||
fn token_bank(
|
||||
token: &TokenContext,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<Bank> {
|
||||
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
|
||||
}
|
||||
|
||||
pub struct Rebalancer {
|
||||
|
@ -122,30 +79,25 @@ impl Rebalancer {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
/// Wrapping client.jupiter_route() in a way that preserves the in/out mints
|
||||
async fn jupiter_route(
|
||||
async fn jupiter_quote(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
only_direct_routes: bool,
|
||||
) -> anyhow::Result<WrappedJupRoute> {
|
||||
let route = self
|
||||
.mango_client
|
||||
.jupiter_route(
|
||||
jupiter_version: jupiter::Version,
|
||||
) -> anyhow::Result<jupiter::Quote> {
|
||||
self.mango_client
|
||||
.jupiter()
|
||||
.quote(
|
||||
input_mint,
|
||||
output_mint,
|
||||
amount,
|
||||
self.config.slippage_bps,
|
||||
JupiterSwapMode::ExactIn,
|
||||
only_direct_routes,
|
||||
jupiter_version,
|
||||
)
|
||||
.await?;
|
||||
Ok(WrappedJupRoute {
|
||||
input_mint,
|
||||
output_mint,
|
||||
route,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Grab three possible routes:
|
||||
|
@ -157,7 +109,7 @@ impl Rebalancer {
|
|||
&self,
|
||||
output_mint: Pubkey,
|
||||
in_amount_quote: u64,
|
||||
) -> anyhow::Result<(Signature, WrappedJupRoute)> {
|
||||
) -> anyhow::Result<(Signature, jupiter::Quote)> {
|
||||
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
||||
let sol_token = self.mango_client.context.token(
|
||||
*self
|
||||
|
@ -167,40 +119,56 @@ impl Rebalancer {
|
|||
.get("SOL") // TODO: better use mint
|
||||
.unwrap(),
|
||||
);
|
||||
let quote_mint = quote_token.mint_info.mint;
|
||||
let sol_mint = sol_token.mint_info.mint;
|
||||
let jupiter_version = self.config.jupiter_version;
|
||||
|
||||
let full_route_job = self.jupiter_route(
|
||||
quote_token.mint_info.mint,
|
||||
let full_route_job = self.jupiter_quote(
|
||||
quote_mint,
|
||||
output_mint,
|
||||
in_amount_quote,
|
||||
false,
|
||||
jupiter_version,
|
||||
);
|
||||
let direct_quote_route_job = self.jupiter_route(
|
||||
quote_token.mint_info.mint,
|
||||
let direct_quote_route_job = self.jupiter_quote(
|
||||
quote_mint,
|
||||
output_mint,
|
||||
in_amount_quote,
|
||||
true,
|
||||
jupiter_version,
|
||||
);
|
||||
|
||||
// For the SOL -> output route we need to adjust the in amount by the SOL price
|
||||
let sol_bank = TokenState::bank(sol_token, &self.account_fetcher)?;
|
||||
let sol_price = TokenState::fetch_price(sol_token, &sol_bank, &self.account_fetcher)?;
|
||||
let sol_price = self
|
||||
.account_fetcher
|
||||
.fetch_bank_price(&sol_token.mint_info.first_bank())?;
|
||||
let in_amount_sol = (I80F48::from(in_amount_quote) / sol_price)
|
||||
.ceil()
|
||||
.to_num::<u64>();
|
||||
let direct_sol_route_job =
|
||||
self.jupiter_route(sol_token.mint_info.mint, output_mint, in_amount_sol, true);
|
||||
self.jupiter_quote(sol_mint, output_mint, in_amount_sol, true, jupiter_version);
|
||||
|
||||
let (full_route, direct_quote_route, direct_sol_route) =
|
||||
tokio::join!(full_route_job, direct_quote_route_job, direct_sol_route_job);
|
||||
let alternatives = [direct_quote_route, direct_sol_route]
|
||||
.into_iter()
|
||||
.filter_map(|v| v.ok())
|
||||
.collect_vec();
|
||||
let mut jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
|
||||
|
||||
// for v6, add a v4 fallback
|
||||
if self.config.jupiter_version == jupiter::Version::V6 {
|
||||
jobs.push(self.jupiter_quote(
|
||||
quote_mint,
|
||||
output_mint,
|
||||
in_amount_quote,
|
||||
false,
|
||||
jupiter::Version::V4,
|
||||
));
|
||||
}
|
||||
|
||||
let mut results = futures::future::join_all(jobs).await;
|
||||
let full_route = results.remove(0)?;
|
||||
let alternatives = results.into_iter().filter_map(|v| v.ok()).collect_vec();
|
||||
|
||||
let (tx_builder, route) = self
|
||||
.determine_best_jupiter_tx(
|
||||
// If the best_route couldn't be fetched, something is wrong
|
||||
&full_route?,
|
||||
&full_route,
|
||||
&alternatives,
|
||||
)
|
||||
.await?;
|
||||
|
@ -219,7 +187,7 @@ impl Rebalancer {
|
|||
&self,
|
||||
input_mint: Pubkey,
|
||||
in_amount: u64,
|
||||
) -> anyhow::Result<(Signature, WrappedJupRoute)> {
|
||||
) -> anyhow::Result<(Signature, jupiter::Quote)> {
|
||||
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
||||
let sol_token = self.mango_client.context.token(
|
||||
*self
|
||||
|
@ -229,24 +197,38 @@ impl Rebalancer {
|
|||
.get("SOL") // TODO: better use mint
|
||||
.unwrap(),
|
||||
);
|
||||
let quote_mint = quote_token.mint_info.mint;
|
||||
let sol_mint = sol_token.mint_info.mint;
|
||||
let jupiter_version = self.config.jupiter_version;
|
||||
|
||||
let full_route_job =
|
||||
self.jupiter_route(input_mint, quote_token.mint_info.mint, in_amount, false);
|
||||
self.jupiter_quote(input_mint, quote_mint, in_amount, false, jupiter_version);
|
||||
let direct_quote_route_job =
|
||||
self.jupiter_route(input_mint, quote_token.mint_info.mint, in_amount, true);
|
||||
self.jupiter_quote(input_mint, quote_mint, in_amount, true, jupiter_version);
|
||||
let direct_sol_route_job =
|
||||
self.jupiter_route(input_mint, sol_token.mint_info.mint, in_amount, true);
|
||||
let (full_route, direct_quote_route, direct_sol_route) =
|
||||
tokio::join!(full_route_job, direct_quote_route_job, direct_sol_route_job);
|
||||
let alternatives = [direct_quote_route, direct_sol_route]
|
||||
.into_iter()
|
||||
.filter_map(|v| v.ok())
|
||||
.collect_vec();
|
||||
self.jupiter_quote(input_mint, sol_mint, in_amount, true, jupiter_version);
|
||||
|
||||
let mut jobs = vec![full_route_job, direct_quote_route_job, direct_sol_route_job];
|
||||
|
||||
// for v6, add a v4 fallback
|
||||
if self.config.jupiter_version == jupiter::Version::V6 {
|
||||
jobs.push(self.jupiter_quote(
|
||||
input_mint,
|
||||
quote_mint,
|
||||
in_amount,
|
||||
false,
|
||||
jupiter::Version::V4,
|
||||
));
|
||||
}
|
||||
|
||||
let mut results = futures::future::join_all(jobs).await;
|
||||
let full_route = results.remove(0)?;
|
||||
let alternatives = results.into_iter().filter_map(|v| v.ok()).collect_vec();
|
||||
|
||||
let (tx_builder, route) = self
|
||||
.determine_best_jupiter_tx(
|
||||
// If the best_route couldn't be fetched, something is wrong
|
||||
&full_route?,
|
||||
&full_route,
|
||||
&alternatives,
|
||||
)
|
||||
.await?;
|
||||
|
@ -259,25 +241,24 @@ impl Rebalancer {
|
|||
|
||||
async fn determine_best_jupiter_tx(
|
||||
&self,
|
||||
full: &WrappedJupRoute,
|
||||
alternatives: &[WrappedJupRoute],
|
||||
) -> anyhow::Result<(TransactionBuilder, WrappedJupRoute)> {
|
||||
full: &jupiter::Quote,
|
||||
alternatives: &[jupiter::Quote],
|
||||
) -> anyhow::Result<(TransactionBuilder, jupiter::Quote)> {
|
||||
let builder = self
|
||||
.mango_client
|
||||
.prepare_jupiter_swap_transaction(full.input_mint, full.output_mint, &full.route)
|
||||
.jupiter()
|
||||
.prepare_swap_transaction(full)
|
||||
.await?;
|
||||
if builder.transaction_size_ok()? {
|
||||
let tx_size = builder.transaction_size()?;
|
||||
if tx_size.is_ok() {
|
||||
return Ok((builder, full.clone()));
|
||||
}
|
||||
trace!(
|
||||
market_info_label = full
|
||||
.route
|
||||
.market_infos
|
||||
.first()
|
||||
.map(|v| v.label.clone())
|
||||
.unwrap_or_else(|| "no market_info".into()),
|
||||
route_label = full.first_route_label(),
|
||||
%full.input_mint,
|
||||
%full.output_mint,
|
||||
?tx_size,
|
||||
limit = ?TransactionSize::limit(),
|
||||
"full route does not fit in a tx",
|
||||
);
|
||||
|
||||
|
@ -291,47 +272,39 @@ impl Rebalancer {
|
|||
|
||||
let best = alternatives
|
||||
.iter()
|
||||
.min_by(|a, b| {
|
||||
a.route
|
||||
.price_impact_pct
|
||||
.partial_cmp(&b.route.price_impact_pct)
|
||||
.unwrap()
|
||||
})
|
||||
.min_by(|a, b| a.price_impact_pct.partial_cmp(&b.price_impact_pct).unwrap())
|
||||
.unwrap();
|
||||
let builder = self
|
||||
.mango_client
|
||||
.prepare_jupiter_swap_transaction(best.input_mint, best.output_mint, &best.route)
|
||||
.jupiter()
|
||||
.prepare_swap_transaction(best)
|
||||
.await?;
|
||||
Ok((builder, best.clone()))
|
||||
}
|
||||
|
||||
fn mango_account(&self) -> anyhow::Result<Box<MangoAccountValue>> {
|
||||
Ok(Box::new(
|
||||
self.account_fetcher
|
||||
.fetch_mango_account(&self.mango_account_address)?,
|
||||
))
|
||||
}
|
||||
|
||||
async fn rebalance_tokens(&self) -> anyhow::Result<()> {
|
||||
let account = self
|
||||
.account_fetcher
|
||||
.fetch_mango_account(&self.mango_account_address)?;
|
||||
let account = self.mango_account()?;
|
||||
|
||||
// TODO: configurable?
|
||||
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
||||
|
||||
let tokens: anyhow::Result<HashMap<TokenIndex, TokenState>> = account
|
||||
.active_token_positions()
|
||||
.map(|token_position| {
|
||||
let token = self.mango_client.context.token(token_position.token_index);
|
||||
Ok((
|
||||
token.token_index,
|
||||
TokenState::new_position(token, token_position, &self.account_fetcher)?,
|
||||
))
|
||||
})
|
||||
.try_collect();
|
||||
let tokens = tokens?;
|
||||
trace!(?tokens, "account tokens");
|
||||
|
||||
for (token_index, token_state) in tokens {
|
||||
for token_position in account.active_token_positions() {
|
||||
let token_index = token_position.token_index;
|
||||
let token = self.mango_client.context.token(token_index);
|
||||
if token_index == quote_token.token_index {
|
||||
continue;
|
||||
}
|
||||
let token_mint = token.mint_info.mint;
|
||||
let token_price = self
|
||||
.account_fetcher
|
||||
.fetch_bank_price(&token.mint_info.first_bank())?;
|
||||
|
||||
// It's not always possible to bring the native balance to 0 through swaps:
|
||||
// Consider a price <1. You need to sell a bunch of tokens to get 1 USDC native and
|
||||
|
@ -343,17 +316,27 @@ impl Rebalancer {
|
|||
// to sell them. Instead they will be withdrawn at the end.
|
||||
// Purchases will aim to purchase slightly more than is needed, such that we can
|
||||
// again withdraw the dust at the end.
|
||||
let dust_threshold = I80F48::from(2) / token_state.price;
|
||||
let dust_threshold = I80F48::from(2) / token_price;
|
||||
|
||||
let mut amount = token_state.native_position;
|
||||
// Some rebalancing can actually change non-USDC positions (rebalancing to SOL)
|
||||
// So re-fetch the current token position amount
|
||||
let bank = token_bank(token, &self.account_fetcher)?;
|
||||
let fresh_amount = || -> anyhow::Result<I80F48> {
|
||||
Ok(self
|
||||
.mango_account()?
|
||||
.token_position_and_raw_index(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO))
|
||||
};
|
||||
let mut amount = fresh_amount()?;
|
||||
|
||||
trace!(token_index, %amount, %dust_threshold, "checking");
|
||||
if amount < 0 {
|
||||
// Buy
|
||||
let buy_amount =
|
||||
amount.abs().ceil() + (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
|
||||
let input_amount = buy_amount
|
||||
* token_state.price
|
||||
* I80F48::from_num(self.config.borrow_settle_excess);
|
||||
let input_amount =
|
||||
buy_amount * token_price * I80F48::from_num(self.config.borrow_settle_excess);
|
||||
let (txsig, route) = self
|
||||
.token_swap_buy(token_mint, input_amount.to_num())
|
||||
.await?;
|
||||
|
@ -365,22 +348,15 @@ impl Rebalancer {
|
|||
info!(
|
||||
%txsig,
|
||||
"bought {} {} for {} {}",
|
||||
token.native_to_ui(I80F48::from_str(&route.route.out_amount).unwrap()),
|
||||
token.native_to_ui(I80F48::from(route.out_amount)),
|
||||
token.name,
|
||||
in_token.native_to_ui(I80F48::from_str(&route.route.in_amount).unwrap()),
|
||||
in_token.native_to_ui(I80F48::from(route.in_amount)),
|
||||
in_token.name,
|
||||
);
|
||||
if !self.refresh_mango_account_after_tx(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, &self.account_fetcher)?;
|
||||
amount = self
|
||||
.mango_client
|
||||
.mango_account()
|
||||
.await?
|
||||
.token_position_and_raw_index(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
amount = fresh_amount()?;
|
||||
}
|
||||
|
||||
if amount > dust_threshold {
|
||||
|
@ -396,27 +372,20 @@ impl Rebalancer {
|
|||
info!(
|
||||
%txsig,
|
||||
"sold {} {} for {} {}",
|
||||
token.native_to_ui(I80F48::from_str(&route.route.in_amount).unwrap()),
|
||||
token.native_to_ui(I80F48::from(route.in_amount)),
|
||||
token.name,
|
||||
out_token.native_to_ui(I80F48::from_str(&route.route.out_amount).unwrap()),
|
||||
out_token.native_to_ui(I80F48::from(route.out_amount)),
|
||||
out_token.name,
|
||||
);
|
||||
if !self.refresh_mango_account_after_tx(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, &self.account_fetcher)?;
|
||||
amount = self
|
||||
.mango_client
|
||||
.mango_account()
|
||||
.await?
|
||||
.token_position_and_raw_index(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
amount = fresh_amount()?;
|
||||
}
|
||||
|
||||
// Any remainder that could not be sold just gets withdrawn to ensure the
|
||||
// TokenPosition is freed up
|
||||
if amount > 0 && amount <= dust_threshold && !token_state.in_use {
|
||||
if amount > 0 && amount <= dust_threshold && !token_position.is_in_use() {
|
||||
let allow_borrow = false;
|
||||
let txsig = self
|
||||
.mango_client
|
||||
|
@ -598,10 +567,7 @@ impl Rebalancer {
|
|||
}
|
||||
|
||||
async fn rebalance_perps(&self) -> anyhow::Result<()> {
|
||||
let account = Box::new(
|
||||
self.account_fetcher
|
||||
.fetch_mango_account(&self.mango_account_address)?,
|
||||
);
|
||||
let account = self.mango_account()?;
|
||||
|
||||
for perp_position in account.active_perp_positions() {
|
||||
let perp = self.mango_client.context.perp(perp_position.market_index);
|
||||
|
|
|
@ -5,15 +5,13 @@ use itertools::Itertools;
|
|||
use tracing::*;
|
||||
|
||||
use mango_v4::state::TokenIndex;
|
||||
use mango_v4_client::jupiter::QueryRoute;
|
||||
use mango_v4_client::{JupiterSwapMode, MangoClient};
|
||||
|
||||
use crate::util;
|
||||
use mango_v4_client::jupiter;
|
||||
use mango_v4_client::MangoClient;
|
||||
|
||||
pub struct Config {
|
||||
pub quote_index: TokenIndex,
|
||||
pub quote_amount: u64,
|
||||
pub mock_jupiter: bool,
|
||||
pub jupiter_version: jupiter::Version,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
|
@ -60,9 +58,9 @@ impl TokenSwapInfoUpdater {
|
|||
}
|
||||
|
||||
/// oracle price is how many "in" tokens to pay for one "out" token
|
||||
fn price_over_oracle(oracle_price: f64, route: QueryRoute) -> anyhow::Result<f64> {
|
||||
let in_amount = route.in_amount.parse::<f64>()?;
|
||||
let out_amount = route.out_amount.parse::<f64>()?;
|
||||
fn price_over_oracle(oracle_price: f64, route: &jupiter::Quote) -> anyhow::Result<f64> {
|
||||
let in_amount = route.in_amount as f64;
|
||||
let out_amount = route.out_amount as f64;
|
||||
let actual_price = in_amount / out_amount;
|
||||
Ok(actual_price / oracle_price)
|
||||
}
|
||||
|
@ -97,31 +95,33 @@ impl TokenSwapInfoUpdater {
|
|||
let token_per_quote_price = quote_price / token_price;
|
||||
|
||||
let token_amount = (self.config.quote_amount as f64 * token_per_quote_price) as u64;
|
||||
let sell_route = util::jupiter_route(
|
||||
&self.mango_client,
|
||||
token_mint,
|
||||
quote_mint,
|
||||
token_amount,
|
||||
slippage,
|
||||
JupiterSwapMode::ExactIn,
|
||||
false,
|
||||
self.config.mock_jupiter,
|
||||
)
|
||||
.await?;
|
||||
let buy_route = util::jupiter_route(
|
||||
&self.mango_client,
|
||||
quote_mint,
|
||||
token_mint,
|
||||
self.config.quote_amount,
|
||||
slippage,
|
||||
JupiterSwapMode::ExactIn,
|
||||
false,
|
||||
self.config.mock_jupiter,
|
||||
)
|
||||
.await?;
|
||||
let sell_route = self
|
||||
.mango_client
|
||||
.jupiter()
|
||||
.quote(
|
||||
token_mint,
|
||||
quote_mint,
|
||||
token_amount,
|
||||
slippage,
|
||||
false,
|
||||
self.config.jupiter_version,
|
||||
)
|
||||
.await?;
|
||||
let buy_route = self
|
||||
.mango_client
|
||||
.jupiter()
|
||||
.quote(
|
||||
quote_mint,
|
||||
token_mint,
|
||||
self.config.quote_amount,
|
||||
slippage,
|
||||
false,
|
||||
self.config.jupiter_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let buy_over_oracle = Self::price_over_oracle(quote_per_token_price, buy_route)?;
|
||||
let sell_over_oracle = Self::price_over_oracle(token_per_quote_price, sell_route)?;
|
||||
let buy_over_oracle = Self::price_over_oracle(quote_per_token_price, &buy_route)?;
|
||||
let sell_over_oracle = Self::price_over_oracle(token_per_quote_price, &sell_route)?;
|
||||
|
||||
self.update(
|
||||
token_index,
|
||||
|
|
|
@ -10,7 +10,7 @@ use mango_v4::{
|
|||
i80f48::ClampToInt,
|
||||
state::{Bank, MangoAccountValue, TokenConditionalSwap, TokenIndex},
|
||||
};
|
||||
use mango_v4_client::{chain_data, health_cache, JupiterSwapMode, MangoClient, MangoGroupContext};
|
||||
use mango_v4_client::{chain_data, health_cache, jupiter, MangoClient, MangoGroupContext};
|
||||
|
||||
use solana_sdk::signature::Signature;
|
||||
use tracing::*;
|
||||
|
@ -34,7 +34,7 @@ pub struct Config {
|
|||
pub min_health_ratio: f64,
|
||||
pub max_trigger_quote_amount: u64,
|
||||
pub refresh_timeout: Duration,
|
||||
pub mock_jupiter: bool,
|
||||
pub jupiter_version: jupiter::Version,
|
||||
pub compute_limit_for_trigger: u32,
|
||||
}
|
||||
|
||||
|
@ -424,7 +424,6 @@ async fn prepare_token_conditional_swap_inner2(
|
|||
{
|
||||
let buy_mint = mango_client.context.mint_info(tcs.buy_token_index).mint;
|
||||
let sell_mint = mango_client.context.mint_info(tcs.sell_token_index).mint;
|
||||
let swap_mode = JupiterSwapMode::ExactIn;
|
||||
// The slippage does not matter since we're not going to execute it
|
||||
let slippage = 100;
|
||||
let input_amount = max_sell_token_to_liqor.min(
|
||||
|
@ -432,20 +431,20 @@ async fn prepare_token_conditional_swap_inner2(
|
|||
.floor()
|
||||
.to_num(),
|
||||
);
|
||||
let route = util::jupiter_route(
|
||||
mango_client,
|
||||
sell_mint,
|
||||
buy_mint,
|
||||
input_amount,
|
||||
slippage,
|
||||
swap_mode,
|
||||
false,
|
||||
config.mock_jupiter,
|
||||
)
|
||||
.await?;
|
||||
let route = mango_client
|
||||
.jupiter()
|
||||
.quote(
|
||||
sell_mint,
|
||||
buy_mint,
|
||||
input_amount,
|
||||
slippage,
|
||||
false,
|
||||
config.jupiter_version,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let sell_amount = route.in_amount.parse::<f64>()?;
|
||||
let buy_amount = route.out_amount.parse::<f64>()?;
|
||||
let sell_amount = route.in_amount as f64;
|
||||
let buy_amount = route.out_amount as f64;
|
||||
let swap_price = sell_amount / buy_amount;
|
||||
|
||||
if swap_price > taker_price.to_num::<f64>() {
|
||||
|
|
|
@ -8,7 +8,7 @@ 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};
|
||||
use mango_v4_client::{chain_data, MangoClient};
|
||||
|
||||
pub fn is_mango_bank<'a>(account: &'a AccountSharedData, group_id: &Pubkey) -> Option<&'a Bank> {
|
||||
let bank = account.load::<Bank>().ok()?;
|
||||
|
@ -37,73 +37,6 @@ 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 fn max_swap_source(
|
||||
client: &MangoClient,
|
||||
|
|
|
@ -11,7 +11,6 @@ use anchor_lang::{AccountDeserialize, Id};
|
|||
use anchor_spl::associated_token::get_associated_token_address;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use bincode::Options;
|
||||
use fixed::types::I80F48;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
|
@ -35,7 +34,7 @@ use solana_sdk::transaction::TransactionError;
|
|||
use crate::account_fetcher::*;
|
||||
use crate::context::MangoGroupContext;
|
||||
use crate::gpa::{fetch_anchor_account, fetch_mango_accounts};
|
||||
use crate::jupiter;
|
||||
use crate::{jupiter, util};
|
||||
|
||||
use anyhow::Context;
|
||||
use solana_sdk::account::ReadableAccount;
|
||||
|
@ -44,6 +43,8 @@ use solana_sdk::signature::{Keypair, Signature};
|
|||
use solana_sdk::sysvar;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signer::Signer};
|
||||
|
||||
pub const MAX_ACCOUNTS_PER_TRANSACTION: usize = 64;
|
||||
|
||||
// very close to anchor_client::Client, which unfortunately has no accessors or Clone
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
|
@ -1213,7 +1214,7 @@ impl MangoClient {
|
|||
.mint_info
|
||||
.banks()
|
||||
.iter()
|
||||
.map(|bank_pubkey| to_writable_account_meta(*bank_pubkey))
|
||||
.map(|bank_pubkey| util::to_writable_account_meta(*bank_pubkey))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let health_remaining_ams = self
|
||||
|
@ -1381,281 +1382,19 @@ impl MangoClient {
|
|||
|
||||
// jupiter
|
||||
|
||||
async fn http_error_handling<T: serde::de::DeserializeOwned>(
|
||||
response: reqwest::Response,
|
||||
) -> anyhow::Result<T> {
|
||||
let status = response.status();
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.context("awaiting body of http request")?;
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("http request failed, status: {status}, body: {response_text}");
|
||||
}
|
||||
serde_json::from_str::<T>(&response_text)
|
||||
.with_context(|| format!("response has unexpected format, body: {response_text}"))
|
||||
pub fn jupiter_v4(&self) -> jupiter::v4::JupiterV4 {
|
||||
jupiter::v4::JupiterV4 { mango_client: self }
|
||||
}
|
||||
|
||||
pub async fn jupiter_route(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage: u64,
|
||||
swap_mode: JupiterSwapMode,
|
||||
only_direct_routes: bool,
|
||||
) -> anyhow::Result<jupiter::QueryRoute> {
|
||||
let response = self
|
||||
.http_client
|
||||
.get("https://quote-api.jup.ag/v4/quote")
|
||||
.query(&[
|
||||
("inputMint", input_mint.to_string()),
|
||||
("outputMint", output_mint.to_string()),
|
||||
("amount", format!("{}", amount)),
|
||||
("onlyDirectRoutes", only_direct_routes.to_string()),
|
||||
("enforceSingleTx", "true".into()),
|
||||
("filterTopNResult", "10".into()),
|
||||
("slippageBps", format!("{}", slippage)),
|
||||
(
|
||||
"swapMode",
|
||||
match swap_mode {
|
||||
JupiterSwapMode::ExactIn => "ExactIn",
|
||||
JupiterSwapMode::ExactOut => "ExactOut",
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.context("quote request to jupiter")?;
|
||||
let quote: jupiter::QueryResult =
|
||||
Self::http_error_handling(response).await.with_context(|| {
|
||||
format!("error requesting jupiter route between {input_mint} and {output_mint}")
|
||||
})?;
|
||||
|
||||
let route = quote.data.first().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"no route for swap. found {} routes, but none were usable",
|
||||
quote.data.len()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(route.clone())
|
||||
pub fn jupiter_v6(&self) -> jupiter::v6::JupiterV6 {
|
||||
jupiter::v6::JupiterV6 { mango_client: self }
|
||||
}
|
||||
|
||||
/// 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,
|
||||
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 swap_response = self
|
||||
.http_client
|
||||
.post("https://quote-api.jup.ag/v4/swap")
|
||||
.json(&jupiter::SwapRequest {
|
||||
route: route.clone(),
|
||||
user_public_key: self.owner.pubkey().to_string(),
|
||||
wrap_unwrap_sol: false,
|
||||
compute_unit_price_micro_lamports: None, // we already prioritize
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("swap transaction request to jupiter")?;
|
||||
|
||||
let swap: jupiter::SwapResponse = Self::http_error_handling(swap_response)
|
||||
.await
|
||||
.context("error requesting jupiter swap")?;
|
||||
|
||||
if swap.setup_transaction.is_some() || swap.cleanup_transaction.is_some() {
|
||||
anyhow::bail!(
|
||||
"chosen jupiter route requires setup or cleanup transactions, can't execute"
|
||||
);
|
||||
}
|
||||
|
||||
let jup_tx = bincode::options()
|
||||
.with_fixint_encoding()
|
||||
.reject_trailing_bytes()
|
||||
.deserialize::<solana_sdk::transaction::VersionedTransaction>(
|
||||
&base64::decode(&swap.swap_transaction)
|
||||
.context("base64 decoding jupiter transaction")?,
|
||||
)
|
||||
.context("parsing jupiter transaction")?;
|
||||
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 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_action_ix_begin = jup_ixs
|
||||
.iter()
|
||||
.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(),
|
||||
target_token.mint_info.first_bank(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vault_ams = [
|
||||
source_token.mint_info.first_vault(),
|
||||
target_token.mint_info.first_vault(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let token_ams = [source_token.mint_info.mint, target_token.mint_info.mint]
|
||||
.into_iter()
|
||||
.map(|mint| {
|
||||
to_writable_account_meta(
|
||||
anchor_spl::associated_token::get_associated_token_address(
|
||||
&self.owner(),
|
||||
&mint,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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!
|
||||
let health_ams = self
|
||||
.derive_health_check_remaining_account_metas(
|
||||
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 mut instructions = Vec::new();
|
||||
|
||||
for ix in &jup_ixs[..jup_action_ix_begin] {
|
||||
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::FlashLoanBegin {
|
||||
account: self.mango_account_address,
|
||||
owner: self.owner(),
|
||||
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(to_readonly_account_meta(self.group()));
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::FlashLoanBegin {
|
||||
loan_amounts,
|
||||
}),
|
||||
});
|
||||
for ix in &jup_ixs[jup_action_ix_begin..jup_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_account_address,
|
||||
owner: self.owner(),
|
||||
token_program: Token::id(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
ams.extend(health_ams);
|
||||
ams.extend(vault_ams);
|
||||
ams.extend(token_ams);
|
||||
ams.push(to_readonly_account_meta(self.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 &jup_ixs[jup_action_ix_end..] {
|
||||
instructions.push(ix.clone());
|
||||
}
|
||||
|
||||
let mut address_lookup_tables = self.mango_address_lookup_tables().await?;
|
||||
address_lookup_tables.extend(jup_alts.into_iter());
|
||||
|
||||
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.clone()],
|
||||
config: self.client.transaction_builder_config,
|
||||
})
|
||||
pub fn jupiter(&self) -> jupiter::Jupiter {
|
||||
jupiter::Jupiter { mango_client: self }
|
||||
}
|
||||
|
||||
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(
|
||||
pub async fn fetch_address_lookup_table(
|
||||
&self,
|
||||
address: Pubkey,
|
||||
) -> anyhow::Result<AddressLookupTableAccount> {
|
||||
|
@ -1670,6 +1409,16 @@ impl MangoClient {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn fetch_address_lookup_tables(
|
||||
&self,
|
||||
alts: impl Iterator<Item = &Pubkey>,
|
||||
) -> anyhow::Result<Vec<AddressLookupTableAccount>> {
|
||||
stream::iter(alts)
|
||||
.then(|a| self.fetch_address_lookup_table(*a))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn mango_address_lookup_tables(
|
||||
&self,
|
||||
) -> anyhow::Result<Vec<AddressLookupTableAccount>> {
|
||||
|
@ -1679,14 +1428,13 @@ impl MangoClient {
|
|||
.await
|
||||
}
|
||||
|
||||
async fn deserialize_instructions_and_alts(
|
||||
pub(crate) async fn deserialize_instructions_and_alts(
|
||||
&self,
|
||||
message: &solana_sdk::message::VersionedMessage,
|
||||
) -> anyhow::Result<(Vec<Instruction>, Vec<AddressLookupTableAccount>)> {
|
||||
let lookups = message.address_table_lookups().unwrap_or_default();
|
||||
let address_lookup_tables = stream::iter(lookups)
|
||||
.then(|a| self.fetch_address_lookup_table(a.account_key))
|
||||
.try_collect::<Vec<_>>()
|
||||
let address_lookup_tables = self
|
||||
.fetch_address_lookup_tables(lookups.iter().map(|a| &a.account_key))
|
||||
.await?;
|
||||
|
||||
let mut account_keys = message.static_account_keys().to_vec();
|
||||
|
@ -1770,6 +1518,26 @@ pub enum MangoClientError {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TransactionSize {
|
||||
pub accounts: usize,
|
||||
pub length: usize,
|
||||
}
|
||||
|
||||
impl TransactionSize {
|
||||
pub fn is_ok(&self) -> bool {
|
||||
let limit = Self::limit();
|
||||
self.length <= limit.length && self.accounts <= limit.accounts
|
||||
}
|
||||
|
||||
pub fn limit() -> Self {
|
||||
Self {
|
||||
accounts: MAX_ACCOUNTS_PER_TRANSACTION,
|
||||
length: solana_sdk::packet::PACKET_DATA_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct TransactionBuilderConfig {
|
||||
// adds a SetComputeUnitPrice instruction in front
|
||||
|
@ -1843,10 +1611,22 @@ impl TransactionBuilder {
|
|||
.map_err(prettify_solana_client_error)
|
||||
}
|
||||
|
||||
pub fn transaction_size_ok(&self) -> anyhow::Result<bool> {
|
||||
pub fn transaction_size(&self) -> anyhow::Result<TransactionSize> {
|
||||
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)
|
||||
let accounts = tx.message.static_account_keys().len()
|
||||
+ tx.message
|
||||
.address_table_lookups()
|
||||
.map(|alts| {
|
||||
alts.iter()
|
||||
.map(|alt| alt.readonly_indexes.len() + alt.writable_indexes.len())
|
||||
.sum()
|
||||
})
|
||||
.unwrap_or(0);
|
||||
Ok(TransactionSize {
|
||||
accounts,
|
||||
length: bytes.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1907,19 +1687,3 @@ pub fn pubkey_from_cli(pubkey: &str) -> Pubkey {
|
|||
Err(_) => keypair_from_cli(pubkey).pubkey(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_readonly_account_meta(pubkey: Pubkey) -> AccountMeta {
|
||||
AccountMeta {
|
||||
pubkey,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta {
|
||||
AccountMeta {
|
||||
pubkey,
|
||||
is_writable: true,
|
||||
is_signer: false,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResult {
|
||||
pub data: Vec<QueryRoute>,
|
||||
pub time_taken: f64,
|
||||
pub context_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRoute {
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub price_impact_pct: f64,
|
||||
pub market_infos: Vec<QueryMarketInfo>,
|
||||
pub amount: String,
|
||||
pub slippage_bps: u64,
|
||||
pub other_amount_threshold: String,
|
||||
pub swap_mode: String,
|
||||
pub fees: Option<QueryRouteFees>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryMarketInfo {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub input_mint: String,
|
||||
pub output_mint: String,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub min_in_amount: Option<String>,
|
||||
pub min_out_amount: Option<String>,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
pub lp_fee: QueryFee,
|
||||
pub platform_fee: QueryFee,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryFee {
|
||||
pub amount: String,
|
||||
pub mint: String,
|
||||
pub pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRouteFees {
|
||||
pub signature_fee: f64,
|
||||
pub open_orders_deposits: Vec<f64>,
|
||||
pub ata_deposits: Vec<f64>,
|
||||
pub total_fee_and_deposits: f64,
|
||||
#[serde(rename = "minimalSOLForTransaction")]
|
||||
pub minimal_sol_for_transaction: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapRequest {
|
||||
pub route: QueryRoute,
|
||||
pub user_public_key: String,
|
||||
#[serde(rename = "wrapUnwrapSOL")]
|
||||
pub wrap_unwrap_sol: bool,
|
||||
pub compute_unit_price_micro_lamports: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapResponse {
|
||||
pub setup_transaction: Option<String>,
|
||||
pub swap_transaction: String,
|
||||
pub cleanup_transaction: Option<String>,
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
pub mod v4;
|
||||
pub mod v6;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{JupiterSwapMode, MangoClient, TransactionBuilder};
|
||||
use fixed::types::I80F48;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Version {
|
||||
Mock,
|
||||
V4,
|
||||
V6,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RawQuote {
|
||||
Mock,
|
||||
V4(v4::QueryRoute),
|
||||
V6(v6::QuoteResponse),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Quote {
|
||||
pub input_mint: Pubkey,
|
||||
pub output_mint: Pubkey,
|
||||
pub price_impact_pct: f64,
|
||||
pub in_amount: u64,
|
||||
pub out_amount: u64,
|
||||
pub raw: RawQuote,
|
||||
}
|
||||
|
||||
impl Quote {
|
||||
pub fn try_from_v4(
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
route: v4::QueryRoute,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Quote {
|
||||
input_mint,
|
||||
output_mint,
|
||||
price_impact_pct: route.price_impact_pct,
|
||||
in_amount: route.in_amount.parse()?,
|
||||
out_amount: route.out_amount.parse()?,
|
||||
raw: RawQuote::V4(route),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_from_v6(query: v6::QuoteResponse) -> anyhow::Result<Self> {
|
||||
Ok(Quote {
|
||||
input_mint: Pubkey::from_str(&query.input_mint)?,
|
||||
output_mint: Pubkey::from_str(&query.output_mint)?,
|
||||
price_impact_pct: query.price_impact_pct.parse()?,
|
||||
in_amount: query
|
||||
.in_amount
|
||||
.as_ref()
|
||||
.map(|a| a.parse())
|
||||
.unwrap_or(Ok(0))?,
|
||||
out_amount: query.out_amount.parse()?,
|
||||
raw: RawQuote::V6(query),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn first_route_label(&self) -> String {
|
||||
let label_maybe = match &self.raw {
|
||||
RawQuote::Mock => Some("mock".into()),
|
||||
RawQuote::V4(raw) => raw.market_infos.first().map(|v| v.label.clone()),
|
||||
RawQuote::V6(raw) => raw
|
||||
.route_plan
|
||||
.first()
|
||||
.and_then(|v| v.swap_info.as_ref())
|
||||
.and_then(|v| v.label.as_ref())
|
||||
.cloned(),
|
||||
};
|
||||
label_maybe.unwrap_or_else(|| "unknown".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Jupiter<'a> {
|
||||
pub mango_client: &'a MangoClient,
|
||||
}
|
||||
|
||||
impl<'a> Jupiter<'a> {
|
||||
async fn quote_mock(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
) -> anyhow::Result<Quote> {
|
||||
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;
|
||||
let input_price = self
|
||||
.mango_client
|
||||
.bank_oracle_price(input_token_index)
|
||||
.await?;
|
||||
let output_price = self
|
||||
.mango_client
|
||||
.bank_oracle_price(output_token_index)
|
||||
.await?;
|
||||
let in_amount = amount;
|
||||
let out_amount = (I80F48::from(amount) * input_price / output_price).to_num::<u64>();
|
||||
Ok(Quote {
|
||||
input_mint,
|
||||
output_mint,
|
||||
price_impact_pct: 0.0,
|
||||
in_amount,
|
||||
out_amount,
|
||||
raw: RawQuote::Mock,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn quote(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage: u64,
|
||||
only_direct_routes: bool,
|
||||
version: Version,
|
||||
) -> anyhow::Result<Quote> {
|
||||
Ok(match version {
|
||||
Version::Mock => self.quote_mock(input_mint, output_mint, amount).await?,
|
||||
Version::V4 => Quote::try_from_v4(
|
||||
input_mint,
|
||||
output_mint,
|
||||
self.mango_client
|
||||
.jupiter_v4()
|
||||
.quote(
|
||||
input_mint,
|
||||
output_mint,
|
||||
amount,
|
||||
slippage,
|
||||
JupiterSwapMode::ExactIn,
|
||||
only_direct_routes,
|
||||
)
|
||||
.await?,
|
||||
)?,
|
||||
Version::V6 => Quote::try_from_v6(
|
||||
self.mango_client
|
||||
.jupiter_v6()
|
||||
.quote(
|
||||
input_mint,
|
||||
output_mint,
|
||||
amount,
|
||||
slippage,
|
||||
only_direct_routes,
|
||||
)
|
||||
.await?,
|
||||
)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn prepare_swap_transaction(
|
||||
&self,
|
||||
quote: &Quote,
|
||||
) -> anyhow::Result<TransactionBuilder> {
|
||||
match "e.raw {
|
||||
RawQuote::Mock => anyhow::bail!("can't prepare jupiter swap for the mock"),
|
||||
RawQuote::V4(raw) => {
|
||||
self.mango_client
|
||||
.jupiter_v4()
|
||||
.prepare_swap_transaction(quote.input_mint, quote.output_mint, raw)
|
||||
.await
|
||||
}
|
||||
RawQuote::V6(raw) => {
|
||||
self.mango_client
|
||||
.jupiter_v6()
|
||||
.prepare_swap_transaction(raw)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anchor_lang::Id;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use bincode::Options;
|
||||
|
||||
use crate::{util, TransactionBuilder};
|
||||
use crate::{JupiterSwapMode, MangoClient};
|
||||
|
||||
use anyhow::Context;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_sdk::signature::Signature;
|
||||
use solana_sdk::{pubkey::Pubkey, signer::Signer};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryResult {
|
||||
pub data: Vec<QueryRoute>,
|
||||
pub time_taken: f64,
|
||||
pub context_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRoute {
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub price_impact_pct: f64,
|
||||
pub market_infos: Vec<QueryMarketInfo>,
|
||||
pub amount: String,
|
||||
pub slippage_bps: u64,
|
||||
pub other_amount_threshold: String,
|
||||
pub swap_mode: String,
|
||||
pub fees: Option<QueryRouteFees>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryMarketInfo {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
pub input_mint: String,
|
||||
pub output_mint: String,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub min_in_amount: Option<String>,
|
||||
pub min_out_amount: Option<String>,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
pub lp_fee: QueryFee,
|
||||
pub platform_fee: QueryFee,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryFee {
|
||||
pub amount: String,
|
||||
pub mint: String,
|
||||
pub pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRouteFees {
|
||||
pub signature_fee: f64,
|
||||
pub open_orders_deposits: Vec<f64>,
|
||||
pub ata_deposits: Vec<f64>,
|
||||
pub total_fee_and_deposits: f64,
|
||||
#[serde(rename = "minimalSOLForTransaction")]
|
||||
pub minimal_sol_for_transaction: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapRequest {
|
||||
pub route: QueryRoute,
|
||||
pub user_public_key: String,
|
||||
#[serde(rename = "wrapUnwrapSOL")]
|
||||
pub wrap_unwrap_sol: bool,
|
||||
pub compute_unit_price_micro_lamports: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapResponse {
|
||||
pub setup_transaction: Option<String>,
|
||||
pub swap_transaction: String,
|
||||
pub cleanup_transaction: Option<String>,
|
||||
}
|
||||
|
||||
pub struct JupiterV4<'a> {
|
||||
pub mango_client: &'a MangoClient,
|
||||
}
|
||||
|
||||
impl<'a> JupiterV4<'a> {
|
||||
pub async fn quote(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage: u64,
|
||||
swap_mode: JupiterSwapMode,
|
||||
only_direct_routes: bool,
|
||||
) -> anyhow::Result<QueryRoute> {
|
||||
let response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.get("https://quote-api.jup.ag/v4/quote")
|
||||
.query(&[
|
||||
("inputMint", input_mint.to_string()),
|
||||
("outputMint", output_mint.to_string()),
|
||||
("amount", format!("{}", amount)),
|
||||
("onlyDirectRoutes", only_direct_routes.to_string()),
|
||||
("enforceSingleTx", "true".into()),
|
||||
("filterTopNResult", "10".into()),
|
||||
("slippageBps", format!("{}", slippage)),
|
||||
(
|
||||
"swapMode",
|
||||
match swap_mode {
|
||||
JupiterSwapMode::ExactIn => "ExactIn",
|
||||
JupiterSwapMode::ExactOut => "ExactOut",
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.context("quote request to jupiter")?;
|
||||
let quote: QueryResult = util::http_error_handling(response).await.with_context(|| {
|
||||
format!("error requesting jupiter route between {input_mint} and {output_mint}")
|
||||
})?;
|
||||
|
||||
let route = quote.data.first().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"no route for swap. found {} routes, but none were usable",
|
||||
quote.data.len()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(route.clone())
|
||||
}
|
||||
|
||||
/// 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_swap_transaction(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
route: &QueryRoute,
|
||||
) -> anyhow::Result<TransactionBuilder> {
|
||||
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 swap_response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.post("https://quote-api.jup.ag/v4/swap")
|
||||
.json(&SwapRequest {
|
||||
route: route.clone(),
|
||||
user_public_key: self.mango_client.owner.pubkey().to_string(),
|
||||
wrap_unwrap_sol: false,
|
||||
compute_unit_price_micro_lamports: None, // we already prioritize
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("swap transaction request to jupiter")?;
|
||||
|
||||
let swap: SwapResponse = util::http_error_handling(swap_response)
|
||||
.await
|
||||
.context("error requesting jupiter swap")?;
|
||||
|
||||
if swap.setup_transaction.is_some() || swap.cleanup_transaction.is_some() {
|
||||
anyhow::bail!(
|
||||
"chosen jupiter route requires setup or cleanup transactions, can't execute"
|
||||
);
|
||||
}
|
||||
|
||||
let jup_tx = bincode::options()
|
||||
.with_fixint_encoding()
|
||||
.reject_trailing_bytes()
|
||||
.deserialize::<solana_sdk::transaction::VersionedTransaction>(
|
||||
&base64::decode(&swap.swap_transaction)
|
||||
.context("base64 decoding jupiter transaction")?,
|
||||
)
|
||||
.context("parsing jupiter transaction")?;
|
||||
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 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
|
||||
.mango_client
|
||||
.deserialize_instructions_and_alts(&jup_tx.message)
|
||||
.await?;
|
||||
let jup_action_ix_begin = jup_ixs
|
||||
.iter()
|
||||
.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(),
|
||||
target_token.mint_info.first_bank(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(util::to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vault_ams = [
|
||||
source_token.mint_info.first_vault(),
|
||||
target_token.mint_info.first_vault(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(util::to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let token_ams = [source_token.mint_info.mint, target_token.mint_info.mint]
|
||||
.into_iter()
|
||||
.map(|mint| {
|
||||
util::to_writable_account_meta(
|
||||
anchor_spl::associated_token::get_associated_token_address(
|
||||
&self.mango_client.owner(),
|
||||
&mint,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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!
|
||||
let health_ams = self
|
||||
.mango_client
|
||||
.derive_health_check_remaining_account_metas(
|
||||
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 mut instructions = Vec::new();
|
||||
|
||||
for ix in &jup_ixs[..jup_action_ix_begin] {
|
||||
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::FlashLoanBegin {
|
||||
account: self.mango_client.mango_account_address,
|
||||
owner: self.mango_client.owner(),
|
||||
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 &jup_ixs[jup_action_ix_begin..jup_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: self.mango_client.owner(),
|
||||
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 &jup_ixs[jup_action_ix_end..] {
|
||||
instructions.push(ix.clone());
|
||||
}
|
||||
|
||||
let mut address_lookup_tables = self.mango_client.mango_address_lookup_tables().await?;
|
||||
address_lookup_tables.extend(jup_alts.into_iter());
|
||||
|
||||
let payer = self.mango_client.owner.pubkey(); // maybe use fee_payer? but usually it's the same
|
||||
|
||||
Ok(TransactionBuilder {
|
||||
instructions,
|
||||
address_lookup_tables,
|
||||
payer,
|
||||
signers: vec![self.mango_client.owner.clone()],
|
||||
config: self.mango_client.client.transaction_builder_config,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn 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
|
||||
.quote(
|
||||
input_mint,
|
||||
output_mint,
|
||||
amount,
|
||||
slippage,
|
||||
swap_mode,
|
||||
only_direct_routes,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tx_builder = self
|
||||
.prepare_swap_transaction(input_mint, output_mint, &route)
|
||||
.await?;
|
||||
|
||||
tx_builder.send_and_confirm(&self.mango_client.client).await
|
||||
}
|
||||
}
|
|
@ -0,0 +1,399 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anchor_lang::prelude::Pubkey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use anchor_lang::Id;
|
||||
use anchor_spl::token::Token;
|
||||
|
||||
use crate::MangoClient;
|
||||
use crate::{util, TransactionBuilder};
|
||||
|
||||
use anyhow::Context;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{instruction::Instruction, signature::Signature};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QuoteResponse {
|
||||
pub input_mint: String,
|
||||
pub in_amount: Option<String>,
|
||||
pub output_mint: String,
|
||||
pub out_amount: String,
|
||||
pub other_amount_threshold: String,
|
||||
pub swap_mode: String,
|
||||
pub slippage_bps: i32,
|
||||
pub platform_fee: Option<PlatformFee>,
|
||||
pub price_impact_pct: String,
|
||||
pub route_plan: Vec<RoutePlan>,
|
||||
pub context_slot: u64,
|
||||
pub time_taken: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlatformFee {
|
||||
pub amount: String,
|
||||
pub fee_bps: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RoutePlan {
|
||||
pub percent: i32,
|
||||
pub swap_info: Option<SwapInfo>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapInfo {
|
||||
pub amm_key: String,
|
||||
pub label: Option<String>,
|
||||
pub input_mint: String,
|
||||
pub output_mint: String,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub fee_amount: String,
|
||||
pub fee_mint: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapRequest {
|
||||
pub user_public_key: String,
|
||||
pub wrap_and_unwrap_sol: bool,
|
||||
pub use_shared_accounts: bool,
|
||||
pub fee_account: Option<String>,
|
||||
pub compute_unit_price_micro_lamports: Option<u64>,
|
||||
pub as_legacy_transaction: bool,
|
||||
pub use_token_ledger: bool,
|
||||
pub destination_token_account: Option<String>,
|
||||
pub quote_response: QuoteResponse,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapResponse {
|
||||
pub swap_transaction: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapInstructionsResponse {
|
||||
pub token_ledger_instruction: Option<InstructionResponse>,
|
||||
pub compute_budget_instructions: Option<Vec<InstructionResponse>>,
|
||||
pub setup_instructions: Option<Vec<InstructionResponse>>,
|
||||
pub swap_instruction: InstructionResponse,
|
||||
pub cleanup_instructions: Option<Vec<InstructionResponse>>,
|
||||
pub address_lookup_table_addresses: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InstructionResponse {
|
||||
pub program_id: String,
|
||||
pub data: Option<String>,
|
||||
pub accounts: Option<Vec<AccountMeta>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AccountMeta {
|
||||
pub pubkey: String,
|
||||
pub is_signer: Option<bool>,
|
||||
pub is_writable: Option<bool>,
|
||||
}
|
||||
|
||||
impl TryFrom<&InstructionResponse> for solana_sdk::instruction::Instruction {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(m: &InstructionResponse) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
program_id: Pubkey::from_str(&m.program_id)?,
|
||||
data: m
|
||||
.data
|
||||
.as_ref()
|
||||
.map(|d| base64::decode(d))
|
||||
.unwrap_or(Ok(vec![]))?,
|
||||
accounts: m
|
||||
.accounts
|
||||
.as_ref()
|
||||
.map(|accs| {
|
||||
accs.iter()
|
||||
.map(|a| a.try_into())
|
||||
.collect::<anyhow::Result<Vec<solana_sdk::instruction::AccountMeta>>>()
|
||||
})
|
||||
.unwrap_or(Ok(vec![]))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&AccountMeta> for solana_sdk::instruction::AccountMeta {
|
||||
type Error = anyhow::Error;
|
||||
fn try_from(m: &AccountMeta) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
pubkey: Pubkey::from_str(&m.pubkey)?,
|
||||
is_signer: m.is_signer.unwrap_or(false),
|
||||
is_writable: m.is_writable.unwrap_or(false),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct JupiterV6<'a> {
|
||||
pub mango_client: &'a MangoClient,
|
||||
}
|
||||
|
||||
impl<'a> JupiterV6<'a> {
|
||||
pub async fn quote(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage: u64,
|
||||
only_direct_routes: bool,
|
||||
) -> anyhow::Result<QuoteResponse> {
|
||||
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 health_account_num =
|
||||
// bank and oracle
|
||||
2 * account.active_token_positions().count()
|
||||
// perp market and oracle
|
||||
+ 2 * account.active_perp_positions().count()
|
||||
// open orders account
|
||||
+ account.active_serum3_orders().count();
|
||||
// The mango instructions need the health account plus
|
||||
// mango program and group and account and instruction introspection.
|
||||
// Other accounts are shared between jupiter and mango:
|
||||
// token accounts, mints, token program, ata program, owner
|
||||
let extra_accounts = 4;
|
||||
// To produce more of a margin for error (also for the tx bytes size)
|
||||
let buffer_accounts = 6;
|
||||
let flash_loan_account_num = health_account_num + extra_accounts + buffer_accounts;
|
||||
|
||||
let response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.get("https://quote-api.jup.ag/v6/quote")
|
||||
.query(&[
|
||||
("inputMint", input_mint.to_string()),
|
||||
("outputMint", output_mint.to_string()),
|
||||
("amount", format!("{}", amount)),
|
||||
("slippageBps", format!("{}", slippage)),
|
||||
("onlyDirectRoutes", only_direct_routes.to_string()),
|
||||
(
|
||||
"maxAccounts",
|
||||
format!(
|
||||
"{}",
|
||||
crate::MAX_ACCOUNTS_PER_TRANSACTION - flash_loan_account_num
|
||||
),
|
||||
),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.context("quote request to jupiter")?;
|
||||
let quote: QuoteResponse =
|
||||
util::http_error_handling(response).await.with_context(|| {
|
||||
format!("error requesting jupiter route between {input_mint} and {output_mint}")
|
||||
})?;
|
||||
|
||||
Ok(quote)
|
||||
}
|
||||
|
||||
/// Find the instructions and account lookup tables for a jupiter swap through mango
|
||||
pub async fn prepare_swap_transaction(
|
||||
&self,
|
||||
quote: &QuoteResponse,
|
||||
) -> anyhow::Result<TransactionBuilder> {
|
||||
let input_mint = Pubkey::from_str("e.input_mint)?;
|
||||
let output_mint = Pubkey::from_str("e.output_mint)?;
|
||||
|
||||
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.mint_info.first_bank(),
|
||||
target_token.mint_info.first_bank(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(util::to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vault_ams = [
|
||||
source_token.mint_info.first_vault(),
|
||||
target_token.mint_info.first_vault(),
|
||||
]
|
||||
.into_iter()
|
||||
.map(util::to_writable_account_meta)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let token_ams = [source_token.mint_info.mint, target_token.mint_info.mint]
|
||||
.into_iter()
|
||||
.map(|mint| {
|
||||
util::to_writable_account_meta(
|
||||
anchor_spl::associated_token::get_associated_token_address(
|
||||
&self.mango_client.owner(),
|
||||
&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 = self
|
||||
.mango_client
|
||||
.derive_health_check_remaining_account_metas(
|
||||
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 swap_response = self
|
||||
.mango_client
|
||||
.http_client
|
||||
.post("https://quote-api.jup.ag/v6/swap-instructions")
|
||||
.json(&SwapRequest {
|
||||
user_public_key: self.mango_client.owner.pubkey().to_string(),
|
||||
wrap_and_unwrap_sol: false,
|
||||
use_shared_accounts: true,
|
||||
fee_account: None,
|
||||
compute_unit_price_micro_lamports: None, // we already prioritize
|
||||
as_legacy_transaction: false,
|
||||
use_token_ledger: false,
|
||||
destination_token_account: None, // default to user ata
|
||||
quote_response: quote.clone(),
|
||||
})
|
||||
.send()
|
||||
.await
|
||||
.context("swap transaction request to jupiter")?;
|
||||
|
||||
let swap: SwapInstructionsResponse = util::http_error_handling(swap_response)
|
||||
.await
|
||||
.context("error requesting jupiter swap")?;
|
||||
|
||||
let mut instructions: Vec<Instruction> = Vec::new();
|
||||
|
||||
for ix in &swap.compute_budget_instructions.unwrap_or_default() {
|
||||
instructions.push(ix.try_into()?);
|
||||
}
|
||||
for ix in &swap.setup_instructions.unwrap_or_default() {
|
||||
instructions.push(ix.try_into()?);
|
||||
}
|
||||
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: self.mango_client.owner(),
|
||||
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,
|
||||
}),
|
||||
});
|
||||
instructions.push((&swap.swap_instruction).try_into()?);
|
||||
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: self.mango_client.owner(),
|
||||
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 &swap.cleanup_instructions.unwrap_or_default() {
|
||||
instructions.push(ix.try_into()?);
|
||||
}
|
||||
|
||||
let mut address_lookup_tables = self.mango_client.mango_address_lookup_tables().await?;
|
||||
let jup_alt_addresses = swap
|
||||
.address_lookup_table_addresses
|
||||
.map(|list| {
|
||||
list.iter()
|
||||
.map(|s| Pubkey::from_str(s))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
.unwrap_or(Ok(vec![]))?;
|
||||
let jup_alts = self
|
||||
.mango_client
|
||||
.fetch_address_lookup_tables(jup_alt_addresses.iter())
|
||||
.await?;
|
||||
address_lookup_tables.extend(jup_alts.into_iter());
|
||||
|
||||
let payer = self.mango_client.owner.pubkey(); // maybe use fee_payer? but usually it's the same
|
||||
|
||||
Ok(TransactionBuilder {
|
||||
instructions,
|
||||
address_lookup_tables,
|
||||
payer,
|
||||
signers: vec![self.mango_client.owner.clone()],
|
||||
config: self.mango_client.client.transaction_builder_config,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn swap(
|
||||
&self,
|
||||
input_mint: Pubkey,
|
||||
output_mint: Pubkey,
|
||||
amount: u64,
|
||||
slippage: u64,
|
||||
only_direct_routes: bool,
|
||||
) -> anyhow::Result<Signature> {
|
||||
let route = self
|
||||
.quote(
|
||||
input_mint,
|
||||
output_mint,
|
||||
amount,
|
||||
slippage,
|
||||
only_direct_routes,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let tx_builder = self.prepare_swap_transaction(&route).await?;
|
||||
|
||||
tx_builder.send_and_confirm(&self.mango_client.client).await
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ use solana_sdk::{
|
|||
transaction::uses_durable_nonce,
|
||||
};
|
||||
|
||||
use anchor_lang::prelude::{AccountMeta, Pubkey};
|
||||
use anyhow::Context;
|
||||
use std::{thread, time};
|
||||
|
||||
/// Some Result<> types don't convert to anyhow::Result nicely. Force them through stringification.
|
||||
|
@ -110,3 +112,34 @@ pub fn tracing_subscriber_init() {
|
|||
.event_format(format)
|
||||
.init();
|
||||
}
|
||||
|
||||
pub async fn http_error_handling<T: serde::de::DeserializeOwned>(
|
||||
response: reqwest::Response,
|
||||
) -> anyhow::Result<T> {
|
||||
let status = response.status();
|
||||
let response_text = response
|
||||
.text()
|
||||
.await
|
||||
.context("awaiting body of http request")?;
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("http request failed, status: {status}, body: {response_text}");
|
||||
}
|
||||
serde_json::from_str::<T>(&response_text)
|
||||
.with_context(|| format!("response has unexpected format, body: {response_text}"))
|
||||
}
|
||||
|
||||
pub fn to_readonly_account_meta(pubkey: Pubkey) -> AccountMeta {
|
||||
AccountMeta {
|
||||
pubkey,
|
||||
is_writable: false,
|
||||
is_signer: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta {
|
||||
AccountMeta {
|
||||
pubkey,
|
||||
is_writable: true,
|
||||
is_signer: false,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue