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:
Christian Kamm 2023-08-24 16:45:01 +02:00 committed by GitHub
parent 6e2363c86f
commit 0f10cb4d92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1233 additions and 725 deletions

View File

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

View File

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

View File

@ -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,

View File

@ -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 {

View File

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

View File

@ -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,

View File

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

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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>,
}

View File

@ -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 &quote.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
}
}
}
}

View File

@ -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
}
}

View File

@ -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(&quote.input_mint)?;
let output_mint = Pubkey::from_str(&quote.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
}
}

View File

@ -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,
}
}