2024-02-19 01:20:12 -08:00
|
|
|
use std::collections::HashSet;
|
2024-03-20 07:25:52 -07:00
|
|
|
use std::time::Duration;
|
2023-08-18 06:36:02 -07:00
|
|
|
use std::{
|
2023-10-07 12:27:19 -07:00
|
|
|
collections::HashMap,
|
2023-08-18 06:36:02 -07:00
|
|
|
pin::Pin,
|
2023-10-07 12:27:19 -07:00
|
|
|
sync::{Arc, RwLock},
|
2024-02-19 01:20:12 -08:00
|
|
|
time::Instant,
|
2023-08-18 06:36:02 -07:00
|
|
|
};
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-08-18 06:36:02 -07:00
|
|
|
use futures_core::Future;
|
2023-08-11 03:08:34 -07:00
|
|
|
use itertools::Itertools;
|
|
|
|
use mango_v4::{
|
|
|
|
i80f48::ClampToInt,
|
2023-08-18 06:36:02 -07:00
|
|
|
state::{Bank, MangoAccountValue, TokenConditionalSwap, TokenIndex},
|
2023-08-11 03:08:34 -07:00
|
|
|
};
|
2024-01-23 08:26:31 -08:00
|
|
|
use mango_v4_client::{chain_data, jupiter, MangoClient, TransactionBuilder};
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
use anyhow::Context as AnyhowContext;
|
2024-03-20 07:25:52 -07:00
|
|
|
use mango_v4::accounts_ix::HealthCheckKind::MaintRatio;
|
2024-02-07 03:52:32 -08:00
|
|
|
use solana_sdk::signature::Signature;
|
2023-07-11 23:38:38 -07:00
|
|
|
use tracing::*;
|
2023-10-07 12:27:19 -07:00
|
|
|
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-12-19 06:37:20 -08:00
|
|
|
use crate::{token_swap_info, util, ErrorTracking, LiqErrorType};
|
2023-08-11 03:08:34 -07:00
|
|
|
|
2023-08-14 03:04:17 -07:00
|
|
|
/// When computing the max possible swap for a liqee, assume the price is this fraction worse for them.
|
|
|
|
///
|
|
|
|
/// That way when executing the swap, the prices may move this much against the liqee without
|
|
|
|
/// making the whole execution fail.
|
|
|
|
const SLIPPAGE_BUFFER: f64 = 0.01; // 1%
|
|
|
|
|
2023-12-05 06:43:38 -08:00
|
|
|
/// If a tcs gets limited due to exhausted net borrows or deposit limits, don't trigger execution if
|
|
|
|
/// the possible value is below this amount. This avoids spamming executions when limits are exhausted.
|
|
|
|
const EXECUTION_THRESHOLD: u64 = 1_000_000; // 1 USD
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
|
|
pub enum Mode {
|
|
|
|
/// Directly execute the trigger, borrowing any buy tokens needed. Normal rebalancing will
|
|
|
|
/// resolve deposits and withdraws created by trigger execution
|
|
|
|
BorrowBuyToken,
|
|
|
|
|
|
|
|
/// Do a jupiter swap in the same tx as the trigger, possibly creating a buy token deposit
|
|
|
|
/// and a sell token borrow. This can create a temporary sell token borrow that gets
|
|
|
|
/// mostly closed by the trigger execution.
|
|
|
|
///
|
|
|
|
/// This is the nicest mode because it leaves the smallest buy/sell token balances, the
|
|
|
|
/// trigger execution is almost single transaction arbitrage.
|
|
|
|
///
|
|
|
|
/// If sell token borrows are impossible (reduce only, borrow limits), this
|
|
|
|
/// will back back on SwapCollateralIntoBuy.
|
|
|
|
SwapSellIntoBuy,
|
|
|
|
|
|
|
|
/// Do a jupiter swap in the same tx as the trigger, buying the buy token for the
|
|
|
|
/// collateral token. This way the liquidator won't need to borrow tokens.
|
|
|
|
SwapCollateralIntoBuy,
|
|
|
|
}
|
|
|
|
|
2023-08-18 06:36:02 -07:00
|
|
|
#[derive(Clone)]
|
2023-07-03 05:09:11 -07:00
|
|
|
pub struct Config {
|
2024-03-20 07:25:52 -07:00
|
|
|
pub refresh_timeout: Duration,
|
2023-07-03 05:09:11 -07:00
|
|
|
pub min_health_ratio: f64,
|
|
|
|
pub max_trigger_quote_amount: u64,
|
2023-08-11 03:48:50 -07:00
|
|
|
pub compute_limit_for_trigger: u32,
|
2023-10-07 12:27:19 -07:00
|
|
|
pub collateral_token_index: TokenIndex,
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
/// At 0, the liquidator would trigger tcs if the cost to the liquidator is the
|
|
|
|
/// same as the cost to the liqee. 0.1 would mean a 10% better price to the liquidator.
|
|
|
|
pub profit_fraction: f64,
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
/// Minimum fraction of max_buy to buy for success when triggering,
|
2023-11-22 23:59:19 -08:00
|
|
|
/// useful in conjunction with jupiter swaps in same tx to avoid over-buying.
|
2023-10-07 12:27:19 -07:00
|
|
|
///
|
|
|
|
/// Can be set to 0 to allow executions of any size.
|
|
|
|
pub min_buy_fraction: f64,
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
pub jupiter_version: jupiter::Version,
|
|
|
|
pub jupiter_slippage_bps: u64,
|
|
|
|
pub mode: Mode,
|
2024-02-19 01:20:12 -08:00
|
|
|
|
|
|
|
pub only_allowed_tokens: HashSet<TokenIndex>,
|
|
|
|
pub forbidden_tokens: HashSet<TokenIndex>,
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
2023-07-03 05:09:11 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
pub enum JupiterQuoteCacheResult<T> {
|
|
|
|
Quote(T),
|
|
|
|
BadPrice(f64),
|
2023-07-03 05:09:11 -07:00
|
|
|
}
|
|
|
|
|
2023-10-09 10:56:45 -07:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct JupiterQuoteCacheEntry {
|
|
|
|
// The lowest seen price (starting at f64::MAX) in input-per-output tokens
|
|
|
|
//
|
|
|
|
// A separate mutex is necessary since we want to wait for the first jupiter quote for
|
|
|
|
// each pair to return before initiating any further ones. Later on there can be multiple
|
|
|
|
// jupiter quotes for a pair at the same time if the initial price check passes.
|
|
|
|
price: Arc<tokio::sync::Mutex<f64>>,
|
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
// While preparing tcs executions, there may be a lot of jupiter queries for the same
|
|
|
|
// pairs. This caches results to avoid hitting jupiter with too many requests by allowing
|
|
|
|
// a cheap-early out.
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct JupiterQuoteCache {
|
2023-10-09 10:56:45 -07:00
|
|
|
// cache lowest price for each in-out mint pair
|
|
|
|
pub quote_cache: RwLock<HashMap<(Pubkey, Pubkey), JupiterQuoteCacheEntry>>,
|
2023-07-03 05:09:11 -07:00
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
impl JupiterQuoteCache {
|
2023-10-09 10:56:45 -07:00
|
|
|
fn cache_entry(&self, input_mint: Pubkey, output_mint: Pubkey) -> JupiterQuoteCacheEntry {
|
|
|
|
let mut quote_cache = self.quote_cache.write().unwrap();
|
|
|
|
quote_cache
|
|
|
|
.entry((input_mint, output_mint))
|
|
|
|
.or_insert_with(|| JupiterQuoteCacheEntry {
|
|
|
|
price: Arc::new(tokio::sync::Mutex::new(f64::MAX)),
|
|
|
|
})
|
|
|
|
.clone()
|
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
/// Quotes. Returns BadPrice if the cache or quote returns a price above max_in_per_out_price.
|
|
|
|
pub async fn quote(
|
|
|
|
&self,
|
|
|
|
client: &MangoClient,
|
|
|
|
input_mint: Pubkey,
|
|
|
|
output_mint: Pubkey,
|
|
|
|
input_amount: u64,
|
|
|
|
slippage_bps: u64,
|
|
|
|
version: jupiter::Version,
|
|
|
|
max_in_per_out_price: f64,
|
|
|
|
) -> anyhow::Result<JupiterQuoteCacheResult<(f64, jupiter::Quote)>> {
|
2023-10-09 10:56:45 -07:00
|
|
|
let cache_entry = self.cache_entry(input_mint, output_mint);
|
|
|
|
|
|
|
|
let held_lock = {
|
|
|
|
let cached_price_lock = cache_entry.price.lock().await;
|
|
|
|
|
|
|
|
if *cached_price_lock == f64::MAX {
|
|
|
|
// If we're the first quote for this pair, run the quote while holding the lock:
|
|
|
|
// we don't want multiple parallel requests to go out that will all potentially
|
|
|
|
// tell us about the same poor price.
|
|
|
|
Some(cached_price_lock)
|
|
|
|
} else {
|
|
|
|
// If a cached price exists, check it against the max
|
|
|
|
if *cached_price_lock > max_in_per_out_price {
|
|
|
|
return Ok(JupiterQuoteCacheResult::BadPrice(*cached_price_lock));
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
2023-10-09 10:56:45 -07:00
|
|
|
|
|
|
|
// Don't hold the lock, parallel requests are ok!
|
|
|
|
None
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
2023-10-09 10:56:45 -07:00
|
|
|
};
|
2023-08-14 03:04:17 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
let (price, quote) = self
|
2023-10-09 10:56:45 -07:00
|
|
|
.quote_inner(
|
2023-10-07 12:27:19 -07:00
|
|
|
client,
|
|
|
|
input_mint,
|
|
|
|
output_mint,
|
|
|
|
input_amount,
|
|
|
|
slippage_bps,
|
|
|
|
version,
|
|
|
|
)
|
|
|
|
.await?;
|
2023-10-09 10:56:45 -07:00
|
|
|
|
|
|
|
{
|
|
|
|
let mut cached_price_lock = if let Some(lock) = held_lock {
|
|
|
|
lock
|
|
|
|
} else {
|
|
|
|
cache_entry.price.lock().await
|
|
|
|
};
|
|
|
|
if price < *cached_price_lock {
|
|
|
|
*cached_price_lock = price;
|
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
2023-08-14 03:04:17 -07:00
|
|
|
|
2023-10-09 10:56:45 -07:00
|
|
|
Ok(if price > max_in_per_out_price {
|
|
|
|
JupiterQuoteCacheResult::BadPrice(price)
|
|
|
|
} else {
|
|
|
|
JupiterQuoteCacheResult::Quote((price, quote))
|
|
|
|
})
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
2023-08-14 03:04:17 -07:00
|
|
|
|
2023-10-09 10:56:45 -07:00
|
|
|
async fn quote_inner(
|
2023-10-07 12:27:19 -07:00
|
|
|
&self,
|
|
|
|
client: &MangoClient,
|
|
|
|
input_mint: Pubkey,
|
|
|
|
output_mint: Pubkey,
|
|
|
|
input_amount: u64,
|
|
|
|
slippage_bps: u64,
|
|
|
|
version: jupiter::Version,
|
|
|
|
) -> anyhow::Result<(f64, jupiter::Quote)> {
|
|
|
|
let quote = client
|
|
|
|
.jupiter()
|
|
|
|
.quote(
|
|
|
|
input_mint,
|
|
|
|
output_mint,
|
|
|
|
input_amount,
|
|
|
|
slippage_bps,
|
|
|
|
false,
|
|
|
|
version,
|
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
let quote_price = quote.in_amount as f64 / quote.out_amount as f64;
|
2023-10-09 10:56:45 -07:00
|
|
|
Ok((quote_price, quote))
|
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
|
2023-10-09 10:56:45 -07:00
|
|
|
async fn unchecked_quote(
|
|
|
|
&self,
|
|
|
|
client: &MangoClient,
|
|
|
|
input_mint: Pubkey,
|
|
|
|
output_mint: Pubkey,
|
|
|
|
input_amount: u64,
|
|
|
|
slippage_bps: u64,
|
|
|
|
version: jupiter::Version,
|
|
|
|
) -> anyhow::Result<(f64, jupiter::Quote)> {
|
|
|
|
match self
|
|
|
|
.quote(
|
|
|
|
client,
|
|
|
|
input_mint,
|
|
|
|
output_mint,
|
|
|
|
input_amount,
|
|
|
|
slippage_bps,
|
|
|
|
version,
|
|
|
|
f64::MAX,
|
|
|
|
)
|
|
|
|
.await?
|
|
|
|
{
|
|
|
|
JupiterQuoteCacheResult::Quote(v) => Ok(v),
|
|
|
|
_ => anyhow::bail!("unreachable case in unchecked_quote"),
|
2023-08-14 03:04:17 -07:00
|
|
|
}
|
|
|
|
}
|
2023-08-11 03:08:34 -07:00
|
|
|
|
2023-10-09 10:56:45 -07:00
|
|
|
async fn cached_price(&self, input_mint: Pubkey, output_mint: Pubkey) -> Option<f64> {
|
2023-10-07 12:27:19 -07:00
|
|
|
if input_mint == output_mint {
|
|
|
|
return Some(1.0);
|
|
|
|
}
|
2023-08-11 03:08:34 -07:00
|
|
|
|
2023-10-09 10:56:45 -07:00
|
|
|
let cache_entry = self.cache_entry(input_mint, output_mint);
|
|
|
|
let cached_price = *cache_entry.price.lock().await;
|
|
|
|
if cached_price != f64::MAX {
|
|
|
|
Some(cached_price)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Quotes collateral -> buy and sell -> collateral swaps
|
|
|
|
///
|
|
|
|
/// Returns BadPrice if the full route (cached or queried) exceeds the max_sell_per_buy_price.
|
|
|
|
///
|
|
|
|
/// Returns Quote((sell_per_buy price, collateral->buy quote, sell->collateral quote)) on success
|
|
|
|
pub async fn quote_collateral_swap(
|
|
|
|
&self,
|
|
|
|
client: &MangoClient,
|
|
|
|
collateral_mint: Pubkey,
|
|
|
|
buy_mint: Pubkey,
|
|
|
|
sell_mint: Pubkey,
|
|
|
|
collateral_amount: u64,
|
|
|
|
sell_amount: u64,
|
|
|
|
slippage_bps: u64,
|
|
|
|
version: jupiter::Version,
|
|
|
|
max_sell_per_buy_price: f64,
|
|
|
|
) -> anyhow::Result<
|
|
|
|
JupiterQuoteCacheResult<(f64, Option<jupiter::Quote>, Option<jupiter::Quote>)>,
|
|
|
|
> {
|
|
|
|
// First check if we have cached prices for both legs and
|
|
|
|
// if those break the specified limit
|
2023-10-09 10:56:45 -07:00
|
|
|
let cached_collateral_to_buy = self.cached_price(collateral_mint, buy_mint).await;
|
|
|
|
let cached_sell_to_collateral = self.cached_price(sell_mint, collateral_mint).await;
|
2023-11-08 00:51:36 -08:00
|
|
|
if let (Some(c_to_b), Some(s_to_c)) = (cached_collateral_to_buy, cached_sell_to_collateral)
|
|
|
|
{
|
|
|
|
let s_to_b = s_to_c * c_to_b;
|
|
|
|
if s_to_b > max_sell_per_buy_price {
|
|
|
|
return Ok(JupiterQuoteCacheResult::BadPrice(s_to_b));
|
2023-08-11 03:08:34 -07:00
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get fresh quotes
|
|
|
|
let collateral_to_buy_quote;
|
|
|
|
let collateral_per_buy_price;
|
|
|
|
if collateral_mint != buy_mint {
|
|
|
|
let (buy_price, buy_quote) = self
|
|
|
|
.unchecked_quote(
|
|
|
|
client,
|
|
|
|
collateral_mint,
|
|
|
|
buy_mint,
|
|
|
|
collateral_amount,
|
|
|
|
slippage_bps,
|
|
|
|
version,
|
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
collateral_per_buy_price = buy_price;
|
|
|
|
collateral_to_buy_quote = Some(buy_quote);
|
|
|
|
} else {
|
|
|
|
collateral_per_buy_price = 1.0;
|
|
|
|
collateral_to_buy_quote = None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let sell_to_collateral_quote;
|
|
|
|
let sell_per_collateral_price;
|
|
|
|
if collateral_mint != sell_mint {
|
|
|
|
let (sell_price, sell_quote) = self
|
|
|
|
.unchecked_quote(
|
|
|
|
client,
|
|
|
|
sell_mint,
|
|
|
|
collateral_mint,
|
|
|
|
sell_amount,
|
|
|
|
slippage_bps,
|
|
|
|
version,
|
|
|
|
)
|
|
|
|
.await?;
|
|
|
|
sell_per_collateral_price = sell_price;
|
|
|
|
sell_to_collateral_quote = Some(sell_quote);
|
|
|
|
} else {
|
|
|
|
sell_per_collateral_price = 1.0;
|
|
|
|
sell_to_collateral_quote = None;
|
2023-08-11 03:08:34 -07:00
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
|
|
|
|
// Check price limit on the new quotes
|
|
|
|
let price = sell_per_collateral_price * collateral_per_buy_price;
|
|
|
|
if price > max_sell_per_buy_price {
|
|
|
|
return Ok(JupiterQuoteCacheResult::BadPrice(price));
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(JupiterQuoteCacheResult::Quote((
|
|
|
|
price,
|
|
|
|
collateral_to_buy_quote,
|
|
|
|
sell_to_collateral_quote,
|
|
|
|
)))
|
|
|
|
}
|
2023-07-03 05:09:11 -07:00
|
|
|
}
|
2023-08-18 06:36:02 -07:00
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
struct PreparedExecution {
|
|
|
|
pubkey: Pubkey,
|
|
|
|
tcs_id: u64,
|
|
|
|
volume: u64,
|
|
|
|
token_indexes: Vec<TokenIndex>,
|
|
|
|
max_buy_token_to_liqee: u64,
|
|
|
|
max_sell_token_to_liqor: u64,
|
2023-10-07 12:27:19 -07:00
|
|
|
min_buy_token: u64,
|
|
|
|
min_taker_price: f32,
|
|
|
|
jupiter_quote: Option<jupiter::Quote>,
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
struct PreparationResult {
|
|
|
|
pubkey: Pubkey,
|
|
|
|
pending_volume: u64,
|
|
|
|
prepared: anyhow::Result<Option<PreparedExecution>>,
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct Context {
|
|
|
|
pub mango_client: Arc<MangoClient>,
|
|
|
|
pub account_fetcher: Arc<chain_data::AccountFetcher>,
|
|
|
|
|
|
|
|
/// Information about current token prices is used to reject tcs early
|
|
|
|
/// that are very likely to not be executable profitably.
|
|
|
|
pub token_swap_info: Arc<token_swap_info::TokenSwapInfoUpdater>,
|
|
|
|
|
|
|
|
/// Cache jupiter prices. Sometimes a lot of tcs look potentially interesting at the same time.
|
|
|
|
/// To avoid spamming the jupiter API for each one, cache the returned prices and don't query again
|
|
|
|
/// if we are likely to get a result that would lead to us not wanting to trigger the tcs.
|
|
|
|
pub jupiter_quote_cache: Arc<JupiterQuoteCache>,
|
|
|
|
|
|
|
|
pub config: Config,
|
|
|
|
pub now_ts: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Context {
|
|
|
|
fn token_bank_price_mint(
|
|
|
|
&self,
|
|
|
|
token_index: TokenIndex,
|
|
|
|
) -> anyhow::Result<(Bank, I80F48, Pubkey)> {
|
2023-12-05 04:22:24 -08:00
|
|
|
let info = self.mango_client.context.token(token_index);
|
2023-10-07 12:27:19 -07:00
|
|
|
let (bank, price) = self
|
|
|
|
.account_fetcher
|
|
|
|
.fetch_bank_and_price(&info.first_bank())?;
|
|
|
|
Ok((bank, price, info.mint))
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
fn tcs_has_plausible_price(
|
|
|
|
&self,
|
|
|
|
tcs: &TokenConditionalSwap,
|
|
|
|
base_price: f64,
|
|
|
|
) -> anyhow::Result<bool> {
|
|
|
|
// The premium the taker receives needs to take taker fees into account
|
2023-11-08 00:51:36 -08:00
|
|
|
let taker_price = tcs.taker_price(tcs.premium_price(base_price, self.now_ts));
|
2023-10-07 12:27:19 -07:00
|
|
|
|
|
|
|
// Never take tcs where the fee exceeds the premium and the triggerer exchanges
|
|
|
|
// tokens at below oracle price.
|
|
|
|
if taker_price < base_price {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
let buy_info = self
|
|
|
|
.token_swap_info
|
|
|
|
.swap_info(tcs.buy_token_index)
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("no swap info for token {}", tcs.buy_token_index))?;
|
|
|
|
let sell_info = self
|
|
|
|
.token_swap_info
|
|
|
|
.swap_info(tcs.sell_token_index)
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("no swap info for token {}", tcs.sell_token_index))?;
|
|
|
|
|
|
|
|
// If this is 1.0 then the exchange can (probably) happen at oracle price.
|
|
|
|
// 1.5 would mean we need to pay 50% more than oracle etc.
|
|
|
|
let cost_over_oracle = buy_info.buy_over_oracle() * sell_info.sell_over_oracle();
|
|
|
|
|
|
|
|
Ok(taker_price >= base_price * cost_over_oracle * (1.0 + self.config.profit_fraction))
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
|
2024-02-19 01:20:12 -08:00
|
|
|
// excluded by config
|
|
|
|
fn tcs_pair_is_allowed(
|
|
|
|
&self,
|
|
|
|
buy_token_index: TokenIndex,
|
|
|
|
sell_token_index: TokenIndex,
|
|
|
|
) -> bool {
|
|
|
|
if self.config.forbidden_tokens.contains(&buy_token_index) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.forbidden_tokens.contains(&sell_token_index) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.only_allowed_tokens.is_empty() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.only_allowed_tokens.contains(&buy_token_index) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.only_allowed_tokens.contains(&sell_token_index) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
// Either expired or triggerable with ok-looking price.
|
|
|
|
fn tcs_is_interesting(&self, tcs: &TokenConditionalSwap) -> anyhow::Result<bool> {
|
|
|
|
if tcs.is_expired(self.now_ts) {
|
|
|
|
return Ok(true);
|
|
|
|
}
|
2024-02-19 01:20:12 -08:00
|
|
|
if !self.tcs_pair_is_allowed(tcs.buy_token_index, tcs.buy_token_index) {
|
|
|
|
return Ok(false);
|
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
|
|
|
|
let (_, buy_token_price, _) = self.token_bank_price_mint(tcs.buy_token_index)?;
|
|
|
|
let (_, sell_token_price, _) = self.token_bank_price_mint(tcs.sell_token_index)?;
|
|
|
|
let base_price = (buy_token_price / sell_token_price).to_num();
|
|
|
|
|
|
|
|
Ok(tcs.is_triggerable(base_price, self.now_ts)
|
|
|
|
&& self.tcs_has_plausible_price(tcs, base_price)?)
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
/// Returns the maximum execution size of a tcs order in quote units
|
|
|
|
pub fn tcs_max_volume(
|
|
|
|
&self,
|
|
|
|
account: &MangoAccountValue,
|
|
|
|
tcs: &TokenConditionalSwap,
|
|
|
|
) -> anyhow::Result<Option<u64>> {
|
|
|
|
let (_, buy_token_price, _) = self.token_bank_price_mint(tcs.buy_token_index)?;
|
|
|
|
let (_, sell_token_price, _) = self.token_bank_price_mint(tcs.sell_token_index)?;
|
2023-08-18 06:36:02 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
let (max_buy, max_sell) = match self.tcs_max_liqee_execution(account, tcs)? {
|
2023-08-18 06:36:02 -07:00
|
|
|
Some(v) => v,
|
|
|
|
None => return Ok(None),
|
|
|
|
};
|
2023-10-07 12:27:19 -07:00
|
|
|
|
|
|
|
let max_quote = (I80F48::from(max_buy) * buy_token_price)
|
|
|
|
.min(I80F48::from(max_sell) * sell_token_price);
|
|
|
|
|
|
|
|
Ok(Some(max_quote.floor().clamp_to_u64()))
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
/// Compute the max viable swap for liqee
|
|
|
|
/// This includes
|
|
|
|
/// - tcs restrictions (remaining buy/sell, create borrows/deposits)
|
|
|
|
/// - reduce only banks
|
2023-12-05 06:43:38 -08:00
|
|
|
/// - net borrow limits:
|
|
|
|
/// - the account may borrow the sell token (and the liqor side may not be a repay)
|
|
|
|
/// - the liqor may borrow the buy token (and the account side may not be a repay)
|
|
|
|
/// this is technically a liqor limitation: the liqor could acquire the token before trying the
|
|
|
|
/// execution... but in practice the liqor may work on margin
|
|
|
|
/// - deposit limits:
|
|
|
|
/// - the account may deposit the buy token (while the liqor borrowed it)
|
|
|
|
/// - the liqor may deposit the sell token (while the account borrowed it)
|
2023-10-07 12:27:19 -07:00
|
|
|
///
|
|
|
|
/// Returns Some((native buy amount, native sell amount)) if execution is sensible
|
2023-12-05 06:43:38 -08:00
|
|
|
/// Returns None if the execution should be skipped (due to limits)
|
2023-10-07 12:27:19 -07:00
|
|
|
pub fn tcs_max_liqee_execution(
|
|
|
|
&self,
|
|
|
|
account: &MangoAccountValue,
|
|
|
|
tcs: &TokenConditionalSwap,
|
|
|
|
) -> anyhow::Result<Option<(u64, u64)>> {
|
|
|
|
let (buy_bank, buy_token_price, _) = self.token_bank_price_mint(tcs.buy_token_index)?;
|
|
|
|
let (sell_bank, sell_token_price, _) = self.token_bank_price_mint(tcs.sell_token_index)?;
|
|
|
|
|
|
|
|
let base_price = buy_token_price / sell_token_price;
|
|
|
|
let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts);
|
|
|
|
let maker_price = tcs.maker_price(premium_price);
|
|
|
|
|
2023-12-05 06:43:38 -08:00
|
|
|
let liqee_buy_position = account
|
2023-10-07 12:27:19 -07:00
|
|
|
.token_position(tcs.buy_token_index)
|
|
|
|
.map(|p| p.native(&buy_bank))
|
|
|
|
.unwrap_or(I80F48::ZERO);
|
2023-12-05 06:43:38 -08:00
|
|
|
let liqee_sell_position = account
|
2023-10-07 12:27:19 -07:00
|
|
|
.token_position(tcs.sell_token_index)
|
|
|
|
.map(|p| p.native(&sell_bank))
|
|
|
|
.unwrap_or(I80F48::ZERO);
|
|
|
|
|
|
|
|
// this is in "buy token received per sell token given" units
|
|
|
|
let swap_price = I80F48::from_num((1.0 - SLIPPAGE_BUFFER) / maker_price);
|
2023-12-05 06:43:38 -08:00
|
|
|
let max_sell_ignoring_limits = util::max_swap_source_ignoring_limits(
|
2023-10-07 12:27:19 -07:00
|
|
|
&self.mango_client,
|
|
|
|
&self.account_fetcher,
|
2023-11-08 00:51:36 -08:00
|
|
|
account,
|
2023-10-07 12:27:19 -07:00
|
|
|
tcs.sell_token_index,
|
|
|
|
tcs.buy_token_index,
|
|
|
|
swap_price,
|
|
|
|
I80F48::ZERO,
|
|
|
|
)?
|
|
|
|
.floor()
|
|
|
|
.to_num::<u64>()
|
2023-12-05 06:43:38 -08:00
|
|
|
.min(tcs.max_sell_for_position(liqee_sell_position, &sell_bank));
|
2023-10-07 12:27:19 -07:00
|
|
|
|
2023-12-05 06:43:38 -08:00
|
|
|
let max_buy_ignoring_limits = tcs.max_buy_for_position(liqee_buy_position, &buy_bank);
|
2023-10-07 12:27:19 -07:00
|
|
|
|
2023-12-05 06:43:38 -08:00
|
|
|
// What follows is a complex manual handling of net borrow/deposit limits, for
|
|
|
|
// the following reason:
|
2023-10-07 12:27:19 -07:00
|
|
|
// Usually, we want to execute tcs even for small amounts because that will close the
|
|
|
|
// tcs order: either due to full execution or due to the health threshold being reached.
|
|
|
|
//
|
2023-12-05 06:43:38 -08:00
|
|
|
// However, when the limits are hit, it will not closed when no further execution
|
|
|
|
// is possible, because limit issues are transient. Furthermore, we don't want to send
|
|
|
|
// tiny tcs trigger transactions, because there's a good chance we would then be sending
|
|
|
|
// lot of those as oracle prices fluctuate.
|
2023-10-07 12:27:19 -07:00
|
|
|
//
|
|
|
|
// Thus, we need to detect if the possible execution amount is tiny _because_ of the
|
2023-12-05 06:43:38 -08:00
|
|
|
// limits. Then skip. If it's tiny for other reasons we can proceed.
|
2023-10-07 12:27:19 -07:00
|
|
|
|
2023-12-05 06:43:38 -08:00
|
|
|
// Do the liqor buy tokens come from deposits or are they borrowed?
|
|
|
|
let mut liqor_buy_borrows = match self.config.mode {
|
2023-10-07 12:27:19 -07:00
|
|
|
Mode::BorrowBuyToken => {
|
|
|
|
// Assume that the liqor has enough buy token if it's collateral
|
|
|
|
if tcs.buy_token_index == self.config.collateral_token_index {
|
|
|
|
0
|
|
|
|
} else {
|
2023-12-05 06:43:38 -08:00
|
|
|
max_buy_ignoring_limits
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Mode::SwapCollateralIntoBuy { .. } => 0,
|
|
|
|
Mode::SwapSellIntoBuy { .. } => {
|
|
|
|
// Never needs buy borrows.
|
|
|
|
// This might need extra sell borrows, but falls back onto SwapCollateralIntoBuy if needed
|
|
|
|
0
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-12-05 06:43:38 -08:00
|
|
|
// First, net borrow limits
|
|
|
|
let max_sell_net_borrows;
|
|
|
|
let max_buy_net_borrows;
|
|
|
|
{
|
|
|
|
fn available_borrows(bank: &Bank, price: I80F48) -> u64 {
|
|
|
|
bank.remaining_net_borrows_quote(price)
|
|
|
|
.saturating_div(price)
|
|
|
|
.clamp_to_u64()
|
|
|
|
}
|
|
|
|
let available_buy_borrows = available_borrows(&buy_bank, buy_token_price);
|
|
|
|
let available_sell_borrows = available_borrows(&sell_bank, sell_token_price);
|
|
|
|
|
|
|
|
// New borrows if max_sell_ignoring_limits was withdrawn on the liqee
|
|
|
|
// We assume that on the liqor side the position is >= 0, so these are true
|
|
|
|
// new borrows.
|
|
|
|
let sell_borrows = (I80F48::from(max_sell_ignoring_limits)
|
|
|
|
- liqee_sell_position.max(I80F48::ZERO))
|
|
|
|
.ceil()
|
|
|
|
.clamp_to_u64();
|
|
|
|
|
|
|
|
// On the buy side, the liqor might need to borrow, see liqor_buy_borrows.
|
|
|
|
// On the liqee side, the bought tokens may repay a borrow, reducing net borrows again
|
|
|
|
let buy_borrows = (I80F48::from(liqor_buy_borrows)
|
|
|
|
+ liqee_buy_position.min(I80F48::ZERO))
|
|
|
|
.ceil()
|
|
|
|
.clamp_to_u64();
|
|
|
|
|
|
|
|
// New maximums adjusted for net borrow limits
|
|
|
|
max_sell_net_borrows = max_sell_ignoring_limits
|
|
|
|
- (sell_borrows - sell_borrows.min(available_sell_borrows));
|
|
|
|
max_buy_net_borrows =
|
|
|
|
max_buy_ignoring_limits - (buy_borrows - buy_borrows.min(available_buy_borrows));
|
|
|
|
liqor_buy_borrows = liqor_buy_borrows.min(max_buy_net_borrows);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Second, deposit limits
|
|
|
|
let max_sell;
|
|
|
|
let max_buy;
|
|
|
|
{
|
|
|
|
let available_buy_deposits = buy_bank.remaining_deposits_until_limit().clamp_to_u64();
|
|
|
|
let available_sell_deposits = sell_bank.remaining_deposits_until_limit().clamp_to_u64();
|
|
|
|
|
|
|
|
// New deposits on the liqee side (reduced by repaid borrows)
|
|
|
|
let liqee_buy_deposits = (I80F48::from(max_buy_net_borrows)
|
|
|
|
+ liqee_buy_position.min(I80F48::ZERO))
|
|
|
|
.ceil()
|
|
|
|
.clamp_to_u64();
|
|
|
|
// the net new deposits can only be as big as the liqor borrows
|
|
|
|
// (assume no borrows, then deposits only move from liqor to liqee)
|
|
|
|
let buy_deposits = liqee_buy_deposits.min(liqor_buy_borrows);
|
|
|
|
|
|
|
|
// We assume the liqor position is always >= 0, meaning there are new sell token deposits if
|
|
|
|
// the sell token gets borrowed on the liqee side.
|
|
|
|
let sell_deposits = (I80F48::from(max_sell_net_borrows)
|
|
|
|
- liqee_sell_position.max(I80F48::ZERO))
|
|
|
|
.ceil()
|
|
|
|
.clamp_to_u64();
|
|
|
|
|
|
|
|
max_sell =
|
|
|
|
max_sell_net_borrows - (sell_deposits - sell_deposits.min(available_sell_deposits));
|
|
|
|
max_buy =
|
|
|
|
max_buy_net_borrows - (buy_deposits - buy_deposits.min(available_buy_deposits));
|
|
|
|
}
|
|
|
|
|
|
|
|
let tiny_due_to_limits = {
|
|
|
|
let buy_threshold = I80F48::from(EXECUTION_THRESHOLD) / buy_token_price;
|
|
|
|
let sell_threshold = I80F48::from(EXECUTION_THRESHOLD) / sell_token_price;
|
|
|
|
max_buy < buy_threshold && max_buy_ignoring_limits > buy_threshold
|
|
|
|
|| max_sell < sell_threshold && max_sell_ignoring_limits > sell_threshold
|
2023-10-07 12:27:19 -07:00
|
|
|
};
|
2023-12-05 06:43:38 -08:00
|
|
|
if tiny_due_to_limits {
|
2023-10-07 12:27:19 -07:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Some((max_buy, max_sell)))
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn find_interesting_tcs_for_account(
|
|
|
|
&self,
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
) -> anyhow::Result<Vec<anyhow::Result<(Pubkey, u64, u64)>>> {
|
|
|
|
let liqee = self.account_fetcher.fetch_mango_account(pubkey)?;
|
|
|
|
|
2023-10-09 10:56:45 -07:00
|
|
|
let interesting_tcs = liqee
|
|
|
|
.active_token_conditional_swaps()
|
|
|
|
.filter_map(|tcs| {
|
|
|
|
match self.tcs_is_interesting(tcs) {
|
|
|
|
Ok(true) => {
|
|
|
|
// Filter out Ok(None) resuts of tcs that shouldn't be executed right now
|
|
|
|
match self.tcs_max_volume(&liqee, tcs) {
|
|
|
|
Ok(Some(v)) => Some(Ok((*pubkey, tcs.id, v))),
|
|
|
|
Ok(None) => None,
|
|
|
|
Err(e) => Some(Err(e)),
|
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
2023-10-09 10:56:45 -07:00
|
|
|
Ok(false) => None,
|
|
|
|
Err(e) => Some(Err(e)),
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
2023-10-09 10:56:45 -07:00
|
|
|
})
|
|
|
|
.collect_vec();
|
|
|
|
if !interesting_tcs.is_empty() {
|
|
|
|
trace!(%pubkey, interesting_tcs_count=interesting_tcs.len(), "found interesting tcs");
|
|
|
|
}
|
|
|
|
Ok(interesting_tcs)
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
2023-10-09 10:56:45 -07:00
|
|
|
#[instrument(skip_all, fields(%pubkey, %tcs_id))]
|
2023-10-07 12:27:19 -07:00
|
|
|
async fn prepare_token_conditional_swap(
|
|
|
|
&self,
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
tcs_id: u64,
|
|
|
|
) -> anyhow::Result<Option<PreparedExecution>> {
|
|
|
|
let now_ts: u64 = std::time::SystemTime::now()
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)?
|
2023-11-08 00:51:36 -08:00
|
|
|
.as_secs();
|
2023-10-07 12:27:19 -07:00
|
|
|
let liqee = self.account_fetcher.fetch_mango_account(pubkey)?;
|
|
|
|
let tcs = liqee.token_conditional_swap_by_id(tcs_id)?.1;
|
|
|
|
|
|
|
|
if tcs.is_expired(now_ts) {
|
2023-10-09 10:56:45 -07:00
|
|
|
trace!("tcs is expired");
|
2023-10-07 12:27:19 -07:00
|
|
|
// Triggering like this will close the expired tcs and not affect the liqor
|
|
|
|
Ok(Some(PreparedExecution {
|
|
|
|
pubkey: *pubkey,
|
|
|
|
tcs_id,
|
|
|
|
volume: 0,
|
|
|
|
token_indexes: vec![],
|
|
|
|
max_buy_token_to_liqee: 0,
|
|
|
|
max_sell_token_to_liqor: 0,
|
|
|
|
min_buy_token: 0,
|
|
|
|
min_taker_price: 0.0,
|
|
|
|
jupiter_quote: None,
|
|
|
|
}))
|
|
|
|
} else {
|
|
|
|
self.prepare_token_conditional_swap_inner(pubkey, &liqee, tcs.id)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
async fn prepare_token_conditional_swap_inner(
|
|
|
|
&self,
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
liqee_old: &MangoAccountValue,
|
|
|
|
tcs_id: u64,
|
|
|
|
) -> anyhow::Result<Option<PreparedExecution>> {
|
2024-01-23 08:26:31 -08:00
|
|
|
let health_cache = self
|
|
|
|
.mango_client
|
|
|
|
.health_cache(liqee_old)
|
2023-10-07 12:27:19 -07:00
|
|
|
.await
|
|
|
|
.context("creating health cache 1")?;
|
|
|
|
if health_cache.is_liquidatable() {
|
2023-10-09 10:56:45 -07:00
|
|
|
trace!("account is liquidatable (pre-fetch)");
|
2023-10-07 12:27:19 -07:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
// get a fresh account and re-check the tcs and health
|
|
|
|
let liqee = self
|
|
|
|
.account_fetcher
|
|
|
|
.fetch_fresh_mango_account(pubkey)
|
2023-08-24 07:45:01 -07:00
|
|
|
.await?;
|
2023-10-07 12:27:19 -07:00
|
|
|
let (_, tcs) = liqee.token_conditional_swap_by_id(tcs_id)?;
|
|
|
|
if tcs.is_expired(self.now_ts) || !self.tcs_is_interesting(tcs)? {
|
2023-10-09 10:56:45 -07:00
|
|
|
trace!("tcs is expired or uninteresting");
|
2023-10-07 12:27:19 -07:00
|
|
|
return Ok(None);
|
|
|
|
}
|
2023-08-24 07:45:01 -07:00
|
|
|
|
2024-01-23 08:26:31 -08:00
|
|
|
let health_cache = self
|
|
|
|
.mango_client
|
|
|
|
.health_cache(&liqee)
|
2023-10-07 12:27:19 -07:00
|
|
|
.await
|
|
|
|
.context("creating health cache 2")?;
|
|
|
|
if health_cache.is_liquidatable() {
|
2023-10-09 10:56:45 -07:00
|
|
|
trace!("account is liquidatable (post-fetch)");
|
2023-10-07 12:27:19 -07:00
|
|
|
return Ok(None);
|
|
|
|
}
|
2023-08-18 06:36:02 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
self.prepare_token_conditional_swap_inner2(pubkey, &liqee, tcs)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
async fn prepare_token_conditional_swap_inner2(
|
|
|
|
&self,
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
liqee: &MangoAccountValue,
|
|
|
|
tcs: &TokenConditionalSwap,
|
|
|
|
) -> anyhow::Result<Option<PreparedExecution>> {
|
|
|
|
let liqor_min_health_ratio = I80F48::from_num(self.config.min_health_ratio);
|
|
|
|
|
|
|
|
// Compute the max viable swap (for liqor and liqee) and min it
|
|
|
|
let (buy_bank, buy_token_price, buy_mint) =
|
|
|
|
self.token_bank_price_mint(tcs.buy_token_index)?;
|
|
|
|
let (sell_bank, sell_token_price, sell_mint) =
|
|
|
|
self.token_bank_price_mint(tcs.sell_token_index)?;
|
|
|
|
|
|
|
|
let base_price = buy_token_price / sell_token_price;
|
|
|
|
let premium_price = tcs.premium_price(base_price.to_num(), self.now_ts);
|
|
|
|
let taker_price = I80F48::from_num(tcs.taker_price(premium_price));
|
|
|
|
|
|
|
|
let max_take_quote = I80F48::from(self.config.max_trigger_quote_amount);
|
|
|
|
|
|
|
|
let (liqee_max_buy, liqee_max_sell) = match self.tcs_max_liqee_execution(liqee, tcs)? {
|
|
|
|
Some(v) => v,
|
2023-10-09 10:56:45 -07:00
|
|
|
None => {
|
|
|
|
trace!("no liqee execution possible");
|
|
|
|
return Ok(None);
|
|
|
|
}
|
2023-10-07 12:27:19 -07:00
|
|
|
};
|
|
|
|
let max_sell_token_to_liqor = liqee_max_sell;
|
|
|
|
|
|
|
|
// In addition to the liqee's requirements, the liqor also has requirement,
|
|
|
|
// because it might not have enough buy token:
|
|
|
|
// - if taking by borrowing buy token or swapping sell into buy:
|
|
|
|
// - respect the min_health_ratio
|
|
|
|
// - possible net borrow limit restrictions from the liqor borrowing the token
|
|
|
|
// - if taking by buying buy token with collateral:
|
|
|
|
// - limit by current current collateral
|
|
|
|
// - liqor has a defined max_take_quote
|
|
|
|
|
|
|
|
// Mode fallback?
|
|
|
|
let mode = match self.config.mode.clone() {
|
|
|
|
Mode::SwapSellIntoBuy => {
|
|
|
|
// This mode falls back on another when sell token borrows are impossible
|
|
|
|
// or too limited
|
|
|
|
let available_quote = sell_bank.remaining_net_borrows_quote(sell_token_price);
|
|
|
|
// Note that needed_quote does not account for an existing sell token balance:
|
|
|
|
// That is fine - if sell token is the collateral, we can just use the collateral
|
|
|
|
// based mode and the result will be the same.
|
|
|
|
let needed_quote = (I80F48::from(max_sell_token_to_liqor) * sell_token_price)
|
|
|
|
.min(I80F48::from(liqee_max_buy) * buy_token_price)
|
|
|
|
.min(max_take_quote);
|
|
|
|
if sell_bank.are_borrows_reduce_only() || needed_quote > available_quote {
|
|
|
|
Mode::SwapCollateralIntoBuy
|
|
|
|
} else {
|
|
|
|
Mode::SwapSellIntoBuy
|
|
|
|
}
|
|
|
|
}
|
2023-11-08 00:51:36 -08:00
|
|
|
m => m,
|
2023-10-07 12:27:19 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
let jupiter_slippage_fraction = 1.0 - self.config.jupiter_slippage_bps as f64 * 0.0001;
|
|
|
|
|
|
|
|
let mut liqor = self.mango_client.mango_account().await?;
|
|
|
|
|
|
|
|
let collateral_token_index = self.config.collateral_token_index;
|
|
|
|
let liqor_existing_buy_token = liqor
|
|
|
|
.ensure_token_position(tcs.buy_token_index)?
|
|
|
|
.0
|
|
|
|
.native(&buy_bank);
|
|
|
|
let liqor_available_buy_token = match mode {
|
2023-12-05 06:43:38 -08:00
|
|
|
Mode::BorrowBuyToken => util::max_swap_source_with_limits(
|
2023-10-07 12:27:19 -07:00
|
|
|
&self.mango_client,
|
|
|
|
&self.account_fetcher,
|
|
|
|
&liqor,
|
|
|
|
tcs.buy_token_index,
|
|
|
|
tcs.sell_token_index,
|
|
|
|
taker_price,
|
|
|
|
liqor_min_health_ratio,
|
|
|
|
)?,
|
|
|
|
Mode::SwapCollateralIntoBuy => {
|
|
|
|
// The transaction will be net-positive, but how much buy token can
|
|
|
|
// the collateral be swapped into?
|
|
|
|
if tcs.buy_token_index == collateral_token_index {
|
|
|
|
liqor_existing_buy_token
|
|
|
|
} else {
|
|
|
|
let (_, collateral_price, _) =
|
|
|
|
self.token_bank_price_mint(collateral_token_index)?;
|
|
|
|
let buy_per_collateral_price = (collateral_price / buy_token_price)
|
|
|
|
* I80F48::from_num(jupiter_slippage_fraction);
|
2023-12-05 06:43:38 -08:00
|
|
|
let collateral_amount = util::max_swap_source_with_limits(
|
2023-10-07 12:27:19 -07:00
|
|
|
&self.mango_client,
|
|
|
|
&self.account_fetcher,
|
|
|
|
&liqor,
|
|
|
|
collateral_token_index,
|
|
|
|
tcs.buy_token_index,
|
|
|
|
buy_per_collateral_price,
|
|
|
|
liqor_min_health_ratio,
|
|
|
|
)?;
|
|
|
|
|
|
|
|
collateral_amount * buy_per_collateral_price
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Mode::SwapSellIntoBuy => {
|
|
|
|
// How big can the sell -> buy swap be?
|
|
|
|
let buy_per_sell_price =
|
|
|
|
(I80F48::from(1) / taker_price) * I80F48::from_num(jupiter_slippage_fraction);
|
2023-12-05 06:43:38 -08:00
|
|
|
let max_sell = util::max_swap_source_with_limits(
|
2023-10-07 12:27:19 -07:00
|
|
|
&self.mango_client,
|
|
|
|
&self.account_fetcher,
|
|
|
|
&liqor,
|
|
|
|
tcs.sell_token_index,
|
|
|
|
tcs.buy_token_index,
|
|
|
|
buy_per_sell_price,
|
|
|
|
liqor_min_health_ratio,
|
|
|
|
)?;
|
|
|
|
max_sell * buy_per_sell_price
|
|
|
|
}
|
|
|
|
};
|
|
|
|
let max_buy_token_to_liqee = liqor_available_buy_token
|
|
|
|
.min(max_take_quote / buy_token_price)
|
|
|
|
.clamp_to_u64()
|
|
|
|
.min(liqee_max_buy);
|
|
|
|
|
|
|
|
if max_sell_token_to_liqor == 0 || max_buy_token_to_liqee == 0 {
|
2023-10-09 10:56:45 -07:00
|
|
|
trace!(
|
|
|
|
liqee_max_buy,
|
|
|
|
liqee_max_sell,
|
|
|
|
max_buy = max_buy_token_to_liqee,
|
|
|
|
max_sell = max_sell_token_to_liqor,
|
|
|
|
"no execution possible"
|
|
|
|
);
|
2023-10-07 12:27:19 -07:00
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
|
|
|
// The quote amount the swap could be at
|
|
|
|
let volume = (I80F48::from(max_buy_token_to_liqee) * buy_token_price)
|
|
|
|
.min(I80F48::from(max_sell_token_to_liqor) * sell_token_price);
|
|
|
|
|
|
|
|
// Final check of the reverse trade on jupiter
|
|
|
|
//
|
|
|
|
// We want swap_at_taker_price * counterswap >= 1 + profit_fraction
|
|
|
|
// so 1/counterswap <= swap_at_taker_price / (1 + profit_fraction)
|
|
|
|
let taker_price_profit = taker_price.to_num::<f64>() / (1.0 + self.config.profit_fraction);
|
|
|
|
let jupiter_quote;
|
|
|
|
let swap_price;
|
|
|
|
let mut bad_price = false;
|
|
|
|
match mode {
|
|
|
|
Mode::BorrowBuyToken | Mode::SwapSellIntoBuy => {
|
|
|
|
// Even if we borrow, we want to check that the rebalance would be profitable
|
|
|
|
let input_amount = volume / sell_token_price;
|
|
|
|
match self
|
|
|
|
.jupiter_quote_cache
|
|
|
|
.quote(
|
|
|
|
&self.mango_client,
|
|
|
|
sell_mint,
|
|
|
|
buy_mint,
|
|
|
|
input_amount.clamp_to_u64(),
|
|
|
|
self.config.jupiter_slippage_bps,
|
|
|
|
self.config.jupiter_version,
|
|
|
|
taker_price_profit,
|
|
|
|
)
|
|
|
|
.await?
|
|
|
|
{
|
|
|
|
JupiterQuoteCacheResult::Quote((price, quote)) => {
|
|
|
|
swap_price = price;
|
|
|
|
|
|
|
|
// Store and execute quote if mode needs it
|
|
|
|
jupiter_quote = (mode == Mode::SwapSellIntoBuy).then_some(quote);
|
|
|
|
}
|
|
|
|
JupiterQuoteCacheResult::BadPrice(price) => {
|
|
|
|
swap_price = price;
|
|
|
|
bad_price = true;
|
|
|
|
jupiter_quote = None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Mode::SwapCollateralIntoBuy => {
|
|
|
|
let (_, collateral_price, collateral_mint) =
|
|
|
|
self.token_bank_price_mint(collateral_token_index)?;
|
|
|
|
|
|
|
|
let max_sell = volume / sell_token_price;
|
|
|
|
let max_buy_collateral_cost = volume / collateral_price;
|
|
|
|
|
|
|
|
// In this mode, we buy the buy token with collateral token before taking the tcs.
|
|
|
|
// Get a quote and store it so it will get executed later.
|
|
|
|
// The quote for sell_token -> collateral is just to check profitability, rebalancing
|
|
|
|
// will take care of it.
|
|
|
|
match self
|
|
|
|
.jupiter_quote_cache
|
|
|
|
.quote_collateral_swap(
|
|
|
|
&self.mango_client,
|
|
|
|
collateral_mint,
|
|
|
|
buy_mint,
|
|
|
|
sell_mint,
|
|
|
|
max_buy_collateral_cost.clamp_to_u64(),
|
|
|
|
max_sell.clamp_to_u64(),
|
|
|
|
self.config.jupiter_slippage_bps,
|
|
|
|
self.config.jupiter_version,
|
|
|
|
taker_price_profit,
|
|
|
|
)
|
|
|
|
.await?
|
|
|
|
{
|
|
|
|
JupiterQuoteCacheResult::Quote((price, collateral_to_buy_quote, _)) => {
|
|
|
|
swap_price = price;
|
|
|
|
jupiter_quote = collateral_to_buy_quote;
|
|
|
|
}
|
|
|
|
JupiterQuoteCacheResult::BadPrice(price) => {
|
|
|
|
swap_price = price;
|
|
|
|
bad_price = true;
|
|
|
|
jupiter_quote = None;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let min_taker_price = (swap_price * (1.0 + self.config.profit_fraction)) as f32;
|
|
|
|
if bad_price || taker_price.to_num::<f32>() < min_taker_price {
|
2023-08-18 06:36:02 -07:00
|
|
|
trace!(
|
|
|
|
max_buy = max_buy_token_to_liqee,
|
|
|
|
max_sell = max_sell_token_to_liqor,
|
|
|
|
jupiter_swap_price = %swap_price,
|
|
|
|
tcs_taker_price = %taker_price,
|
2023-10-07 12:27:19 -07:00
|
|
|
"skipping because swap price isn't good enough compared to trigger price",
|
2023-08-18 06:36:02 -07:00
|
|
|
);
|
|
|
|
return Ok(None);
|
|
|
|
}
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
let min_buy = (volume / buy_token_price).to_num::<f64>() * self.config.min_buy_fraction;
|
2023-08-18 06:36:02 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
trace!(
|
|
|
|
max_buy = max_buy_token_to_liqee,
|
|
|
|
max_sell = max_sell_token_to_liqor,
|
|
|
|
"prepared execution",
|
|
|
|
);
|
2023-08-18 06:36:02 -07:00
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
Ok(Some(PreparedExecution {
|
|
|
|
pubkey: *pubkey,
|
|
|
|
tcs_id: tcs.id,
|
|
|
|
volume: volume.clamp_to_u64(),
|
|
|
|
token_indexes: vec![tcs.buy_token_index, tcs.sell_token_index],
|
|
|
|
max_buy_token_to_liqee,
|
|
|
|
max_sell_token_to_liqor,
|
|
|
|
min_buy_token: min_buy as u64,
|
|
|
|
min_taker_price,
|
|
|
|
jupiter_quote,
|
|
|
|
}))
|
|
|
|
}
|
2023-08-18 06:36:02 -07:00
|
|
|
|
|
|
|
/// Runs tcs jobs in parallel
|
|
|
|
///
|
|
|
|
/// Will run jobs until either the max_trigger_quote_amount is exhausted or
|
|
|
|
/// max_completed jobs have been run while respecting the available free token
|
|
|
|
/// positions on the liqor.
|
|
|
|
///
|
|
|
|
/// It proceeds in two phases:
|
|
|
|
/// - Preparation: Evaluates tcs and collects a set of them to trigger.
|
|
|
|
/// The preparation does things like check jupiter for profitability and
|
|
|
|
/// refetching the account to make sure it's up to date.
|
|
|
|
/// - Execution: Selects the prepared jobs that fit the liqor's available or free
|
|
|
|
/// token positions.
|
|
|
|
///
|
|
|
|
/// Returns a list of transaction signatures as well as the pubkeys of liqees.
|
|
|
|
pub async fn execute_tcs(
|
|
|
|
&self,
|
2023-11-08 00:51:36 -08:00
|
|
|
tcs: &mut [(Pubkey, u64, u64)],
|
2024-03-20 07:25:52 -07:00
|
|
|
error_tracking: Arc<RwLock<ErrorTracking<Pubkey, LiqErrorType>>>,
|
2023-08-18 06:36:02 -07:00
|
|
|
) -> anyhow::Result<(Vec<Signature>, Vec<Pubkey>)> {
|
|
|
|
use rand::distributions::{Distribution, WeightedError, WeightedIndex};
|
|
|
|
|
|
|
|
let max_volume = self.config.max_trigger_quote_amount;
|
|
|
|
let mut pending_volume = 0;
|
|
|
|
let mut prepared_volume = 0;
|
|
|
|
|
|
|
|
let max_prepared = 32;
|
|
|
|
let mut prepared_executions = vec![];
|
|
|
|
|
|
|
|
let mut pending = vec![];
|
|
|
|
let mut no_new_job = false;
|
|
|
|
|
|
|
|
// What goes on below is roughly the following:
|
|
|
|
//
|
|
|
|
// We have a bunch of tcs we want to try executing in `tcs`.
|
|
|
|
// We pick a random ones (weighted by volume) and collect their `pending` jobs.
|
|
|
|
// Once the maximum number of prepared jobs (`max_prepared`) or `max_volume`
|
|
|
|
// for this run is reached, we wait for one of the jobs to finish.
|
|
|
|
// This will either free up the job slot and volume or commit it.
|
|
|
|
// If it freed up a slot, another job can be added to `pending`
|
|
|
|
// If `no_new_job` can be added to `pending`, we also start waiting for completion.
|
|
|
|
while prepared_executions.len() < max_prepared && prepared_volume < max_volume {
|
|
|
|
// If it's impossible to start another job right now, we need to wait
|
|
|
|
// for one to complete (or we're done)
|
|
|
|
if prepared_executions.len() + pending.len() >= max_prepared
|
|
|
|
|| prepared_volume + pending_volume >= max_volume
|
|
|
|
|| no_new_job
|
|
|
|
{
|
|
|
|
if pending.is_empty() {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// select_all to run until one completes
|
|
|
|
let (result, _index, remaining): (PreparationResult, _, _) =
|
|
|
|
futures::future::select_all(pending).await;
|
|
|
|
pending = remaining;
|
|
|
|
pending_volume -= result.pending_volume;
|
|
|
|
match result.prepared {
|
|
|
|
Ok(Some(prepared)) => {
|
|
|
|
prepared_volume += prepared.volume;
|
|
|
|
prepared_executions.push(prepared);
|
|
|
|
}
|
|
|
|
Ok(None) => {
|
|
|
|
// maybe the tcs isn't executable after the account was updated
|
|
|
|
}
|
|
|
|
Err(e) => {
|
2023-10-09 10:56:45 -07:00
|
|
|
trace!(%result.pubkey, "preparation error {:?}", e);
|
2024-03-20 07:25:52 -07:00
|
|
|
error_tracking.write().unwrap().record(
|
2023-12-19 06:37:20 -08:00
|
|
|
LiqErrorType::TcsExecution,
|
|
|
|
&result.pubkey,
|
|
|
|
e.to_string(),
|
|
|
|
);
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
no_new_job = false;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pick a random tcs with volume that would still fit the limit
|
|
|
|
let available_volume = max_volume - pending_volume - prepared_volume;
|
|
|
|
let (pubkey, tcs_id, volume) = {
|
|
|
|
let weights = tcs.iter().map(|(_, _, volume)| {
|
|
|
|
if *volume == u64::MAX {
|
|
|
|
// entries marked like this have been processed already
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
let volume = (*volume).min(max_volume).max(1);
|
|
|
|
if volume <= available_volume {
|
|
|
|
volume
|
|
|
|
} else {
|
|
|
|
0
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let dist_result = WeightedIndex::new(weights);
|
|
|
|
if let Err(WeightedError::AllWeightsZero) = dist_result {
|
|
|
|
// If there's no fitting new job, complete one of the pending
|
|
|
|
// ones to check if that frees up some volume allowance
|
|
|
|
no_new_job = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let dist = dist_result.unwrap();
|
|
|
|
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
let sample = dist.sample(&mut rng);
|
|
|
|
let (pubkey, tcs_id, volume) = &mut tcs[sample];
|
|
|
|
let volume_copy = *volume;
|
|
|
|
*volume = u64::MAX; // don't run this one again
|
|
|
|
(*pubkey, *tcs_id, volume_copy)
|
|
|
|
};
|
|
|
|
|
|
|
|
// start the new one
|
2024-03-20 07:25:52 -07:00
|
|
|
if let Some(job) = self.prepare_job(&pubkey, tcs_id, volume, error_tracking.clone()) {
|
2023-08-18 06:36:02 -07:00
|
|
|
pending_volume += volume;
|
|
|
|
pending.push(job);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We have now prepared a list of tcs we want to execute in `prepared_jobs`.
|
|
|
|
// The complication is that they will alter the liqor and we need to make sure to send
|
|
|
|
// health accounts that will work independently of the order of these tx hitting the chain.
|
|
|
|
|
|
|
|
let mut liqor = self.mango_client.mango_account().await?;
|
|
|
|
let allowed_tokens = prepared_executions
|
|
|
|
.iter()
|
2023-11-08 00:51:36 -08:00
|
|
|
.flat_map(|v| &v.token_indexes)
|
2023-08-18 06:36:02 -07:00
|
|
|
.copied()
|
|
|
|
.unique()
|
|
|
|
.filter(|&idx| liqor.ensure_token_position(idx).is_ok())
|
|
|
|
.collect_vec();
|
|
|
|
|
|
|
|
// Create futures for all the executions that use only allowed tokens
|
|
|
|
let jobs = prepared_executions
|
|
|
|
.into_iter()
|
|
|
|
.filter(|v| {
|
|
|
|
v.token_indexes
|
|
|
|
.iter()
|
|
|
|
.all(|token| allowed_tokens.contains(token))
|
|
|
|
})
|
|
|
|
.map(|v| self.start_prepared_job(v, allowed_tokens.clone()));
|
|
|
|
|
|
|
|
// Execute everything
|
|
|
|
let results = futures::future::join_all(jobs).await;
|
|
|
|
let successes = results
|
|
|
|
.into_iter()
|
|
|
|
.filter_map(|(pubkey, result)| match result {
|
|
|
|
Ok(v) => Some((pubkey, v)),
|
|
|
|
Err(err) => {
|
2023-10-09 10:56:45 -07:00
|
|
|
trace!(%pubkey, "execution error {:?}", err);
|
2024-03-20 07:25:52 -07:00
|
|
|
error_tracking.write().unwrap().record(
|
|
|
|
LiqErrorType::TcsExecution,
|
|
|
|
&pubkey,
|
|
|
|
err.to_string(),
|
|
|
|
);
|
2023-08-18 06:36:02 -07:00
|
|
|
None
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let (completed_pubkeys, completed_txs) = successes.unzip();
|
|
|
|
Ok((completed_txs, completed_pubkeys))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Maybe returns a future that might return a PreparedExecution
|
|
|
|
fn prepare_job(
|
|
|
|
&self,
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
tcs_id: u64,
|
|
|
|
volume: u64,
|
2024-03-20 07:25:52 -07:00
|
|
|
error_tracking: Arc<RwLock<ErrorTracking<Pubkey, LiqErrorType>>>,
|
2023-08-18 06:36:02 -07:00
|
|
|
) -> Option<Pin<Box<dyn Future<Output = PreparationResult> + Send>>> {
|
|
|
|
// Skip a pubkey if there've been too many errors recently
|
2024-03-20 07:25:52 -07:00
|
|
|
if let Some(error_entry) = error_tracking.read().unwrap().had_too_many_errors(
|
|
|
|
LiqErrorType::TcsExecution,
|
|
|
|
pubkey,
|
|
|
|
Instant::now(),
|
|
|
|
) {
|
2023-08-18 06:36:02 -07:00
|
|
|
trace!(
|
|
|
|
"skip checking for tcs on account {pubkey}, had {} errors recently",
|
|
|
|
error_entry.count
|
|
|
|
);
|
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
let context = self.clone();
|
2023-11-08 00:51:36 -08:00
|
|
|
let pubkey = *pubkey;
|
2023-08-18 06:36:02 -07:00
|
|
|
let job = async move {
|
|
|
|
PreparationResult {
|
|
|
|
pubkey,
|
|
|
|
pending_volume: volume,
|
2023-10-07 12:27:19 -07:00
|
|
|
prepared: context
|
|
|
|
.prepare_token_conditional_swap(&pubkey, tcs_id)
|
|
|
|
.await,
|
2023-08-18 06:36:02 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
Some(Box::pin(job))
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn start_prepared_job(
|
|
|
|
&self,
|
|
|
|
pending: PreparedExecution,
|
|
|
|
allowed_tokens: Vec<TokenIndex>,
|
|
|
|
) -> (Pubkey, anyhow::Result<Signature>) {
|
|
|
|
(
|
|
|
|
pending.pubkey,
|
|
|
|
self.start_prepared_job_inner(pending, allowed_tokens).await,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn start_prepared_job_inner(
|
|
|
|
&self,
|
|
|
|
pending: PreparedExecution,
|
|
|
|
allowed_tokens: Vec<TokenIndex>,
|
|
|
|
) -> anyhow::Result<Signature> {
|
2023-10-07 12:27:19 -07:00
|
|
|
// Jupiter quote is provided only for triggers, not close-expired
|
|
|
|
let mut tx_builder = if let Some(jupiter_quote) = pending.jupiter_quote {
|
|
|
|
self.mango_client
|
|
|
|
.jupiter()
|
|
|
|
.prepare_swap_transaction(&jupiter_quote)
|
|
|
|
.await?
|
|
|
|
} else {
|
|
|
|
// compute ix is part of the jupiter swap in the above case
|
|
|
|
let compute_ix =
|
|
|
|
solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit(
|
|
|
|
self.config.compute_limit_for_trigger,
|
|
|
|
);
|
2023-12-05 04:23:11 -08:00
|
|
|
let fee_payer = self.mango_client.client.fee_payer();
|
2023-10-07 12:27:19 -07:00
|
|
|
TransactionBuilder {
|
|
|
|
instructions: vec![compute_ix],
|
2023-12-05 04:23:11 -08:00
|
|
|
signers: vec![self.mango_client.owner.clone(), fee_payer],
|
2024-02-07 03:52:32 -08:00
|
|
|
..self.mango_client.transaction_builder().await?
|
2023-10-07 12:27:19 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-08-18 06:36:02 -07:00
|
|
|
let liqee = self.account_fetcher.fetch_mango_account(&pending.pubkey)?;
|
2023-11-03 03:20:37 -07:00
|
|
|
let mut trigger_ixs = self
|
2023-08-18 06:36:02 -07:00
|
|
|
.mango_client
|
|
|
|
.token_conditional_swap_trigger_instruction(
|
|
|
|
(&pending.pubkey, &liqee),
|
|
|
|
pending.tcs_id,
|
|
|
|
pending.max_buy_token_to_liqee,
|
|
|
|
pending.max_sell_token_to_liqor,
|
2023-10-07 12:27:19 -07:00
|
|
|
pending.min_buy_token,
|
|
|
|
pending.min_taker_price,
|
2023-08-18 06:36:02 -07:00
|
|
|
&allowed_tokens,
|
|
|
|
)
|
|
|
|
.await?;
|
2023-11-03 03:20:37 -07:00
|
|
|
tx_builder
|
|
|
|
.instructions
|
|
|
|
.append(&mut trigger_ixs.instructions);
|
2023-10-07 12:27:19 -07:00
|
|
|
|
2024-03-20 07:25:52 -07:00
|
|
|
let (_, tcs) = liqee.token_conditional_swap_by_id(pending.tcs_id)?;
|
|
|
|
let affected_tokens = allowed_tokens
|
|
|
|
.iter()
|
|
|
|
.chain(&[tcs.buy_token_index, tcs.sell_token_index])
|
|
|
|
.copied()
|
|
|
|
.collect_vec();
|
|
|
|
let liqor = &self.mango_client.mango_account().await?;
|
|
|
|
tx_builder.instructions.append(
|
|
|
|
&mut self
|
|
|
|
.mango_client
|
|
|
|
.health_check_instruction(
|
|
|
|
liqor,
|
|
|
|
self.config.min_health_ratio,
|
|
|
|
affected_tokens,
|
|
|
|
vec![],
|
|
|
|
MaintRatio,
|
|
|
|
)
|
|
|
|
.await?
|
|
|
|
.instructions,
|
|
|
|
);
|
|
|
|
|
2023-10-07 12:27:19 -07:00
|
|
|
let txsig = tx_builder
|
|
|
|
.send_and_confirm(&self.mango_client.client)
|
2023-08-18 06:36:02 -07:00
|
|
|
.await?;
|
|
|
|
info!(
|
|
|
|
pubkey = %pending.pubkey,
|
|
|
|
tcs_id = pending.tcs_id,
|
|
|
|
%txsig,
|
|
|
|
"executed token conditional swap",
|
|
|
|
);
|
|
|
|
Ok(txsig)
|
|
|
|
}
|
|
|
|
}
|