parent
653cf9f30b
commit
01d5237162
|
@ -3462,6 +3462,7 @@ dependencies = [
|
||||||
"atty",
|
"atty",
|
||||||
"base64 0.13.1",
|
"base64 0.13.1",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
"borsh 0.10.3",
|
||||||
"clap 3.2.25",
|
"clap 3.2.25",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
|
"fixed 1.11.0 (git+https://github.com/blockworks-foundation/fixed.git?branch=v1.11.0-borsh0_10-mango)",
|
||||||
|
@ -3529,6 +3530,7 @@ dependencies = [
|
||||||
"async-channel",
|
"async-channel",
|
||||||
"async-stream 0.2.1",
|
"async-stream 0.2.1",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"borsh 0.10.3",
|
||||||
"bs58 0.3.1",
|
"bs58 0.3.1",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"bytes 1.5.0",
|
"bytes 1.5.0",
|
||||||
|
@ -3557,6 +3559,7 @@ dependencies = [
|
||||||
"serum_dex 0.5.10 (git+https://github.com/openbook-dex/program.git)",
|
"serum_dex 0.5.10 (git+https://github.com/openbook-dex/program.git)",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"solana-account-decoder",
|
"solana-account-decoder",
|
||||||
|
"solana-address-lookup-table-program",
|
||||||
"solana-client",
|
"solana-client",
|
||||||
"solana-logger",
|
"solana-logger",
|
||||||
"solana-rpc",
|
"solana-rpc",
|
||||||
|
|
|
@ -92,6 +92,31 @@ struct JupiterSwap {
|
||||||
rpc: Rpc,
|
rpc: Rpc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug, Clone)]
|
||||||
|
struct SanctumSwap {
|
||||||
|
#[clap(long)]
|
||||||
|
account: String,
|
||||||
|
|
||||||
|
/// also pays for everything
|
||||||
|
#[clap(short, long)]
|
||||||
|
owner: String,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
input_mint: String,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
output_mint: String,
|
||||||
|
|
||||||
|
#[clap(short, long)]
|
||||||
|
amount: u64,
|
||||||
|
|
||||||
|
#[clap(short, long, default_value = "50")]
|
||||||
|
max_slippage_bps: u64,
|
||||||
|
|
||||||
|
#[clap(flatten)]
|
||||||
|
rpc: Rpc,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(ArgEnum, Clone, Debug)]
|
#[derive(ArgEnum, Clone, Debug)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
pub enum CliSide {
|
pub enum CliSide {
|
||||||
|
@ -189,6 +214,7 @@ enum Command {
|
||||||
CreateAccount(CreateAccount),
|
CreateAccount(CreateAccount),
|
||||||
Deposit(Deposit),
|
Deposit(Deposit),
|
||||||
JupiterSwap(JupiterSwap),
|
JupiterSwap(JupiterSwap),
|
||||||
|
SanctumSwap(SanctumSwap),
|
||||||
GroupAddress {
|
GroupAddress {
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
creator: String,
|
creator: String,
|
||||||
|
@ -312,6 +338,19 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||||
.await?;
|
.await?;
|
||||||
println!("{}", txsig);
|
println!("{}", txsig);
|
||||||
}
|
}
|
||||||
|
Command::SanctumSwap(cmd) => {
|
||||||
|
let client = cmd.rpc.client(Some(&cmd.owner))?;
|
||||||
|
let account = pubkey_from_cli(&cmd.account);
|
||||||
|
let owner = Arc::new(keypair_from_cli(&cmd.owner));
|
||||||
|
let input_mint = pubkey_from_cli(&cmd.input_mint);
|
||||||
|
let output_mint = pubkey_from_cli(&cmd.output_mint);
|
||||||
|
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||||
|
let txsig = client
|
||||||
|
.sanctum()
|
||||||
|
.swap(input_mint, output_mint, cmd.max_slippage_bps, cmd.amount)
|
||||||
|
.await?;
|
||||||
|
println!("{}", txsig);
|
||||||
|
}
|
||||||
Command::GroupAddress { creator, num } => {
|
Command::GroupAddress { creator, num } => {
|
||||||
let creator = pubkey_from_cli(&creator);
|
let creator = pubkey_from_cli(&creator);
|
||||||
println!("{}", MangoClient::group_for_admin(creator, num));
|
println!("{}", MangoClient::group_for_admin(creator, num));
|
||||||
|
|
|
@ -42,6 +42,7 @@ shellexpand = "2.1.0"
|
||||||
solana-account-decoder = { workspace = true }
|
solana-account-decoder = { workspace = true }
|
||||||
solana-client = { workspace = true }
|
solana-client = { workspace = true }
|
||||||
solana-logger = { workspace = true }
|
solana-logger = { workspace = true }
|
||||||
|
solana-address-lookup-table-program = "~1.16.7"
|
||||||
solana-rpc = { workspace = true }
|
solana-rpc = { workspace = true }
|
||||||
solana-sdk = { workspace = true }
|
solana-sdk = { workspace = true }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
@ -50,4 +51,5 @@ tokio-tungstenite = "0.16.1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
regex = "1.9.5"
|
regex = "1.9.5"
|
||||||
hdrhistogram = "7.5.4"
|
hdrhistogram = "7.5.4"
|
||||||
indexmap = "2.0.0"
|
indexmap = "2.0.0"
|
||||||
|
borsh = { version = "0.10.3", features = ["const-generics"] }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::trigger_tcs;
|
use crate::trigger_tcs;
|
||||||
use anchor_lang::prelude::Pubkey;
|
use anchor_lang::prelude::Pubkey;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use mango_v4_client::{jupiter, priority_fees_cli};
|
use mango_v4_client::{priority_fees_cli, swap};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
|
@ -28,11 +28,11 @@ pub(crate) enum JupiterVersionArg {
|
||||||
V6,
|
V6,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<JupiterVersionArg> for jupiter::Version {
|
impl From<JupiterVersionArg> for swap::Version {
|
||||||
fn from(a: JupiterVersionArg) -> Self {
|
fn from(a: JupiterVersionArg) -> Self {
|
||||||
match a {
|
match a {
|
||||||
JupiterVersionArg::Mock => jupiter::Version::Mock,
|
JupiterVersionArg::Mock => swap::Version::Mock,
|
||||||
JupiterVersionArg::V6 => jupiter::Version::V6,
|
JupiterVersionArg::V6 => swap::Version::V6,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,12 @@ pub struct Cli {
|
||||||
#[clap(long, env, value_parser, value_delimiter = ',')]
|
#[clap(long, env, value_parser, value_delimiter = ',')]
|
||||||
pub(crate) rebalance_alternate_jupiter_route_tokens: Option<Vec<u16>>,
|
pub(crate) rebalance_alternate_jupiter_route_tokens: Option<Vec<u16>>,
|
||||||
|
|
||||||
|
/// query sanctum for routes to and from these tokens
|
||||||
|
///
|
||||||
|
/// These routes will only be used when trying to rebalance a LST token
|
||||||
|
#[clap(long, env, value_parser, value_delimiter = ',')]
|
||||||
|
pub(crate) rebalance_alternate_sanctum_route_tokens: Option<Vec<u16>>,
|
||||||
|
|
||||||
/// When closing borrows, the rebalancer can't close token positions exactly.
|
/// When closing borrows, the rebalancer can't close token positions exactly.
|
||||||
/// Instead it purchases too much and then gets rid of the excess in a second step.
|
/// Instead it purchases too much and then gets rid of the excess in a second step.
|
||||||
/// If this is 0.05, then it'll swap borrow_value * (1 + 0.05) quote token into borrow token.
|
/// If this is 0.05, then it'll swap borrow_value * (1 + 0.05) quote token into borrow token.
|
||||||
|
@ -236,4 +242,16 @@ pub struct Cli {
|
||||||
/// max number of liquidation/tcs to do concurrently
|
/// max number of liquidation/tcs to do concurrently
|
||||||
#[clap(long, env, default_value = "5")]
|
#[clap(long, env, default_value = "5")]
|
||||||
pub(crate) max_parallel_operations: u64,
|
pub(crate) max_parallel_operations: u64,
|
||||||
|
|
||||||
|
/// Also use sanctum for rebalancing
|
||||||
|
#[clap(long, env, value_enum, default_value = "false")]
|
||||||
|
pub(crate) sanctum_enabled: BoolArg,
|
||||||
|
|
||||||
|
/// override the url to sanctum
|
||||||
|
#[clap(long, env, default_value = "https://api.sanctum.so/v1")]
|
||||||
|
pub(crate) sanctum_url: String,
|
||||||
|
|
||||||
|
/// override the sanctum http request timeout
|
||||||
|
#[clap(long, env, default_value = "30")]
|
||||||
|
pub(crate) sanctum_timeout_secs: u64,
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.jupiter_timeout(Duration::from_secs(cli.jupiter_timeout_secs))
|
.jupiter_timeout(Duration::from_secs(cli.jupiter_timeout_secs))
|
||||||
.jupiter_v6_url(cli.jupiter_v6_url.clone())
|
.jupiter_v6_url(cli.jupiter_v6_url.clone())
|
||||||
.jupiter_token(cli.jupiter_token.clone())
|
.jupiter_token(cli.jupiter_token.clone())
|
||||||
|
.sanctum_url(cli.sanctum_url.clone())
|
||||||
|
.sanctum_timeout(Duration::from_secs(cli.sanctum_timeout_secs))
|
||||||
.transaction_builder_config(
|
.transaction_builder_config(
|
||||||
TransactionBuilderConfig::builder()
|
TransactionBuilderConfig::builder()
|
||||||
.priority_fee_provider(prio_provider)
|
.priority_fee_provider(prio_provider)
|
||||||
|
@ -257,16 +259,26 @@ async fn main() -> anyhow::Result<()> {
|
||||||
.rebalance_alternate_jupiter_route_tokens
|
.rebalance_alternate_jupiter_route_tokens
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
alternate_sanctum_route_tokens: cli
|
||||||
|
.rebalance_alternate_sanctum_route_tokens
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default(),
|
||||||
allow_withdraws: signer_is_owner,
|
allow_withdraws: signer_is_owner,
|
||||||
|
use_sanctum: cli.sanctum_enabled == BoolArg::True,
|
||||||
};
|
};
|
||||||
rebalance_config.validate(&mango_client.context);
|
rebalance_config.validate(&mango_client.context);
|
||||||
|
|
||||||
let rebalancer = Arc::new(rebalance::Rebalancer {
|
let mut rebalancer = rebalance::Rebalancer {
|
||||||
mango_client: mango_client.clone(),
|
mango_client: mango_client.clone(),
|
||||||
account_fetcher: account_fetcher.clone(),
|
account_fetcher: account_fetcher.clone(),
|
||||||
mango_account_address: cli.liqor_mango_account,
|
mango_account_address: cli.liqor_mango_account,
|
||||||
config: rebalance_config,
|
config: rebalance_config,
|
||||||
});
|
sanctum_supported_mints: HashSet::<Pubkey>::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let live_rpc_client = mango_client.client.new_rpc_async();
|
||||||
|
rebalancer.init(&live_rpc_client).await;
|
||||||
|
let rebalancer = Arc::new(rebalancer);
|
||||||
|
|
||||||
let liquidation = Box::new(LiquidationState {
|
let liquidation = Box::new(LiquidationState {
|
||||||
mango_client: mango_client.clone(),
|
mango_client: mango_client.clone(),
|
||||||
|
@ -407,7 +419,7 @@ async fn main() -> anyhow::Result<()> {
|
||||||
// But need to take care to abort if the above job aborts beforehand.
|
// But need to take care to abort if the above job aborts beforehand.
|
||||||
if cli.rebalance == BoolArg::True {
|
if cli.rebalance == BoolArg::True {
|
||||||
let rebalance_job =
|
let rebalance_job =
|
||||||
spawn_rebalance_job(&shared_state, rebalance_trigger_receiver, rebalancer);
|
spawn_rebalance_job(shared_state.clone(), rebalance_trigger_receiver, rebalancer);
|
||||||
optional_jobs.push(rebalance_job);
|
optional_jobs.push(rebalance_job);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -523,14 +535,13 @@ fn spawn_telemetry_job(cli: &Cli, mango_client: Arc<MangoClient>) -> JoinHandle<
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_rebalance_job(
|
fn spawn_rebalance_job(
|
||||||
shared_state: &Arc<RwLock<SharedState>>,
|
shared_state: Arc<RwLock<SharedState>>,
|
||||||
rebalance_trigger_receiver: async_channel::Receiver<()>,
|
rebalance_trigger_receiver: async_channel::Receiver<()>,
|
||||||
rebalancer: Arc<Rebalancer>,
|
rebalancer: Arc<Rebalancer>,
|
||||||
) -> JoinHandle<()> {
|
) -> JoinHandle<()> {
|
||||||
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(30));
|
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(30));
|
||||||
|
|
||||||
tokio::spawn({
|
tokio::spawn({
|
||||||
let shared_state = shared_state.clone();
|
|
||||||
async move {
|
async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
|
|
@ -5,13 +5,16 @@ use mango_v4::state::{
|
||||||
PlaceOrderType, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
PlaceOrderType, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||||
};
|
};
|
||||||
use mango_v4_client::{
|
use mango_v4_client::{
|
||||||
chain_data, jupiter, perp_pnl, MangoClient, MangoGroupContext, PerpMarketContext, TokenContext,
|
chain_data, perp_pnl, swap, MangoClient, MangoGroupContext, PerpMarketContext, TokenContext,
|
||||||
TransactionBuilder, TransactionSize,
|
TransactionBuilder, TransactionSize,
|
||||||
};
|
};
|
||||||
|
use solana_client::nonblocking::rpc_client::RpcClient;
|
||||||
|
|
||||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||||
|
|
||||||
use solana_sdk::signature::Signature;
|
use solana_sdk::signature::Signature;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
@ -26,10 +29,12 @@ pub struct Config {
|
||||||
/// If this is 1.05, then it'll swap borrow_value * 1.05 quote token into borrow token.
|
/// If this is 1.05, then it'll swap borrow_value * 1.05 quote token into borrow token.
|
||||||
pub borrow_settle_excess: f64,
|
pub borrow_settle_excess: f64,
|
||||||
pub refresh_timeout: Duration,
|
pub refresh_timeout: Duration,
|
||||||
pub jupiter_version: jupiter::Version,
|
pub jupiter_version: swap::Version,
|
||||||
pub skip_tokens: Vec<TokenIndex>,
|
pub skip_tokens: Vec<TokenIndex>,
|
||||||
pub alternate_jupiter_route_tokens: Vec<TokenIndex>,
|
pub alternate_jupiter_route_tokens: Vec<TokenIndex>,
|
||||||
|
pub alternate_sanctum_route_tokens: Vec<TokenIndex>,
|
||||||
pub allow_withdraws: bool,
|
pub allow_withdraws: bool,
|
||||||
|
pub use_sanctum: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
@ -56,6 +61,7 @@ pub struct Rebalancer {
|
||||||
pub account_fetcher: Arc<chain_data::AccountFetcher>,
|
pub account_fetcher: Arc<chain_data::AccountFetcher>,
|
||||||
pub mango_account_address: Pubkey,
|
pub mango_account_address: Pubkey,
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
|
pub sanctum_supported_mints: HashSet<Pubkey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rebalancer {
|
impl Rebalancer {
|
||||||
|
@ -105,16 +111,16 @@ impl Rebalancer {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn jupiter_quote(
|
async fn swap_quote(
|
||||||
&self,
|
&self,
|
||||||
input_mint: Pubkey,
|
input_mint: Pubkey,
|
||||||
output_mint: Pubkey,
|
output_mint: Pubkey,
|
||||||
amount: u64,
|
amount: u64,
|
||||||
only_direct_routes: bool,
|
only_direct_routes: bool,
|
||||||
jupiter_version: jupiter::Version,
|
jupiter_version: swap::Version,
|
||||||
) -> anyhow::Result<jupiter::Quote> {
|
) -> anyhow::Result<swap::Quote> {
|
||||||
self.mango_client
|
self.mango_client
|
||||||
.jupiter()
|
.swap()
|
||||||
.quote(
|
.quote(
|
||||||
input_mint,
|
input_mint,
|
||||||
output_mint,
|
output_mint,
|
||||||
|
@ -126,29 +132,31 @@ impl Rebalancer {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Grab three possible routes:
|
/// Grab multiples possible routes:
|
||||||
/// 1. USDC -> output (complex routes)
|
/// 1. USDC -> output (complex routes)
|
||||||
/// 2. USDC -> output (direct route only)
|
/// 2. USDC -> output (direct route only)
|
||||||
/// 3. alternate_jupiter_route_tokens -> output (direct route only)
|
/// 3. if enabled, sanctum routes - might generate 0, 1 or more routes
|
||||||
/// Use 1. if it fits into a tx. Otherwise use the better of 2./3.
|
/// 4. input -> alternate_jupiter_route_tokens (direct route only) - might generate 0, 1 or more routes
|
||||||
|
/// Use best of 1/2/3. if it fits into a tx,
|
||||||
|
/// Otherwise use the best of 4.
|
||||||
async fn token_swap_buy(
|
async fn token_swap_buy(
|
||||||
&self,
|
&self,
|
||||||
account: &MangoAccountValue,
|
account: &MangoAccountValue,
|
||||||
output_mint: Pubkey,
|
output_mint: Pubkey,
|
||||||
in_amount_quote: u64,
|
in_amount_quote: u64,
|
||||||
) -> anyhow::Result<(Signature, jupiter::Quote)> {
|
) -> anyhow::Result<(Signature, swap::Quote)> {
|
||||||
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
||||||
let quote_mint = quote_token.mint;
|
let quote_mint = quote_token.mint;
|
||||||
let jupiter_version = self.config.jupiter_version;
|
let jupiter_version = self.config.jupiter_version;
|
||||||
|
|
||||||
let full_route_job = self.jupiter_quote(
|
let full_route_job = self.swap_quote(
|
||||||
quote_mint,
|
quote_mint,
|
||||||
output_mint,
|
output_mint,
|
||||||
in_amount_quote,
|
in_amount_quote,
|
||||||
false,
|
false,
|
||||||
jupiter_version,
|
jupiter_version,
|
||||||
);
|
);
|
||||||
let direct_quote_route_job = self.jupiter_quote(
|
let direct_quote_route_job = self.swap_quote(
|
||||||
quote_mint,
|
quote_mint,
|
||||||
output_mint,
|
output_mint,
|
||||||
in_amount_quote,
|
in_amount_quote,
|
||||||
|
@ -157,31 +165,52 @@ impl Rebalancer {
|
||||||
);
|
);
|
||||||
let mut jobs = vec![full_route_job, direct_quote_route_job];
|
let mut jobs = vec![full_route_job, direct_quote_route_job];
|
||||||
|
|
||||||
for in_token_index in &self.config.alternate_jupiter_route_tokens {
|
if self.can_use_sanctum_for_token(output_mint)? {
|
||||||
let in_token = self.mango_client.context.token(*in_token_index);
|
for in_token_index in &self.config.alternate_sanctum_route_tokens {
|
||||||
// For the alternate output routes we need to adjust the in amount by the token price
|
let (alt_mint, alt_in_amount) =
|
||||||
let in_price = self
|
self.get_alternative_token_amount(in_token_index, in_amount_quote)?;
|
||||||
.account_fetcher
|
let sanctum_alt_route_job = self.swap_quote(
|
||||||
.fetch_bank_price(&in_token.first_bank())?;
|
alt_mint,
|
||||||
let in_amount = (I80F48::from(in_amount_quote) / in_price)
|
output_mint,
|
||||||
.ceil()
|
alt_in_amount,
|
||||||
.to_num::<u64>();
|
false,
|
||||||
let direct_route_job =
|
swap::Version::Sanctum,
|
||||||
self.jupiter_quote(in_token.mint, output_mint, in_amount, true, jupiter_version);
|
);
|
||||||
jobs.push(direct_route_job);
|
jobs.push(sanctum_alt_route_job);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut results = futures::future::join_all(jobs).await;
|
let results = futures::future::join_all(jobs).await;
|
||||||
let full_route = results.remove(0)?;
|
let routes: Vec<_> = results.into_iter().filter_map(|v| v.ok()).collect_vec();
|
||||||
let alternatives = results.into_iter().filter_map(|v| v.ok()).collect_vec();
|
|
||||||
|
|
||||||
let (mut tx_builder, route) = self
|
let best_route_res = self
|
||||||
.determine_best_jupiter_tx(
|
.determine_best_swap_tx(routes, quote_mint, output_mint)
|
||||||
// If the best_route couldn't be fetched, something is wrong
|
.await;
|
||||||
&full_route,
|
|
||||||
&alternatives,
|
let (mut tx_builder, route) = match best_route_res {
|
||||||
)
|
Ok(x) => x,
|
||||||
.await?;
|
Err(e) => {
|
||||||
|
warn!("could not use simple routes because of {}, trying with an alternative one (if configured)", e);
|
||||||
|
|
||||||
|
self.get_jupiter_alt_route(
|
||||||
|
|in_token_index| {
|
||||||
|
let (alt_mint, alt_in_amount) =
|
||||||
|
self.get_alternative_token_amount(in_token_index, in_amount_quote)?;
|
||||||
|
let swap = self.swap_quote(
|
||||||
|
alt_mint,
|
||||||
|
output_mint,
|
||||||
|
alt_in_amount,
|
||||||
|
true,
|
||||||
|
jupiter_version,
|
||||||
|
);
|
||||||
|
Ok(swap)
|
||||||
|
},
|
||||||
|
quote_mint,
|
||||||
|
output_mint,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let seq_check_ix = self
|
let seq_check_ix = self
|
||||||
.mango_client
|
.mango_client
|
||||||
|
@ -195,45 +224,72 @@ impl Rebalancer {
|
||||||
Ok((sig, route))
|
Ok((sig, route))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Grab three possible routes:
|
/// Grab multiples possible routes:
|
||||||
/// 1. input -> USDC (complex routes)
|
/// 1. input -> USDC (complex routes)
|
||||||
/// 2. input -> USDC (direct route only)
|
/// 2. input -> USDC (direct route only)
|
||||||
/// 3. input -> alternate_jupiter_route_tokens (direct route only)
|
/// 3. if enabled, sanctum routes - might generate 0, 1 or more routes
|
||||||
/// Use 1. if it fits into a tx. Otherwise use the better of 2./3.
|
/// 4. input -> alternate_jupiter_route_tokens (direct route only) - might generate 0, 1 or more routes
|
||||||
|
/// Use best of 1/2/3. if it fits into a tx,
|
||||||
|
/// Otherwise use the best of 4.
|
||||||
async fn token_swap_sell(
|
async fn token_swap_sell(
|
||||||
&self,
|
&self,
|
||||||
account: &MangoAccountValue,
|
account: &MangoAccountValue,
|
||||||
input_mint: Pubkey,
|
input_mint: Pubkey,
|
||||||
in_amount: u64,
|
in_amount: u64,
|
||||||
) -> anyhow::Result<(Signature, jupiter::Quote)> {
|
) -> anyhow::Result<(Signature, swap::Quote)> {
|
||||||
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
let quote_token = self.mango_client.context.token(QUOTE_TOKEN_INDEX);
|
||||||
let quote_mint = quote_token.mint;
|
let quote_mint = quote_token.mint;
|
||||||
let jupiter_version = self.config.jupiter_version;
|
let jupiter_version = self.config.jupiter_version;
|
||||||
|
|
||||||
let full_route_job =
|
let full_route_job =
|
||||||
self.jupiter_quote(input_mint, quote_mint, in_amount, false, jupiter_version);
|
self.swap_quote(input_mint, quote_mint, in_amount, false, jupiter_version);
|
||||||
let direct_quote_route_job =
|
let direct_quote_route_job =
|
||||||
self.jupiter_quote(input_mint, quote_mint, in_amount, true, jupiter_version);
|
self.swap_quote(input_mint, quote_mint, in_amount, true, jupiter_version);
|
||||||
let mut jobs = vec![full_route_job, direct_quote_route_job];
|
let mut jobs = vec![full_route_job, direct_quote_route_job];
|
||||||
|
|
||||||
for out_token_index in &self.config.alternate_jupiter_route_tokens {
|
if self.can_use_sanctum_for_token(input_mint)? {
|
||||||
let out_token = self.mango_client.context.token(*out_token_index);
|
for out_token_index in &self.config.alternate_sanctum_route_tokens {
|
||||||
let direct_route_job =
|
let out_token = self.mango_client.context.token(*out_token_index);
|
||||||
self.jupiter_quote(input_mint, out_token.mint, in_amount, true, jupiter_version);
|
let sanctum_job = self.swap_quote(
|
||||||
jobs.push(direct_route_job);
|
input_mint,
|
||||||
|
out_token.mint,
|
||||||
|
in_amount,
|
||||||
|
false,
|
||||||
|
swap::Version::Sanctum,
|
||||||
|
);
|
||||||
|
jobs.push(sanctum_job);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut results = futures::future::join_all(jobs).await;
|
let results = futures::future::join_all(jobs).await;
|
||||||
let full_route = results.remove(0)?;
|
let routes: Vec<_> = results.into_iter().filter_map(|v| v.ok()).collect_vec();
|
||||||
let alternatives = results.into_iter().filter_map(|v| v.ok()).collect_vec();
|
|
||||||
|
|
||||||
let (mut tx_builder, route) = self
|
let best_route_res = self
|
||||||
.determine_best_jupiter_tx(
|
.determine_best_swap_tx(routes, input_mint, quote_mint)
|
||||||
// If the best_route couldn't be fetched, something is wrong
|
.await;
|
||||||
&full_route,
|
|
||||||
&alternatives,
|
let (mut tx_builder, route) = match best_route_res {
|
||||||
)
|
Ok(x) => x,
|
||||||
.await?;
|
Err(e) => {
|
||||||
|
warn!("could not use simple routes because of {}, trying with an alternative one (if configured)", e);
|
||||||
|
|
||||||
|
self.get_jupiter_alt_route(
|
||||||
|
|out_token_index| {
|
||||||
|
let out_token = self.mango_client.context.token(*out_token_index);
|
||||||
|
Ok(self.swap_quote(
|
||||||
|
input_mint,
|
||||||
|
out_token.mint,
|
||||||
|
in_amount,
|
||||||
|
true,
|
||||||
|
jupiter_version,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
input_mint,
|
||||||
|
quote_mint,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let seq_check_ix = self
|
let seq_check_ix = self
|
||||||
.mango_client
|
.mango_client
|
||||||
|
@ -247,47 +303,133 @@ impl Rebalancer {
|
||||||
Ok((sig, route))
|
Ok((sig, route))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn determine_best_jupiter_tx(
|
fn get_alternative_token_amount(
|
||||||
&self,
|
&self,
|
||||||
full: &jupiter::Quote,
|
in_token_index: &u16,
|
||||||
alternatives: &[jupiter::Quote],
|
in_amount_quote: u64,
|
||||||
) -> anyhow::Result<(TransactionBuilder, jupiter::Quote)> {
|
) -> anyhow::Result<(Pubkey, u64)> {
|
||||||
let builder = self
|
let in_token: &TokenContext = self.mango_client.context.token(*in_token_index);
|
||||||
.mango_client
|
let in_price = self
|
||||||
.jupiter()
|
.account_fetcher
|
||||||
.prepare_swap_transaction(full)
|
.fetch_bank_price(&in_token.first_bank())?;
|
||||||
.await?;
|
let in_amount = (I80F48::from(in_amount_quote) / in_price)
|
||||||
let tx_size = builder.transaction_size()?;
|
.ceil()
|
||||||
if tx_size.is_within_limit() {
|
.to_num::<u64>();
|
||||||
return Ok((builder, full.clone()));
|
|
||||||
}
|
|
||||||
trace!(
|
|
||||||
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",
|
|
||||||
);
|
|
||||||
|
|
||||||
if alternatives.is_empty() {
|
Ok((in_token.mint, in_amount))
|
||||||
anyhow::bail!(
|
}
|
||||||
"no alternative routes from {} to {}",
|
|
||||||
full.input_mint,
|
fn can_use_sanctum_for_token(&self, mint: Pubkey) -> anyhow::Result<bool> {
|
||||||
full.output_mint
|
if !self.config.use_sanctum {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = self.mango_client.context.token_by_mint(&mint)?;
|
||||||
|
|
||||||
|
let can_swap_on_sanctum = self.can_swap_on_sanctum(mint);
|
||||||
|
|
||||||
|
// forbid swapping to something that could be used in another sanctum swap, creating a cycle
|
||||||
|
let is_an_alt_for_sanctum = self
|
||||||
|
.config
|
||||||
|
.alternate_sanctum_route_tokens
|
||||||
|
.contains(&token.token_index);
|
||||||
|
|
||||||
|
Ok(can_swap_on_sanctum && !is_an_alt_for_sanctum)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_jupiter_alt_route<T: Future<Output = anyhow::Result<swap::Quote>>>(
|
||||||
|
&self,
|
||||||
|
quote_fetcher: impl Fn(&u16) -> anyhow::Result<T>,
|
||||||
|
original_input_mint: Pubkey,
|
||||||
|
original_output_mint: Pubkey,
|
||||||
|
) -> anyhow::Result<(TransactionBuilder, swap::Quote)> {
|
||||||
|
let mut alt_jobs = vec![];
|
||||||
|
for in_token_index in &self.config.alternate_jupiter_route_tokens {
|
||||||
|
alt_jobs.push(quote_fetcher(in_token_index)?);
|
||||||
|
}
|
||||||
|
let alt_results = futures::future::join_all(alt_jobs).await;
|
||||||
|
let alt_routes: Vec<_> = alt_results.into_iter().filter_map(|v| v.ok()).collect_vec();
|
||||||
|
|
||||||
|
let best_route = self
|
||||||
|
.determine_best_swap_tx(alt_routes, original_input_mint, original_output_mint)
|
||||||
|
.await?;
|
||||||
|
Ok(best_route)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn determine_best_swap_tx(
|
||||||
|
&self,
|
||||||
|
mut routes: Vec<swap::Quote>,
|
||||||
|
original_input_mint: Pubkey,
|
||||||
|
original_output_mint: Pubkey,
|
||||||
|
) -> anyhow::Result<(TransactionBuilder, swap::Quote)> {
|
||||||
|
let mut prices = HashMap::<Pubkey, I80F48>::new();
|
||||||
|
let mut get_or_fetch_price = |m| {
|
||||||
|
let entry = prices.entry(m).or_insert_with(|| {
|
||||||
|
let token = self
|
||||||
|
.mango_client
|
||||||
|
.context
|
||||||
|
.token_by_mint(&m)
|
||||||
|
.expect("token for mint not found");
|
||||||
|
self.account_fetcher
|
||||||
|
.fetch_bank_price(&token.first_bank())
|
||||||
|
.expect("failed to fetch price")
|
||||||
|
});
|
||||||
|
*entry
|
||||||
|
};
|
||||||
|
|
||||||
|
routes.sort_by_cached_key(|r| {
|
||||||
|
let in_price = get_or_fetch_price(r.input_mint);
|
||||||
|
let out_price = get_or_fetch_price(r.output_mint);
|
||||||
|
let amount = out_price * I80F48::from_num(r.out_amount)
|
||||||
|
- in_price * I80F48::from_num(r.in_amount);
|
||||||
|
|
||||||
|
let t = match r.raw {
|
||||||
|
swap::RawQuote::Mock => "mock",
|
||||||
|
swap::RawQuote::V6(_) => "jupiter",
|
||||||
|
swap::RawQuote::Sanctum(_) => "sanctum",
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"quote for {} vs {} [using {}] is {}@{} vs {}@{} -> amount={}",
|
||||||
|
r.input_mint,
|
||||||
|
r.output_mint,
|
||||||
|
t,
|
||||||
|
r.in_amount,
|
||||||
|
in_price,
|
||||||
|
r.out_amount,
|
||||||
|
out_price,
|
||||||
|
amount
|
||||||
|
);
|
||||||
|
|
||||||
|
std::cmp::Reverse(amount)
|
||||||
|
});
|
||||||
|
|
||||||
|
for route in routes {
|
||||||
|
let builder = self
|
||||||
|
.mango_client
|
||||||
|
.swap()
|
||||||
|
.prepare_swap_transaction(&route)
|
||||||
|
.await?;
|
||||||
|
let tx_size = builder.transaction_size()?;
|
||||||
|
if tx_size.is_within_limit() {
|
||||||
|
return Ok((builder, route.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
route_label = route.first_route_label(),
|
||||||
|
%route.input_mint,
|
||||||
|
%route.output_mint,
|
||||||
|
?tx_size,
|
||||||
|
limit = ?TransactionSize::limit(),
|
||||||
|
"route does not fit in a tx",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let best = alternatives
|
anyhow::bail!(
|
||||||
.iter()
|
"no routes from {} to {}",
|
||||||
.min_by(|a, b| a.price_impact_pct.partial_cmp(&b.price_impact_pct).unwrap())
|
original_input_mint,
|
||||||
.unwrap();
|
original_output_mint
|
||||||
let builder = self
|
);
|
||||||
.mango_client
|
|
||||||
.jupiter()
|
|
||||||
.prepare_swap_transaction(best)
|
|
||||||
.await?;
|
|
||||||
Ok((builder, best.clone()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mango_account(&self) -> anyhow::Result<Box<MangoAccountValue>> {
|
fn mango_account(&self) -> anyhow::Result<Box<MangoAccountValue>> {
|
||||||
|
@ -620,4 +762,15 @@ impl Rebalancer {
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn can_swap_on_sanctum(&self, mint: Pubkey) -> bool {
|
||||||
|
self.sanctum_supported_mints.contains(&mint)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(&mut self, live_rpc_client: &RpcClient) {
|
||||||
|
match swap::sanctum::load_supported_token_mints(live_rpc_client).await {
|
||||||
|
Err(e) => warn!("Could not load list of sanctum supported mint: {}", e),
|
||||||
|
Ok(mint) => self.sanctum_supported_mints.extend(mint),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use mango_v4_client::error_tracking::ErrorTracking;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
use mango_v4::state::TokenIndex;
|
use mango_v4::state::TokenIndex;
|
||||||
use mango_v4_client::jupiter;
|
use mango_v4_client::swap;
|
||||||
use mango_v4_client::MangoClient;
|
use mango_v4_client::MangoClient;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -15,7 +15,7 @@ pub struct Config {
|
||||||
/// Size in quote_index-token native tokens to quote.
|
/// Size in quote_index-token native tokens to quote.
|
||||||
pub quote_amount: u64,
|
pub quote_amount: u64,
|
||||||
|
|
||||||
pub jupiter_version: jupiter::Version,
|
pub jupiter_version: swap::Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -84,7 +84,7 @@ impl TokenSwapInfoUpdater {
|
||||||
lock.swap_infos.get(&token_index).cloned()
|
lock.swap_infos.get(&token_index).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn in_per_out_price(route: &jupiter::Quote) -> f64 {
|
fn in_per_out_price(route: &swap::Quote) -> f64 {
|
||||||
let in_amount = route.in_amount as f64;
|
let in_amount = route.in_amount as f64;
|
||||||
let out_amount = route.out_amount as f64;
|
let out_amount = route.out_amount as f64;
|
||||||
in_amount / out_amount
|
in_amount / out_amount
|
||||||
|
@ -149,7 +149,7 @@ impl TokenSwapInfoUpdater {
|
||||||
let token_amount = (self.config.quote_amount as f64 * token_per_quote_oracle) as u64;
|
let token_amount = (self.config.quote_amount as f64 * token_per_quote_oracle) as u64;
|
||||||
let sell_route = self
|
let sell_route = self
|
||||||
.mango_client
|
.mango_client
|
||||||
.jupiter()
|
.swap()
|
||||||
.quote(
|
.quote(
|
||||||
token_mint,
|
token_mint,
|
||||||
quote_mint,
|
quote_mint,
|
||||||
|
@ -161,7 +161,7 @@ impl TokenSwapInfoUpdater {
|
||||||
.await?;
|
.await?;
|
||||||
let buy_route = self
|
let buy_route = self
|
||||||
.mango_client
|
.mango_client
|
||||||
.jupiter()
|
.swap()
|
||||||
.quote(
|
.quote(
|
||||||
quote_mint,
|
quote_mint,
|
||||||
token_mint,
|
token_mint,
|
||||||
|
|
|
@ -13,7 +13,7 @@ use mango_v4::{
|
||||||
i80f48::ClampToInt,
|
i80f48::ClampToInt,
|
||||||
state::{Bank, MangoAccountValue, TokenConditionalSwap, TokenIndex},
|
state::{Bank, MangoAccountValue, TokenConditionalSwap, TokenIndex},
|
||||||
};
|
};
|
||||||
use mango_v4_client::{chain_data, jupiter, MangoClient, TransactionBuilder};
|
use mango_v4_client::{chain_data, swap, MangoClient, TransactionBuilder};
|
||||||
|
|
||||||
use anyhow::Context as AnyhowContext;
|
use anyhow::Context as AnyhowContext;
|
||||||
use mango_v4::accounts_ix::HealthCheckKind::MaintRatio;
|
use mango_v4::accounts_ix::HealthCheckKind::MaintRatio;
|
||||||
|
@ -73,7 +73,7 @@ pub struct Config {
|
||||||
/// Can be set to 0 to allow executions of any size.
|
/// Can be set to 0 to allow executions of any size.
|
||||||
pub min_buy_fraction: f64,
|
pub min_buy_fraction: f64,
|
||||||
|
|
||||||
pub jupiter_version: jupiter::Version,
|
pub jupiter_version: swap::Version,
|
||||||
pub jupiter_slippage_bps: u64,
|
pub jupiter_slippage_bps: u64,
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
|
|
||||||
|
@ -124,9 +124,9 @@ impl JupiterQuoteCache {
|
||||||
output_mint: Pubkey,
|
output_mint: Pubkey,
|
||||||
input_amount: u64,
|
input_amount: u64,
|
||||||
slippage_bps: u64,
|
slippage_bps: u64,
|
||||||
version: jupiter::Version,
|
version: swap::Version,
|
||||||
max_in_per_out_price: f64,
|
max_in_per_out_price: f64,
|
||||||
) -> anyhow::Result<JupiterQuoteCacheResult<(f64, jupiter::Quote)>> {
|
) -> anyhow::Result<JupiterQuoteCacheResult<(f64, swap::Quote)>> {
|
||||||
let cache_entry = self.cache_entry(input_mint, output_mint);
|
let cache_entry = self.cache_entry(input_mint, output_mint);
|
||||||
|
|
||||||
let held_lock = {
|
let held_lock = {
|
||||||
|
@ -184,10 +184,10 @@ impl JupiterQuoteCache {
|
||||||
output_mint: Pubkey,
|
output_mint: Pubkey,
|
||||||
input_amount: u64,
|
input_amount: u64,
|
||||||
slippage_bps: u64,
|
slippage_bps: u64,
|
||||||
version: jupiter::Version,
|
version: swap::Version,
|
||||||
) -> anyhow::Result<(f64, jupiter::Quote)> {
|
) -> anyhow::Result<(f64, swap::Quote)> {
|
||||||
let quote = client
|
let quote = client
|
||||||
.jupiter()
|
.swap()
|
||||||
.quote(
|
.quote(
|
||||||
input_mint,
|
input_mint,
|
||||||
output_mint,
|
output_mint,
|
||||||
|
@ -208,8 +208,8 @@ impl JupiterQuoteCache {
|
||||||
output_mint: Pubkey,
|
output_mint: Pubkey,
|
||||||
input_amount: u64,
|
input_amount: u64,
|
||||||
slippage_bps: u64,
|
slippage_bps: u64,
|
||||||
version: jupiter::Version,
|
version: swap::Version,
|
||||||
) -> anyhow::Result<(f64, jupiter::Quote)> {
|
) -> anyhow::Result<(f64, swap::Quote)> {
|
||||||
match self
|
match self
|
||||||
.quote(
|
.quote(
|
||||||
client,
|
client,
|
||||||
|
@ -255,11 +255,10 @@ impl JupiterQuoteCache {
|
||||||
collateral_amount: u64,
|
collateral_amount: u64,
|
||||||
sell_amount: u64,
|
sell_amount: u64,
|
||||||
slippage_bps: u64,
|
slippage_bps: u64,
|
||||||
version: jupiter::Version,
|
version: swap::Version,
|
||||||
max_sell_per_buy_price: f64,
|
max_sell_per_buy_price: f64,
|
||||||
) -> anyhow::Result<
|
) -> anyhow::Result<JupiterQuoteCacheResult<(f64, Option<swap::Quote>, Option<swap::Quote>)>>
|
||||||
JupiterQuoteCacheResult<(f64, Option<jupiter::Quote>, Option<jupiter::Quote>)>,
|
{
|
||||||
> {
|
|
||||||
// First check if we have cached prices for both legs and
|
// First check if we have cached prices for both legs and
|
||||||
// if those break the specified limit
|
// if those break the specified limit
|
||||||
let cached_collateral_to_buy = self.cached_price(collateral_mint, buy_mint).await;
|
let cached_collateral_to_buy = self.cached_price(collateral_mint, buy_mint).await;
|
||||||
|
@ -338,7 +337,7 @@ struct PreparedExecution {
|
||||||
max_sell_token_to_liqor: u64,
|
max_sell_token_to_liqor: u64,
|
||||||
min_buy_token: u64,
|
min_buy_token: u64,
|
||||||
min_taker_price: f32,
|
min_taker_price: f32,
|
||||||
jupiter_quote: Option<jupiter::Quote>,
|
jupiter_quote: Option<swap::Quote>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PreparationResult {
|
struct PreparationResult {
|
||||||
|
@ -1200,7 +1199,7 @@ impl Context {
|
||||||
// Jupiter quote is provided only for triggers, not close-expired
|
// Jupiter quote is provided only for triggers, not close-expired
|
||||||
let mut tx_builder = if let Some(jupiter_quote) = pending.jupiter_quote {
|
let mut tx_builder = if let Some(jupiter_quote) = pending.jupiter_quote {
|
||||||
self.mango_client
|
self.mango_client
|
||||||
.jupiter()
|
.swap()
|
||||||
.prepare_swap_transaction(&jupiter_quote)
|
.prepare_swap_transaction(&jupiter_quote)
|
||||||
.await?
|
.await?
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -46,3 +46,4 @@ base64 = "0.13.0"
|
||||||
bincode = "1.3.3"
|
bincode = "1.3.3"
|
||||||
tracing = { version = "0.1", features = ["log"] }
|
tracing = { version = "0.1", features = ["log"] }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
borsh = { version = "0.10.3", features = ["const-generics"] }
|
||||||
|
|
|
@ -27,14 +27,14 @@ use mango_v4::state::{
|
||||||
PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex, INSURANCE_TOKEN_INDEX,
|
PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex, INSURANCE_TOKEN_INDEX,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::account_fetcher::*;
|
|
||||||
use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig};
|
use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig};
|
||||||
use crate::context::MangoGroupContext;
|
use crate::context::MangoGroupContext;
|
||||||
use crate::gpa::{fetch_anchor_account, fetch_mango_accounts};
|
use crate::gpa::{fetch_anchor_account, fetch_mango_accounts};
|
||||||
use crate::health_cache;
|
use crate::health_cache;
|
||||||
use crate::priority_fees::{FixedPriorityFeeProvider, PriorityFeeProvider};
|
use crate::priority_fees::{FixedPriorityFeeProvider, PriorityFeeProvider};
|
||||||
|
use crate::util;
|
||||||
use crate::util::PreparedInstructions;
|
use crate::util::PreparedInstructions;
|
||||||
use crate::{jupiter, util};
|
use crate::{account_fetcher::*, swap};
|
||||||
use solana_address_lookup_table_program::state::AddressLookupTable;
|
use solana_address_lookup_table_program::state::AddressLookupTable;
|
||||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||||
use solana_client::rpc_client::SerializableTransaction;
|
use solana_client::rpc_client::SerializableTransaction;
|
||||||
|
@ -105,6 +105,15 @@ pub struct ClientConfig {
|
||||||
#[builder(default = "\"\".into()")]
|
#[builder(default = "\"\".into()")]
|
||||||
pub jupiter_token: String,
|
pub jupiter_token: String,
|
||||||
|
|
||||||
|
#[builder(default = "\"https://api.sanctum.so/v1\".into()")]
|
||||||
|
pub sanctum_url: String,
|
||||||
|
|
||||||
|
/// Sanctum Timeout, defaults to 30s
|
||||||
|
///
|
||||||
|
/// This timeout applies to jupiter requests.
|
||||||
|
#[builder(default = "Duration::from_secs(30)")]
|
||||||
|
pub sanctum_timeout: Duration,
|
||||||
|
|
||||||
/// Determines how fallback oracle accounts are provided to instructions. Defaults to Dynamic.
|
/// Determines how fallback oracle accounts are provided to instructions. Defaults to Dynamic.
|
||||||
#[builder(default = "FallbackOracleConfig::Dynamic")]
|
#[builder(default = "FallbackOracleConfig::Dynamic")]
|
||||||
pub fallback_oracle_config: FallbackOracleConfig,
|
pub fallback_oracle_config: FallbackOracleConfig,
|
||||||
|
@ -2169,17 +2178,71 @@ impl MangoClient {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// jupiter
|
// Swap (jupiter, sanctum)
|
||||||
|
pub fn swap(&self) -> swap::Swap {
|
||||||
|
swap::Swap { mango_client: self }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn jupiter_v6(&self) -> jupiter::v6::JupiterV6 {
|
pub fn jupiter_v6(&self) -> swap::jupiter_v6::JupiterV6 {
|
||||||
jupiter::v6::JupiterV6 {
|
swap::jupiter_v6::JupiterV6 {
|
||||||
mango_client: self,
|
mango_client: self,
|
||||||
timeout_duration: self.client.config.jupiter_timeout,
|
timeout_duration: self.client.config.jupiter_timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn jupiter(&self) -> jupiter::Jupiter {
|
pub fn sanctum(&self) -> swap::sanctum::Sanctum {
|
||||||
jupiter::Jupiter { mango_client: self }
|
swap::sanctum::Sanctum {
|
||||||
|
mango_client: self,
|
||||||
|
timeout_duration: self.client.config.sanctum_timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = self
|
||||||
|
.fetch_address_lookup_tables(lookups.iter().map(|a| &a.account_key))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut account_keys = message.static_account_keys().to_vec();
|
||||||
|
for (lookups, table) in lookups.iter().zip(address_lookup_tables.iter()) {
|
||||||
|
account_keys.extend(
|
||||||
|
lookups
|
||||||
|
.writable_indexes
|
||||||
|
.iter()
|
||||||
|
.map(|&index| table.addresses[index as usize]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (lookups, table) in lookups.iter().zip(address_lookup_tables.iter()) {
|
||||||
|
account_keys.extend(
|
||||||
|
lookups
|
||||||
|
.readonly_indexes
|
||||||
|
.iter()
|
||||||
|
.map(|&index| table.addresses[index as usize]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let compiled_ix = message
|
||||||
|
.instructions()
|
||||||
|
.iter()
|
||||||
|
.map(|ci| solana_sdk::instruction::Instruction {
|
||||||
|
program_id: *ci.program_id(&account_keys),
|
||||||
|
accounts: ci
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.map(|&index| AccountMeta {
|
||||||
|
pubkey: account_keys[index as usize],
|
||||||
|
is_signer: message.is_signer(index.into()),
|
||||||
|
is_writable: message.is_maybe_writable(index.into()),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
data: ci.data.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((compiled_ix, address_lookup_tables))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_address_lookup_table(
|
pub async fn fetch_address_lookup_table(
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use anchor_lang::{AccountDeserialize, Discriminator};
|
use anchor_lang::{AccountDeserialize, Discriminator};
|
||||||
|
use futures::{stream, StreamExt};
|
||||||
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
|
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
|
||||||
|
|
||||||
use solana_account_decoder::UiAccountEncoding;
|
use solana_account_decoder::UiAccountEncoding;
|
||||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||||
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
|
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
|
||||||
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
|
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
|
||||||
use solana_sdk::account::AccountSharedData;
|
use solana_sdk::account::{Account, AccountSharedData};
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
pub async fn fetch_mango_accounts(
|
pub async fn fetch_mango_accounts(
|
||||||
|
@ -148,3 +149,49 @@ pub async fn fetch_multiple_accounts(
|
||||||
.map(|(acc, key)| (*key, acc.unwrap().into()))
|
.map(|(acc, key)| (*key, acc.unwrap().into()))
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch multiple account using one request per chunk of `max_chunk_size` accounts
|
||||||
|
/// Can execute in parallel up to `parallel_rpc_requests`
|
||||||
|
///
|
||||||
|
/// WARNING: some accounts requested may be missing from the result
|
||||||
|
pub async fn fetch_multiple_accounts_in_chunks(
|
||||||
|
rpc: &RpcClientAsync,
|
||||||
|
keys: &[Pubkey],
|
||||||
|
max_chunk_size: usize,
|
||||||
|
parallel_rpc_requests: usize,
|
||||||
|
) -> anyhow::Result<Vec<(Pubkey, Account)>> {
|
||||||
|
let config = RpcAccountInfoConfig {
|
||||||
|
encoding: Some(UiAccountEncoding::Base64),
|
||||||
|
..RpcAccountInfoConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let raw_results = stream::iter(keys)
|
||||||
|
.chunks(max_chunk_size)
|
||||||
|
.map(|keys| {
|
||||||
|
let account_info_config = config.clone();
|
||||||
|
async move {
|
||||||
|
let keys = keys.iter().map(|x| **x).collect::<Vec<Pubkey>>();
|
||||||
|
let req_res = rpc
|
||||||
|
.get_multiple_accounts_with_config(&keys, account_info_config)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match req_res {
|
||||||
|
Ok(v) => Ok(keys.into_iter().zip(v.value).collect::<Vec<_>>()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.buffer_unordered(parallel_rpc_requests)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let result = raw_results
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Result<Vec<_>, _>>()?
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|(pubkey, account_opt)| account_opt.map(|acc| (pubkey, acc)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
|
@ -13,11 +13,11 @@ mod context;
|
||||||
pub mod error_tracking;
|
pub mod error_tracking;
|
||||||
pub mod gpa;
|
pub mod gpa;
|
||||||
pub mod health_cache;
|
pub mod health_cache;
|
||||||
pub mod jupiter;
|
|
||||||
pub mod perp_pnl;
|
pub mod perp_pnl;
|
||||||
pub mod priority_fees;
|
pub mod priority_fees;
|
||||||
pub mod priority_fees_cli;
|
pub mod priority_fees_cli;
|
||||||
pub mod snapshot_source;
|
pub mod snapshot_source;
|
||||||
|
pub mod swap;
|
||||||
mod util;
|
mod util;
|
||||||
pub mod websocket_source;
|
pub mod websocket_source;
|
||||||
|
|
||||||
|
|
|
@ -73,13 +73,7 @@ pub struct SwapRequest {
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct SwapResponse {
|
pub struct JupiterSwapInstructionsResponse {
|
||||||
pub swap_transaction: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct SwapInstructionsResponse {
|
|
||||||
pub token_ledger_instruction: Option<InstructionResponse>,
|
pub token_ledger_instruction: Option<InstructionResponse>,
|
||||||
pub compute_budget_instructions: Option<Vec<InstructionResponse>>,
|
pub compute_budget_instructions: Option<Vec<InstructionResponse>>,
|
||||||
pub setup_instructions: Option<Vec<InstructionResponse>>,
|
pub setup_instructions: Option<Vec<InstructionResponse>>,
|
||||||
|
@ -298,7 +292,7 @@ impl<'a> JupiterV6<'a> {
|
||||||
.await
|
.await
|
||||||
.context("swap transaction request to jupiter")?;
|
.context("swap transaction request to jupiter")?;
|
||||||
|
|
||||||
let swap: SwapInstructionsResponse = util::http_error_handling(swap_response)
|
let swap: JupiterSwapInstructionsResponse = util::http_error_handling(swap_response)
|
||||||
.await
|
.await
|
||||||
.context("error requesting jupiter swap")?;
|
.context("error requesting jupiter swap")?;
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
pub mod v6;
|
pub mod jupiter_v6;
|
||||||
|
pub mod sanctum;
|
||||||
|
pub mod sanctum_state;
|
||||||
|
|
||||||
use anchor_lang::prelude::*;
|
use anchor_lang::prelude::*;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
@ -10,13 +12,15 @@ use fixed::types::I80F48;
|
||||||
pub enum Version {
|
pub enum Version {
|
||||||
Mock,
|
Mock,
|
||||||
V6,
|
V6,
|
||||||
|
Sanctum,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum RawQuote {
|
pub enum RawQuote {
|
||||||
Mock,
|
Mock,
|
||||||
V6(v6::QuoteResponse),
|
V6(jupiter_v6::QuoteResponse),
|
||||||
|
Sanctum(sanctum::QuoteResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -30,7 +34,7 @@ pub struct Quote {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Quote {
|
impl Quote {
|
||||||
pub fn try_from_v6(query: v6::QuoteResponse) -> anyhow::Result<Self> {
|
pub fn try_from_v6(query: jupiter_v6::QuoteResponse) -> anyhow::Result<Self> {
|
||||||
Ok(Quote {
|
Ok(Quote {
|
||||||
input_mint: Pubkey::from_str(&query.input_mint)?,
|
input_mint: Pubkey::from_str(&query.input_mint)?,
|
||||||
output_mint: Pubkey::from_str(&query.output_mint)?,
|
output_mint: Pubkey::from_str(&query.output_mint)?,
|
||||||
|
@ -45,6 +49,25 @@ impl Quote {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn try_from_sanctum(
|
||||||
|
input_mint: Pubkey,
|
||||||
|
output_mint: Pubkey,
|
||||||
|
query: sanctum::QuoteResponse,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
Ok(Quote {
|
||||||
|
input_mint: input_mint,
|
||||||
|
output_mint: output_mint,
|
||||||
|
price_impact_pct: query.fee_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::Sanctum(query),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn first_route_label(&self) -> String {
|
pub fn first_route_label(&self) -> String {
|
||||||
let label_maybe = match &self.raw {
|
let label_maybe = match &self.raw {
|
||||||
RawQuote::Mock => Some("mock".into()),
|
RawQuote::Mock => Some("mock".into()),
|
||||||
|
@ -54,16 +77,17 @@ impl Quote {
|
||||||
.and_then(|v| v.swap_info.as_ref())
|
.and_then(|v| v.swap_info.as_ref())
|
||||||
.and_then(|v| v.label.as_ref())
|
.and_then(|v| v.label.as_ref())
|
||||||
.cloned(),
|
.cloned(),
|
||||||
|
RawQuote::Sanctum(raw) => Some(raw.swap_src.clone()),
|
||||||
};
|
};
|
||||||
label_maybe.unwrap_or_else(|| "unknown".into())
|
label_maybe.unwrap_or_else(|| "unknown".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Jupiter<'a> {
|
pub struct Swap<'a> {
|
||||||
pub mango_client: &'a MangoClient,
|
pub mango_client: &'a MangoClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Jupiter<'a> {
|
impl<'a> Swap<'a> {
|
||||||
async fn quote_mock(
|
async fn quote_mock(
|
||||||
&self,
|
&self,
|
||||||
input_mint: Pubkey,
|
input_mint: Pubkey,
|
||||||
|
@ -123,6 +147,14 @@ impl<'a> Jupiter<'a> {
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)?,
|
)?,
|
||||||
|
Version::Sanctum => Quote::try_from_sanctum(
|
||||||
|
input_mint,
|
||||||
|
output_mint,
|
||||||
|
self.mango_client
|
||||||
|
.sanctum()
|
||||||
|
.quote(input_mint, output_mint, amount)
|
||||||
|
.await?,
|
||||||
|
)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,6 +170,18 @@ impl<'a> Jupiter<'a> {
|
||||||
.prepare_swap_transaction(raw)
|
.prepare_swap_transaction(raw)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
RawQuote::Sanctum(raw) => {
|
||||||
|
let max_slippage_bps = (quote.price_impact_pct * 100.0).ceil() as u64;
|
||||||
|
self.mango_client
|
||||||
|
.sanctum()
|
||||||
|
.prepare_swap_transaction(
|
||||||
|
quote.input_mint,
|
||||||
|
quote.output_mint,
|
||||||
|
max_slippage_bps,
|
||||||
|
raw,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,401 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anchor_lang::{system_program, Id};
|
||||||
|
use anchor_spl::token::Token;
|
||||||
|
use anyhow::Context;
|
||||||
|
use bincode::Options;
|
||||||
|
use mango_v4::accounts_zerocopy::AccountReader;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use solana_address_lookup_table_program::state::AddressLookupTable;
|
||||||
|
use solana_client::nonblocking::rpc_client::RpcClient;
|
||||||
|
use solana_sdk::account::Account;
|
||||||
|
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Signature};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::gpa::fetch_multiple_accounts_in_chunks;
|
||||||
|
use crate::swap::sanctum_state;
|
||||||
|
use crate::{util, MangoClient, TransactionBuilder};
|
||||||
|
use borsh::BorshDeserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct QuoteResponse {
|
||||||
|
pub in_amount: Option<String>,
|
||||||
|
pub out_amount: String,
|
||||||
|
pub fee_amount: String,
|
||||||
|
pub fee_mint: String,
|
||||||
|
pub fee_pct: String,
|
||||||
|
pub swap_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SwapRequest {
|
||||||
|
pub amount: String,
|
||||||
|
pub quoted_amount: String,
|
||||||
|
pub input: String,
|
||||||
|
pub mode: String,
|
||||||
|
pub output_lst_mint: String,
|
||||||
|
pub signer: String,
|
||||||
|
pub swap_src: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SanctumSwapResponse {
|
||||||
|
pub tx: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Sanctum<'a> {
|
||||||
|
pub mango_client: &'a MangoClient,
|
||||||
|
pub timeout_duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Sanctum<'a> {
|
||||||
|
pub async fn quote(
|
||||||
|
&self,
|
||||||
|
input_mint: Pubkey,
|
||||||
|
output_mint: Pubkey,
|
||||||
|
amount: u64,
|
||||||
|
) -> anyhow::Result<QuoteResponse> {
|
||||||
|
if input_mint == output_mint {
|
||||||
|
anyhow::bail!("Need two distinct mint to swap");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 query_args = vec![
|
||||||
|
("input", input_mint.to_string()),
|
||||||
|
("outputLstMint", output_mint.to_string()),
|
||||||
|
("amount", format!("{}", amount)),
|
||||||
|
];
|
||||||
|
let config = self.mango_client.client.config();
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.mango_client
|
||||||
|
.http_client
|
||||||
|
.get(format!("{}/swap/quote", config.sanctum_url))
|
||||||
|
.query(&query_args)
|
||||||
|
.timeout(self.timeout_duration)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("quote request to sanctum")?;
|
||||||
|
let quote: QuoteResponse =
|
||||||
|
util::http_error_handling(response).await.with_context(|| {
|
||||||
|
format!("error requesting sanctum route between {input_mint} and {output_mint} (using url: {})", config.sanctum_url)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(quote)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the instructions and account lookup tables for a sanctum swap through mango
|
||||||
|
pub async fn prepare_swap_transaction(
|
||||||
|
&self,
|
||||||
|
input_mint: Pubkey,
|
||||||
|
output_mint: Pubkey,
|
||||||
|
max_slippage_bps: u64,
|
||||||
|
quote: &QuoteResponse,
|
||||||
|
) -> anyhow::Result<TransactionBuilder> {
|
||||||
|
tracing::info!("swapping using sanctum");
|
||||||
|
|
||||||
|
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.first_bank(), target_token.first_bank()]
|
||||||
|
.into_iter()
|
||||||
|
.map(util::to_writable_account_meta)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let vault_ams = [source_token.first_vault(), target_token.first_vault()]
|
||||||
|
.into_iter()
|
||||||
|
.map(util::to_writable_account_meta)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let owner = self.mango_client.owner();
|
||||||
|
let account = &self.mango_client.mango_account().await?;
|
||||||
|
|
||||||
|
let token_ams = [source_token.mint, target_token.mint]
|
||||||
|
.into_iter()
|
||||||
|
.map(|mint| {
|
||||||
|
util::to_writable_account_meta(
|
||||||
|
anchor_spl::associated_token::get_associated_token_address(&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, _health_cu) = self
|
||||||
|
.mango_client
|
||||||
|
.derive_health_check_remaining_account_metas(
|
||||||
|
account,
|
||||||
|
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 config = self.mango_client.client.config();
|
||||||
|
|
||||||
|
let in_amount = quote
|
||||||
|
.in_amount
|
||||||
|
.clone()
|
||||||
|
.expect("sanctum require a in amount");
|
||||||
|
let quote_amount_u64 = quote.out_amount.parse::<u64>()?;
|
||||||
|
let out_amount = ((quote_amount_u64 as f64) * (1.0 - (max_slippage_bps as f64) / 10_000.0))
|
||||||
|
.ceil() as u64;
|
||||||
|
|
||||||
|
let swap_response = self
|
||||||
|
.mango_client
|
||||||
|
.http_client
|
||||||
|
.post(format!("{}/swap", config.sanctum_url))
|
||||||
|
.json(&SwapRequest {
|
||||||
|
amount: in_amount.clone(),
|
||||||
|
quoted_amount: out_amount.to_string(),
|
||||||
|
input: input_mint.to_string(),
|
||||||
|
mode: "ExactIn".to_string(),
|
||||||
|
output_lst_mint: output_mint.to_string(),
|
||||||
|
signer: owner.to_string(),
|
||||||
|
swap_src: quote.swap_src.clone(),
|
||||||
|
})
|
||||||
|
.timeout(self.timeout_duration)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("swap transaction request to sanctum")?;
|
||||||
|
|
||||||
|
let swap_r: SanctumSwapResponse = util::http_error_handling(swap_response)
|
||||||
|
.await
|
||||||
|
.context("error requesting sanctum swap")?;
|
||||||
|
|
||||||
|
let tx = bincode::options()
|
||||||
|
.with_fixint_encoding()
|
||||||
|
.reject_trailing_bytes()
|
||||||
|
.deserialize::<solana_sdk::transaction::VersionedTransaction>(
|
||||||
|
&base64::decode(&swap_r.tx).context("base64 decoding sanctum transaction")?,
|
||||||
|
)
|
||||||
|
.context("parsing sanctum transaction")?;
|
||||||
|
|
||||||
|
let (sanctum_ixs_orig, sanctum_alts) = self
|
||||||
|
.mango_client
|
||||||
|
.deserialize_instructions_and_alts(&tx.message)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let system_program = system_program::ID;
|
||||||
|
let ata_program = anchor_spl::associated_token::ID;
|
||||||
|
let token_program = anchor_spl::token::ID;
|
||||||
|
let compute_budget_program: Pubkey = 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 sync_native_pack =
|
||||||
|
anchor_spl::token::spl_token::instruction::TokenInstruction::SyncNative.pack();
|
||||||
|
|
||||||
|
// Remove auto wrapping of SOL->wSOL
|
||||||
|
let sanctum_ixs: Vec<Instruction> = sanctum_ixs_orig
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|ix| {
|
||||||
|
!(ix.program_id == system_program)
|
||||||
|
&& !(ix.program_id == token_program && ix.data == sync_native_pack)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let sanctum_action_ix_begin = sanctum_ixs
|
||||||
|
.iter()
|
||||||
|
.position(|ix| !is_setup_ix(ix.program_id))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!("sanctum swap response only had setup-like instructions")
|
||||||
|
})?;
|
||||||
|
let sanctum_action_ix_end = sanctum_ixs.len()
|
||||||
|
- sanctum_ixs
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.position(|ix| !is_setup_ix(ix.program_id))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut instructions: Vec<Instruction> = Vec::new();
|
||||||
|
|
||||||
|
for ix in &sanctum_ixs[..sanctum_action_ix_begin] {
|
||||||
|
instructions.push(ix.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the source token account is created (sanctum takes care of the output account)
|
||||||
|
instructions.push(
|
||||||
|
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
|
||||||
|
&owner,
|
||||||
|
&owner,
|
||||||
|
&source_token.mint,
|
||||||
|
&Token::id(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
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 &sanctum_ixs[sanctum_action_ix_begin..sanctum_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,
|
||||||
|
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 &sanctum_ixs[sanctum_action_ix_end..] {
|
||||||
|
instructions.push(ix.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut address_lookup_tables = self.mango_client.mango_address_lookup_tables().await?;
|
||||||
|
address_lookup_tables.extend(sanctum_alts.into_iter());
|
||||||
|
|
||||||
|
let payer = owner; // 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
|
||||||
|
.config()
|
||||||
|
.transaction_builder_config
|
||||||
|
.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn swap(
|
||||||
|
&self,
|
||||||
|
input_mint: Pubkey,
|
||||||
|
output_mint: Pubkey,
|
||||||
|
max_slippage_bps: u64,
|
||||||
|
amount: u64,
|
||||||
|
) -> anyhow::Result<Signature> {
|
||||||
|
let route = self.quote(input_mint, output_mint, amount).await?;
|
||||||
|
|
||||||
|
let tx_builder = self
|
||||||
|
.prepare_swap_transaction(input_mint, output_mint, max_slippage_bps, &route)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx_builder.send_and_confirm(&self.mango_client.client).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_supported_token_mints(
|
||||||
|
live_rpc_client: &RpcClient,
|
||||||
|
) -> anyhow::Result<HashSet<Pubkey>> {
|
||||||
|
let address = Pubkey::from_str("EhWxBHdmQ3yDmPzhJbKtGMM9oaZD42emt71kSieghy5")?;
|
||||||
|
|
||||||
|
let lookup_table_data = live_rpc_client.get_account(&address).await?;
|
||||||
|
let lookup_table = AddressLookupTable::deserialize(&lookup_table_data.data())?;
|
||||||
|
let accounts: Vec<Account> =
|
||||||
|
fetch_multiple_accounts_in_chunks(live_rpc_client, &lookup_table.addresses, 100, 1)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|x| x.1)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut lst_mints = HashSet::new();
|
||||||
|
for account in accounts {
|
||||||
|
let account = Account::from(account);
|
||||||
|
let mut account_data = account.data();
|
||||||
|
let t = sanctum_state::StakePool::deserialize(&mut account_data);
|
||||||
|
if let Ok(d) = t {
|
||||||
|
lst_mints.insert(d.pool_mint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hardcoded for now
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("CgntPoLka5pD5fesJYhGmUCF8KU1QS1ZmZiuAuMZr2az").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("7ge2xKsZXmqPxa3YmXxXmzCp9Hc2ezrTxh6PECaxCwrL").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("GUAMR8ciiaijraJeLDEDrFVaueLm9YzWWY9R7CBPL9rA").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("Jito4APyf642JPZPx3hGc6WWJ8zPKtRbRs4P815Awbb").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("CtMyWsrUtAwXWiGr9WjHT5fC3p3fgV8cyGpLTo2LJzG1").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("2qyEeSAWKfU18AFthrF7JA8z8ZCi1yt76Tqs917vwQTV").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("DqhH94PjkZsjAqEze2BEkWhFQJ6EyU6MdtMphMgnXqeK").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("F8h46pYkaqPJNP2MRkUUUtRkf8efCkpoqehn9g1bTTm7").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("5oc4nmbNTda9fx8Tw57ShLD132aqDK65vuHH4RU1K4LZ").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
lst_mints.insert(
|
||||||
|
Pubkey::from_str("stk9ApL5HeVAwPLr3TLhDXdZS8ptVu7zp6ov8HFDuMi").expect("invalid lst mint"),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(lst_mints)
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
use {
|
||||||
|
borsh::BorshDeserialize,
|
||||||
|
solana_sdk::{pubkey::Pubkey, stake::state::Lockup},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, BorshDeserialize)]
|
||||||
|
pub enum AccountType {
|
||||||
|
/// If the account has not been initialized, the enum will be 0
|
||||||
|
Uninitialized,
|
||||||
|
/// Stake pool
|
||||||
|
StakePool,
|
||||||
|
/// Validator stake list
|
||||||
|
ValidatorList,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Debug, PartialEq, BorshDeserialize)]
|
||||||
|
pub struct StakePool {
|
||||||
|
/// Account type, must be StakePool currently
|
||||||
|
pub account_type: AccountType,
|
||||||
|
|
||||||
|
/// Manager authority, allows for updating the staker, manager, and fee
|
||||||
|
/// account
|
||||||
|
pub manager: Pubkey,
|
||||||
|
|
||||||
|
/// Staker authority, allows for adding and removing validators, and
|
||||||
|
/// managing stake distribution
|
||||||
|
pub staker: Pubkey,
|
||||||
|
|
||||||
|
/// Stake deposit authority
|
||||||
|
///
|
||||||
|
/// If a depositor pubkey is specified on initialization, then deposits must
|
||||||
|
/// be signed by this authority. If no deposit authority is specified,
|
||||||
|
/// then the stake pool will default to the result of:
|
||||||
|
/// `Pubkey::find_program_address(
|
||||||
|
/// &[&stake_pool_address.as_ref(), b"deposit"],
|
||||||
|
/// program_id,
|
||||||
|
/// )`
|
||||||
|
pub stake_deposit_authority: Pubkey,
|
||||||
|
|
||||||
|
/// Stake withdrawal authority bump seed
|
||||||
|
/// for `create_program_address(&[state::StakePool account, "withdrawal"])`
|
||||||
|
pub stake_withdraw_bump_seed: u8,
|
||||||
|
|
||||||
|
/// Validator stake list storage account
|
||||||
|
pub validator_list: Pubkey,
|
||||||
|
|
||||||
|
/// Reserve stake account, holds deactivated stake
|
||||||
|
pub reserve_stake: Pubkey,
|
||||||
|
|
||||||
|
/// Pool Mint
|
||||||
|
pub pool_mint: Pubkey,
|
||||||
|
|
||||||
|
/// Manager fee account
|
||||||
|
pub manager_fee_account: Pubkey,
|
||||||
|
|
||||||
|
/// Pool token program id
|
||||||
|
pub token_program_id: Pubkey,
|
||||||
|
|
||||||
|
/// Total stake under management.
|
||||||
|
/// Note that if `last_update_epoch` does not match the current epoch then
|
||||||
|
/// this field may not be accurate
|
||||||
|
pub total_lamports: u64,
|
||||||
|
|
||||||
|
/// Total supply of pool tokens (should always match the supply in the Pool
|
||||||
|
/// Mint)
|
||||||
|
pub pool_token_supply: u64,
|
||||||
|
|
||||||
|
/// Last epoch the `total_lamports` field was updated
|
||||||
|
pub last_update_epoch: u64,
|
||||||
|
|
||||||
|
/// Lockup that all stakes in the pool must have
|
||||||
|
pub lockup: Lockup,
|
||||||
|
|
||||||
|
/// Fee taken as a proportion of rewards each epoch
|
||||||
|
pub epoch_fee: Fee,
|
||||||
|
|
||||||
|
/// Fee for next epoch
|
||||||
|
pub next_epoch_fee: FutureEpoch<Fee>,
|
||||||
|
|
||||||
|
/// Preferred deposit validator vote account pubkey
|
||||||
|
pub preferred_deposit_validator_vote_address: Option<Pubkey>,
|
||||||
|
|
||||||
|
/// Preferred withdraw validator vote account pubkey
|
||||||
|
pub preferred_withdraw_validator_vote_address: Option<Pubkey>,
|
||||||
|
|
||||||
|
/// Fee assessed on stake deposits
|
||||||
|
pub stake_deposit_fee: Fee,
|
||||||
|
|
||||||
|
/// Fee assessed on withdrawals
|
||||||
|
pub stake_withdrawal_fee: Fee,
|
||||||
|
|
||||||
|
/// Future stake withdrawal fee, to be set for the following epoch
|
||||||
|
pub next_stake_withdrawal_fee: FutureEpoch<Fee>,
|
||||||
|
|
||||||
|
/// Fees paid out to referrers on referred stake deposits.
|
||||||
|
/// Expressed as a percentage (0 - 100) of deposit fees.
|
||||||
|
/// i.e. `stake_deposit_fee`% of stake deposited is collected as deposit
|
||||||
|
/// fees for every deposit and `stake_referral_fee`% of the collected
|
||||||
|
/// stake deposit fees is paid out to the referrer
|
||||||
|
pub stake_referral_fee: u8,
|
||||||
|
|
||||||
|
/// Toggles whether the `DepositSol` instruction requires a signature from
|
||||||
|
/// this `sol_deposit_authority`
|
||||||
|
pub sol_deposit_authority: Option<Pubkey>,
|
||||||
|
|
||||||
|
/// Fee assessed on SOL deposits
|
||||||
|
pub sol_deposit_fee: Fee,
|
||||||
|
|
||||||
|
/// Fees paid out to referrers on referred SOL deposits.
|
||||||
|
/// Expressed as a percentage (0 - 100) of SOL deposit fees.
|
||||||
|
/// i.e. `sol_deposit_fee`% of SOL deposited is collected as deposit fees
|
||||||
|
/// for every deposit and `sol_referral_fee`% of the collected SOL
|
||||||
|
/// deposit fees is paid out to the referrer
|
||||||
|
pub sol_referral_fee: u8,
|
||||||
|
|
||||||
|
/// Toggles whether the `WithdrawSol` instruction requires a signature from
|
||||||
|
/// the `deposit_authority`
|
||||||
|
pub sol_withdraw_authority: Option<Pubkey>,
|
||||||
|
|
||||||
|
/// Fee assessed on SOL withdrawals
|
||||||
|
pub sol_withdrawal_fee: Fee,
|
||||||
|
|
||||||
|
/// Future SOL withdrawal fee, to be set for the following epoch
|
||||||
|
pub next_sol_withdrawal_fee: FutureEpoch<Fee>,
|
||||||
|
|
||||||
|
/// Last epoch's total pool tokens, used only for APR estimation
|
||||||
|
pub last_epoch_pool_token_supply: u64,
|
||||||
|
|
||||||
|
/// Last epoch's total lamports, used only for APR estimation
|
||||||
|
pub last_epoch_total_lamports: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of
|
||||||
|
/// the rewards
|
||||||
|
/// If either the numerator or the denominator is 0, the fee is considered to be
|
||||||
|
/// 0
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, BorshDeserialize)]
|
||||||
|
pub struct Fee {
|
||||||
|
/// denominator of the fee ratio
|
||||||
|
pub denominator: u64,
|
||||||
|
/// numerator of the fee ratio
|
||||||
|
pub numerator: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wrapper type that "counts down" epochs, which is Borsh-compatible with the
|
||||||
|
/// native `Option`
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, BorshDeserialize)]
|
||||||
|
pub enum FutureEpoch<T> {
|
||||||
|
/// Nothing is set
|
||||||
|
None,
|
||||||
|
/// Value is ready after the next epoch boundary
|
||||||
|
One(T),
|
||||||
|
/// Value is ready after two epoch boundaries
|
||||||
|
Two(T),
|
||||||
|
}
|
Loading…
Reference in New Issue