Liquidator, liqtest and rust client fixes (#336)
Liquidator, liqtest and rust client fixes - Make rust MangoClient fully async - Update and improve liquidation test setup scripts - Update liquidator to use versioned transactions - Update liquidator to use mango and jupiter account lookup tables - Use jupiter v4 api to get versioned transactions Perp liquidation is still not fully good. See discussion aboult perp_liq_pnl_with_token.
This commit is contained in:
parent
1c36b1b493
commit
5a38506f04
|
@ -1118,10 +1118,12 @@ dependencies = [
|
|||
"anchor-lang",
|
||||
"anchor-spl",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.13.1",
|
||||
"bincode",
|
||||
"fixed",
|
||||
"fixed-macro",
|
||||
"futures 0.3.25",
|
||||
"itertools 0.10.5",
|
||||
"log 0.4.17",
|
||||
"mango-v4",
|
||||
|
@ -1132,6 +1134,7 @@ dependencies = [
|
|||
"serum_dex 0.5.6",
|
||||
"shellexpand",
|
||||
"solana-account-decoder",
|
||||
"solana-address-lookup-table-program",
|
||||
"solana-client",
|
||||
"solana-sdk",
|
||||
"spl-associated-token-account",
|
||||
|
|
|
@ -119,7 +119,8 @@ impl Rpc {
|
|||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
@ -137,7 +138,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
num
|
||||
} else {
|
||||
// find free account_num
|
||||
let accounts = MangoClient::find_accounts(&client, group, &owner)?;
|
||||
let accounts = MangoClient::find_accounts(&client, group, &owner).await?;
|
||||
if accounts.is_empty() {
|
||||
0
|
||||
} else {
|
||||
|
@ -149,14 +150,9 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
+ 1
|
||||
}
|
||||
};
|
||||
let (account, txsig) = MangoClient::create_account(
|
||||
&client,
|
||||
group,
|
||||
&owner,
|
||||
&owner,
|
||||
account_num,
|
||||
&cmd.name,
|
||||
)?;
|
||||
let (account, txsig) =
|
||||
MangoClient::create_account(&client, group, &owner, &owner, account_num, &cmd.name)
|
||||
.await?;
|
||||
println!("{}", account);
|
||||
println!("{}", txsig);
|
||||
}
|
||||
|
@ -165,8 +161,8 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let account = client::pubkey_from_cli(&cmd.account);
|
||||
let owner = client::keypair_from_cli(&cmd.owner);
|
||||
let mint = client::pubkey_from_cli(&cmd.mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
||||
let txsig = client.token_deposit(mint, cmd.amount)?;
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let txsig = client.token_deposit(mint, cmd.amount).await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::JupiterSwap(cmd) => {
|
||||
|
@ -175,14 +171,16 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let owner = client::keypair_from_cli(&cmd.owner);
|
||||
let input_mint = client::pubkey_from_cli(&cmd.input_mint);
|
||||
let output_mint = client::pubkey_from_cli(&cmd.output_mint);
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner)?;
|
||||
let txsig = client.jupiter_swap(
|
||||
let client = MangoClient::new_for_existing_account(client, account, owner).await?;
|
||||
let txsig = client
|
||||
.jupiter_swap(
|
||||
input_mint,
|
||||
output_mint,
|
||||
cmd.amount,
|
||||
cmd.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
println!("{}", txsig);
|
||||
}
|
||||
Command::GroupAddress { creator, num } => {
|
||||
|
|
|
@ -11,8 +11,10 @@ anchor-client = { path = "../anchor/client" }
|
|||
anchor-lang = { path = "../anchor/lang" }
|
||||
anchor-spl = { path = "../anchor/spl" }
|
||||
anyhow = "1.0"
|
||||
async-trait = "0.1.52"
|
||||
fixed = { version = "=1.11.0", features = ["serde", "borsh"] }
|
||||
fixed-macro = "^1.1.1"
|
||||
futures = "0.3.25"
|
||||
itertools = "0.10.3"
|
||||
mango-v4 = { path = "../programs/mango-v4", features = ["no-entrypoint", "client"] }
|
||||
pyth-sdk-solana = "0.1.0"
|
||||
|
@ -21,6 +23,7 @@ shellexpand = "2.1.0"
|
|||
solana-account-decoder = "~1.14.9"
|
||||
solana-client = "~1.14.9"
|
||||
solana-sdk = "~1.14.9"
|
||||
solana-address-lookup-table-program = "~1.14.9"
|
||||
spl-associated-token-account = "1.0.3"
|
||||
thiserror = "1.0.31"
|
||||
log = "0.4"
|
||||
|
|
|
@ -6,15 +6,22 @@ use anyhow::Context;
|
|||
use anchor_client::ClientError;
|
||||
use anchor_lang::AccountDeserialize;
|
||||
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
use mango_v4::state::MangoAccountValue;
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
pub trait AccountFetcher: Sync + Send {
|
||||
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
|
||||
async fn fetch_raw_account_lookup_table(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<AccountSharedData> {
|
||||
self.fetch_raw_account(address).await
|
||||
}
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
@ -22,35 +29,37 @@ pub trait AccountFetcher: Sync + Send {
|
|||
}
|
||||
|
||||
// Can't be in the trait, since then it would no longer be object-safe...
|
||||
pub fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
|
||||
pub async fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
|
||||
fetcher: &dyn AccountFetcher,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<T> {
|
||||
let account = fetcher.fetch_raw_account(address)?;
|
||||
let account = fetcher.fetch_raw_account(address).await?;
|
||||
let mut data: &[u8] = &account.data();
|
||||
T::try_deserialize(&mut data)
|
||||
.with_context(|| format!("deserializing anchor account {}", address))
|
||||
}
|
||||
|
||||
// Can't be in the trait, since then it would no longer be object-safe...
|
||||
pub fn account_fetcher_fetch_mango_account(
|
||||
pub async fn account_fetcher_fetch_mango_account(
|
||||
fetcher: &dyn AccountFetcher,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<MangoAccountValue> {
|
||||
let account = fetcher.fetch_raw_account(address)?;
|
||||
let account = fetcher.fetch_raw_account(address).await?;
|
||||
let data: &[u8] = &account.data();
|
||||
MangoAccountValue::from_bytes(&data[8..])
|
||||
.with_context(|| format!("deserializing mango account {}", address))
|
||||
}
|
||||
|
||||
pub struct RpcAccountFetcher {
|
||||
pub rpc: RpcClient,
|
||||
pub rpc: RpcClientAsync,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl AccountFetcher for RpcAccountFetcher {
|
||||
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
self.rpc
|
||||
.get_account_with_commitment(address, self.rpc.commitment())
|
||||
.await
|
||||
.with_context(|| format!("fetch account {}", *address))?
|
||||
.value
|
||||
.ok_or(ClientError::AccountNotFound)
|
||||
|
@ -58,7 +67,7 @@ impl AccountFetcher for RpcAccountFetcher {
|
|||
.map(Into::into)
|
||||
}
|
||||
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
@ -78,7 +87,10 @@ impl AccountFetcher for RpcAccountFetcher {
|
|||
},
|
||||
with_context: Some(true),
|
||||
};
|
||||
let accs = self.rpc.get_program_accounts_with_config(program, config)?;
|
||||
let accs = self
|
||||
.rpc
|
||||
.get_program_accounts_with_config(program, config)
|
||||
.await?;
|
||||
// convert Account -> AccountSharedData
|
||||
Ok(accs
|
||||
.into_iter()
|
||||
|
@ -121,18 +133,19 @@ impl<T: AccountFetcher> CachedAccountFetcher<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
|
||||
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
|
||||
let mut cache = self.cache.lock().unwrap();
|
||||
if let Some(account) = cache.accounts.get(address) {
|
||||
return Ok(account.clone());
|
||||
}
|
||||
let account = self.fetcher.fetch_raw_account(address)?;
|
||||
let account = self.fetcher.fetch_raw_account(address).await?;
|
||||
cache.accounts.insert(*address, account.clone());
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
@ -147,7 +160,8 @@ impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
|
|||
}
|
||||
let accounts = self
|
||||
.fetcher
|
||||
.fetch_program_accounts(program, discriminator)?;
|
||||
.fetch_program_accounts(program, discriminator)
|
||||
.await?;
|
||||
cache
|
||||
.keys_for_program_and_discriminator
|
||||
.insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect());
|
||||
|
|
|
@ -11,7 +11,7 @@ use mango_v4::state::{MangoAccount, MangoAccountValue};
|
|||
|
||||
use anyhow::Context;
|
||||
|
||||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::{AccountSharedData, ReadableAccount};
|
||||
use solana_sdk::clock::Slot;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
@ -19,7 +19,7 @@ use solana_sdk::signature::Signature;
|
|||
|
||||
pub struct AccountFetcher {
|
||||
pub chain_data: Arc<RwLock<ChainData>>,
|
||||
pub rpc: RpcClient,
|
||||
pub rpc: RpcClientAsync,
|
||||
}
|
||||
|
||||
impl AccountFetcher {
|
||||
|
@ -55,16 +55,19 @@ impl AccountFetcher {
|
|||
}
|
||||
|
||||
// fetches via RPC, stores in ChainData, returns new version
|
||||
pub fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
|
||||
pub async fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<T> {
|
||||
self.refresh_account_via_rpc(address)?;
|
||||
self.refresh_account_via_rpc(address).await?;
|
||||
self.fetch(address)
|
||||
}
|
||||
|
||||
pub fn fetch_fresh_mango_account(&self, address: &Pubkey) -> anyhow::Result<MangoAccountValue> {
|
||||
self.refresh_account_via_rpc(address)?;
|
||||
pub async fn fetch_fresh_mango_account(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<MangoAccountValue> {
|
||||
self.refresh_account_via_rpc(address).await?;
|
||||
self.fetch_mango_account(address)
|
||||
}
|
||||
|
||||
|
@ -76,10 +79,11 @@ impl AccountFetcher {
|
|||
.clone())
|
||||
}
|
||||
|
||||
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
|
||||
pub async fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
|
||||
let response = self
|
||||
.rpc
|
||||
.get_account_with_commitment(address, self.rpc.commitment())
|
||||
.await
|
||||
.with_context(|| format!("refresh account {} via rpc", address))?;
|
||||
let slot = response.context.slot;
|
||||
let account = response
|
||||
|
@ -100,8 +104,8 @@ impl AccountFetcher {
|
|||
}
|
||||
|
||||
/// Return the maximum slot reported for the processing of the signatures
|
||||
pub fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
|
||||
let statuses = self.rpc.get_signature_statuses(signatures)?.value;
|
||||
pub async fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
|
||||
let statuses = self.rpc.get_signature_statuses(signatures).await?.value;
|
||||
Ok(statuses
|
||||
.iter()
|
||||
.map(|status_opt| status_opt.as_ref().map(|status| status.slot).unwrap_or(0))
|
||||
|
@ -110,7 +114,7 @@ impl AccountFetcher {
|
|||
}
|
||||
|
||||
/// Return success once all addresses have data >= min_slot
|
||||
pub fn refresh_accounts_via_rpc_until_slot(
|
||||
pub async fn refresh_accounts_via_rpc_until_slot(
|
||||
&self,
|
||||
addresses: &[Pubkey],
|
||||
min_slot: Slot,
|
||||
|
@ -126,7 +130,7 @@ impl AccountFetcher {
|
|||
min_slot
|
||||
);
|
||||
}
|
||||
let data_slot = self.refresh_account_via_rpc(address)?;
|
||||
let data_slot = self.refresh_account_via_rpc(address).await?;
|
||||
if data_slot >= min_slot {
|
||||
break;
|
||||
}
|
||||
|
@ -137,15 +141,29 @@ impl AccountFetcher {
|
|||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl crate::AccountFetcher for AccountFetcher {
|
||||
fn fetch_raw_account(
|
||||
async fn fetch_raw_account(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<solana_sdk::account::AccountSharedData> {
|
||||
self.fetch_raw(&address)
|
||||
self.fetch_raw(address)
|
||||
}
|
||||
|
||||
fn fetch_program_accounts(
|
||||
async fn fetch_raw_account_lookup_table(
|
||||
&self,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<AccountSharedData> {
|
||||
// Fetch data via RPC if missing: the chain data updater doesn't know about all the
|
||||
// lookup talbes we may need.
|
||||
if let Ok(alt) = self.fetch_raw(address) {
|
||||
return Ok(alt);
|
||||
}
|
||||
self.refresh_account_via_rpc(address).await?;
|
||||
self.fetch_raw(address)
|
||||
}
|
||||
|
||||
async fn fetch_program_accounts(
|
||||
&self,
|
||||
program: &Pubkey,
|
||||
discriminator: [u8; 8],
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,23 +1,24 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use anchor_client::{Client, ClientError, Cluster, Program};
|
||||
use anchor_client::ClientError;
|
||||
|
||||
use anchor_lang::__private::bytemuck;
|
||||
|
||||
use mango_v4::state::{
|
||||
MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, Serum3MarketIndex,
|
||||
TokenIndex,
|
||||
Group, MangoAccountValue, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market,
|
||||
Serum3MarketIndex, TokenIndex,
|
||||
};
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::gpa::*;
|
||||
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_sdk::account::Account;
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TokenContext {
|
||||
|
@ -64,6 +65,8 @@ pub struct MangoGroupContext {
|
|||
|
||||
pub perp_markets: HashMap<PerpMarketIndex, PerpMarketContext>,
|
||||
pub perp_market_indexes_by_name: HashMap<String, PerpMarketIndex>,
|
||||
|
||||
pub address_lookup_tables: Vec<Pubkey>,
|
||||
}
|
||||
|
||||
impl MangoGroupContext {
|
||||
|
@ -94,17 +97,11 @@ impl MangoGroupContext {
|
|||
self.perp(perp_market_index).address
|
||||
}
|
||||
|
||||
pub fn new_from_rpc(
|
||||
group: Pubkey,
|
||||
cluster: Cluster,
|
||||
commitment: CommitmentConfig,
|
||||
) -> Result<Self, ClientError> {
|
||||
let program =
|
||||
Client::new_with_options(cluster, std::rc::Rc::new(Keypair::new()), commitment)
|
||||
.program(mango_v4::ID);
|
||||
pub async fn new_from_rpc(rpc: &RpcClientAsync, group: Pubkey) -> anyhow::Result<Self> {
|
||||
let program = mango_v4::ID;
|
||||
|
||||
// tokens
|
||||
let mint_info_tuples = fetch_mint_infos(&program, group)?;
|
||||
let mint_info_tuples = fetch_mint_infos(rpc, program, group).await?;
|
||||
let mut tokens = mint_info_tuples
|
||||
.iter()
|
||||
.map(|(pk, mi)| {
|
||||
|
@ -124,7 +121,7 @@ impl MangoGroupContext {
|
|||
// reading the banks is only needed for the token names and decimals
|
||||
// FUTURE: either store the names on MintInfo as well, or maybe don't store them at all
|
||||
// because they are in metaplex?
|
||||
let bank_tuples = fetch_banks(&program, group)?;
|
||||
let bank_tuples = fetch_banks(rpc, program, group).await?;
|
||||
for (_, bank) in bank_tuples {
|
||||
let token = tokens.get_mut(&bank.token_index).unwrap();
|
||||
token.name = bank.name().into();
|
||||
|
@ -133,11 +130,15 @@ impl MangoGroupContext {
|
|||
assert!(tokens.values().all(|t| t.decimals != u8::MAX));
|
||||
|
||||
// serum3 markets
|
||||
let serum3_market_tuples = fetch_serum3_markets(&program, group)?;
|
||||
let serum3_market_tuples = fetch_serum3_markets(rpc, program, group).await?;
|
||||
let serum3_markets_external = stream::iter(serum3_market_tuples.iter())
|
||||
.then(|(_, s)| fetch_raw_account(rpc, s.serum_market_external))
|
||||
.try_collect::<Vec<_>>()
|
||||
.await?;
|
||||
let serum3_markets = serum3_market_tuples
|
||||
.iter()
|
||||
.map(|(pk, s)| {
|
||||
let market_external_account = fetch_raw_account(&program, s.serum_market_external)?;
|
||||
.zip(serum3_markets_external.iter())
|
||||
.map(|((pk, s), market_external_account)| {
|
||||
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
|
||||
&market_external_account.data
|
||||
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
|
||||
|
@ -148,7 +149,7 @@ impl MangoGroupContext {
|
|||
&s.serum_program,
|
||||
)
|
||||
.unwrap();
|
||||
Ok((
|
||||
(
|
||||
s.market_index,
|
||||
Serum3MarketContext {
|
||||
address: *pk,
|
||||
|
@ -163,12 +164,12 @@ impl MangoGroupContext {
|
|||
coin_lot_size: market_external.coin_lot_size,
|
||||
pc_lot_size: market_external.pc_lot_size,
|
||||
},
|
||||
))
|
||||
)
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, ClientError>>()?;
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// perp markets
|
||||
let perp_market_tuples = fetch_perp_markets(&program, group)?;
|
||||
let perp_market_tuples = fetch_perp_markets(rpc, program, group).await?;
|
||||
let perp_markets = perp_market_tuples
|
||||
.iter()
|
||||
.map(|(pk, pm)| {
|
||||
|
@ -196,6 +197,14 @@ impl MangoGroupContext {
|
|||
.map(|(i, p)| (p.market.name().to_string(), *i))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let group_data = fetch_anchor_account::<Group>(rpc, &group).await?;
|
||||
let address_lookup_tables = group_data
|
||||
.address_lookup_tables
|
||||
.iter()
|
||||
.filter(|&&k| k != Pubkey::default())
|
||||
.cloned()
|
||||
.collect::<Vec<Pubkey>>();
|
||||
|
||||
Ok(MangoGroupContext {
|
||||
group,
|
||||
tokens,
|
||||
|
@ -204,6 +213,7 @@ impl MangoGroupContext {
|
|||
serum3_market_indexes_by_name,
|
||||
perp_markets,
|
||||
perp_market_indexes_by_name,
|
||||
address_lookup_tables,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -331,9 +341,9 @@ fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey {
|
|||
Pubkey::new(bytemuck::cast_slice(&d as &[_]))
|
||||
}
|
||||
|
||||
fn fetch_raw_account(program: &Program, address: Pubkey) -> Result<Account, ClientError> {
|
||||
let rpc = program.rpc();
|
||||
rpc.get_account_with_commitment(&address, rpc.commitment())?
|
||||
async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result<Account, ClientError> {
|
||||
rpc.get_account_with_commitment(&address, rpc.commitment())
|
||||
.await?
|
||||
.value
|
||||
.ok_or(ClientError::AccountNotFound)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
use anchor_client::{ClientError, Program};
|
||||
use anchor_lang::Discriminator;
|
||||
use anchor_lang::{AccountDeserialize, Discriminator};
|
||||
|
||||
use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market};
|
||||
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
|
||||
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
|
||||
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub fn fetch_mango_accounts(
|
||||
program: &Program,
|
||||
pub async fn fetch_mango_accounts(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
owner: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, MangoAccountValue)>, ClientError> {
|
||||
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue)>> {
|
||||
let config = RpcProgramAccountsConfig {
|
||||
filters: Some(vec![
|
||||
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
|
@ -28,47 +29,103 @@ pub fn fetch_mango_accounts(
|
|||
},
|
||||
..RpcProgramAccountsConfig::default()
|
||||
};
|
||||
program
|
||||
.rpc()
|
||||
.get_program_accounts_with_config(&program.id(), config)?
|
||||
rpc.get_program_accounts_with_config(&program, config)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(key, account)| Ok((key, MangoAccountValue::from_bytes(&account.data[8..])?)))
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
}
|
||||
|
||||
pub fn fetch_banks(program: &Program, group: Pubkey) -> Result<Vec<(Pubkey, Bank)>, ClientError> {
|
||||
program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
pub async fn fetch_anchor_account<T: AccountDeserialize>(
|
||||
rpc: &RpcClientAsync,
|
||||
address: &Pubkey,
|
||||
) -> anyhow::Result<T> {
|
||||
let account = rpc.get_account(address).await?;
|
||||
Ok(T::try_deserialize(&mut (&account.data as &[u8]))?)
|
||||
}
|
||||
|
||||
pub fn fetch_mint_infos(
|
||||
program: &Program,
|
||||
group: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, MintInfo)>, ClientError> {
|
||||
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
async fn fetch_anchor_accounts<T: AccountDeserialize + Discriminator>(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
filters: Vec<RpcFilterType>,
|
||||
) -> anyhow::Result<Vec<(Pubkey, T)>> {
|
||||
let account_type_filter =
|
||||
RpcFilterType::Memcmp(Memcmp::new_base58_encoded(0, &T::discriminator()));
|
||||
let config = RpcProgramAccountsConfig {
|
||||
filters: Some([vec![account_type_filter], filters].concat()),
|
||||
account_config: RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
..RpcAccountInfoConfig::default()
|
||||
},
|
||||
..RpcProgramAccountsConfig::default()
|
||||
};
|
||||
rpc.get_program_accounts_with_config(&program, config)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(key, account)| Ok((key, T::try_deserialize(&mut (&account.data as &[u8]))?)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn fetch_serum3_markets(
|
||||
program: &Program,
|
||||
pub async fn fetch_banks(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, Serum3Market)>, ClientError> {
|
||||
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
) -> anyhow::Result<Vec<(Pubkey, Bank)>> {
|
||||
fetch_anchor_accounts::<Bank>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn fetch_perp_markets(
|
||||
program: &Program,
|
||||
pub async fn fetch_mint_infos(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> Result<Vec<(Pubkey, PerpMarket)>, ClientError> {
|
||||
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
) -> anyhow::Result<Vec<(Pubkey, MintInfo)>> {
|
||||
fetch_anchor_accounts::<MintInfo>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))])
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_serum3_markets(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> anyhow::Result<Vec<(Pubkey, Serum3Market)>> {
|
||||
fetch_anchor_accounts::<Serum3Market>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_perp_markets(
|
||||
rpc: &RpcClientAsync,
|
||||
program: Pubkey,
|
||||
group: Pubkey,
|
||||
) -> anyhow::Result<Vec<(Pubkey, PerpMarket)>> {
|
||||
fetch_anchor_accounts::<PerpMarket>(
|
||||
rpc,
|
||||
program,
|
||||
vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
|
||||
8,
|
||||
&group.to_bytes(),
|
||||
))],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::{AccountFetcher, MangoGroupContext};
|
||||
use anyhow::Context;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||
use mango_v4::health::{FixedOrderAccountRetriever, HealthCache};
|
||||
use mango_v4::state::MangoAccountValue;
|
||||
|
||||
pub fn new(
|
||||
pub async fn new(
|
||||
context: &MangoGroupContext,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
account: &MangoAccountValue,
|
||||
|
@ -13,18 +14,18 @@ pub fn new(
|
|||
let active_perp_len = account.active_perp_positions().count();
|
||||
|
||||
let metas = context.derive_health_check_remaining_account_metas(account, vec![], false)?;
|
||||
let accounts = metas
|
||||
.iter()
|
||||
.map(|meta| {
|
||||
let accounts: anyhow::Result<Vec<KeyedAccountSharedData>> = stream::iter(metas.iter())
|
||||
.then(|meta| async {
|
||||
Ok(KeyedAccountSharedData::new(
|
||||
meta.pubkey,
|
||||
account_fetcher.fetch_raw_account(&meta.pubkey)?,
|
||||
account_fetcher.fetch_raw_account(&meta.pubkey).await?,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<_>>>()?;
|
||||
.try_collect()
|
||||
.await;
|
||||
|
||||
let retriever = FixedOrderAccountRetriever {
|
||||
ais: accounts,
|
||||
ais: accounts?,
|
||||
n_banks: active_token_len,
|
||||
n_perps: active_perp_len,
|
||||
begin_perp: active_token_len * 2,
|
||||
|
|
|
@ -5,20 +5,21 @@ use serde::{Deserialize, Serialize};
|
|||
pub struct QueryResult {
|
||||
pub data: Vec<QueryRoute>,
|
||||
pub time_taken: f64,
|
||||
pub context_slot: String,
|
||||
pub context_slot: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRoute {
|
||||
pub in_amount: u64,
|
||||
pub out_amount: u64,
|
||||
pub amount: u64,
|
||||
pub other_amount_threshold: u64,
|
||||
pub out_amount_with_slippage: Option<u64>,
|
||||
pub swap_mode: String,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub price_impact_pct: f64,
|
||||
pub market_infos: Vec<QueryMarketInfo>,
|
||||
pub amount: String,
|
||||
pub slippage_bps: u64,
|
||||
pub other_amount_threshold: String,
|
||||
pub swap_mode: String,
|
||||
pub fees: Option<QueryRouteFees>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
|
@ -28,22 +29,35 @@ pub struct QueryMarketInfo {
|
|||
pub label: String,
|
||||
pub input_mint: String,
|
||||
pub output_mint: String,
|
||||
pub in_amount: u64,
|
||||
pub out_amount: u64,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub in_amount: String,
|
||||
pub out_amount: String,
|
||||
pub min_in_amount: Option<String>,
|
||||
pub min_out_amount: Option<String>,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
pub lp_fee: QueryFee,
|
||||
pub platform_fee: QueryFee,
|
||||
pub not_enough_liquidity: bool,
|
||||
pub price_impact_pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryFee {
|
||||
pub amount: u64,
|
||||
pub amount: String,
|
||||
pub mint: String,
|
||||
pub pct: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct QueryRouteFees {
|
||||
pub signature_fee: f64,
|
||||
pub open_orders_deposits: Vec<f64>,
|
||||
pub ata_deposits: Vec<f64>,
|
||||
pub total_fee_and_deposits: f64,
|
||||
#[serde(rename = "minimalSOLForTransaction")]
|
||||
pub minimal_sol_for_transaction: f64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SwapRequest {
|
||||
|
|
|
@ -15,7 +15,7 @@ pub enum Direction {
|
|||
|
||||
/// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order.
|
||||
/// Note: keep in sync with perp.ts:getSettlePnlCandidates
|
||||
pub fn fetch_top(
|
||||
pub async fn fetch_top(
|
||||
context: &crate::context::MangoGroupContext,
|
||||
account_fetcher: &impl AccountFetcher,
|
||||
perp_market_index: PerpMarketIndex,
|
||||
|
@ -25,20 +25,23 @@ pub fn fetch_top(
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let now_ts: u64 = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)?
|
||||
.as_millis()
|
||||
.as_secs()
|
||||
.try_into()?;
|
||||
|
||||
let perp = context.perp(perp_market_index);
|
||||
let perp_market =
|
||||
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address)?;
|
||||
let oracle_acc = account_fetcher.fetch_raw_account(&perp_market.oracle)?;
|
||||
account_fetcher_fetch_anchor_account::<PerpMarket>(account_fetcher, &perp.address).await?;
|
||||
let oracle_acc = account_fetcher
|
||||
.fetch_raw_account(&perp_market.oracle)
|
||||
.await?;
|
||||
let oracle_price = perp_market.oracle_price(
|
||||
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let accounts =
|
||||
account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?;
|
||||
let accounts = account_fetcher
|
||||
.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())
|
||||
.await?;
|
||||
|
||||
let mut accounts_pnl = accounts
|
||||
.iter()
|
||||
|
@ -54,6 +57,7 @@ pub fn fetch_top(
|
|||
return None;
|
||||
}
|
||||
let mut perp_pos = perp_pos.unwrap().clone();
|
||||
perp_pos.settle_funding(&perp_market);
|
||||
perp_pos.update_settle_limit(&perp_market, now_ts);
|
||||
let pnl = perp_pos.pnl_for_price(&perp_market, oracle_price).unwrap();
|
||||
let limited_pnl = perp_pos.apply_pnl_settle_limit(pnl, &perp_market);
|
||||
|
@ -88,8 +92,9 @@ pub fn fetch_top(
|
|||
} else {
|
||||
I80F48::ZERO
|
||||
};
|
||||
let perp_settle_health =
|
||||
crate::health_cache::new(context, account_fetcher, &acc)?.perp_settle_health();
|
||||
let perp_settle_health = crate::health_cache::new(context, account_fetcher, &acc)
|
||||
.await?
|
||||
.perp_settle_health();
|
||||
let settleable_pnl = if perp_settle_health > 0 && !acc.being_liquidated() {
|
||||
(*pnl).max(-perp_settle_health)
|
||||
} else {
|
||||
|
|
|
@ -4,7 +4,6 @@ use crate::MangoClient;
|
|||
use itertools::Itertools;
|
||||
|
||||
use anchor_lang::{__private::bytemuck::cast_ref, solana_program};
|
||||
use client::prettify_client_error;
|
||||
use futures::Future;
|
||||
use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex};
|
||||
use solana_sdk::{
|
||||
|
@ -93,15 +92,12 @@ pub async fn loop_update_index_and_rate(
|
|||
|
||||
let token_indices_clone = token_indices.clone();
|
||||
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let token_names = token_indices_clone
|
||||
.iter()
|
||||
.map(|token_index| client.context.token(*token_index).name.to_owned())
|
||||
.join(",");
|
||||
|
||||
let program = client.program();
|
||||
let mut req = program.request();
|
||||
req = req.instruction(ComputeBudgetInstruction::set_compute_unit_price(1));
|
||||
let mut instructions = vec![ComputeBudgetInstruction::set_compute_unit_price(1)];
|
||||
for token_index in token_indices_clone.iter() {
|
||||
let token = client.context.token(*token_index);
|
||||
let banks_for_a_token = token.mint_info.banks();
|
||||
|
@ -131,10 +127,12 @@ pub async fn loop_update_index_and_rate(
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
ix.accounts.append(&mut banks);
|
||||
req = req.instruction(ix);
|
||||
instructions.push(ix);
|
||||
}
|
||||
let pre = Instant::now();
|
||||
let sig_result = req.send().map_err(prettify_client_error);
|
||||
let sig_result = client
|
||||
.send_and_confirm_permissionless_tx(instructions)
|
||||
.await;
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
|
@ -152,21 +150,6 @@ pub async fn loop_update_index_and_rate(
|
|||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,11 +164,13 @@ pub async fn loop_consume_events(
|
|||
interval.tick().await;
|
||||
|
||||
let client = mango_client.clone();
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let mut event_queue: EventQueue =
|
||||
client.program().account(perp_market.event_queue).unwrap();
|
||||
|
||||
let find_accounts = || async {
|
||||
let mut num_of_events = 0;
|
||||
let mut event_queue: EventQueue = client
|
||||
.client
|
||||
.rpc_anchor_account(&perp_market.event_queue)
|
||||
.await?;
|
||||
|
||||
// TODO: future, choose better constant of how many max events to pack
|
||||
// TODO: future, choose better constant of how many max mango accounts to pack
|
||||
|
@ -208,10 +193,28 @@ pub async fn loop_consume_events(
|
|||
EventType::Liquidate => {}
|
||||
}
|
||||
event_queue.pop_front()?;
|
||||
num_of_events+=1;
|
||||
num_of_events += 1;
|
||||
}
|
||||
|
||||
let mut ams_ = set
|
||||
if num_of_events == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some((set, num_of_events)))
|
||||
};
|
||||
|
||||
let event_info: anyhow::Result<Option<(HashSet<Pubkey>, u32)>> = find_accounts().await;
|
||||
|
||||
let (event_accounts, num_of_events) = match event_info {
|
||||
Ok(Some(x)) => x,
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
log::error!("preparing consume_events ams: {err:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut event_ams = event_accounts
|
||||
.iter()
|
||||
.map(|key| -> AccountMeta {
|
||||
AccountMeta {
|
||||
|
@ -222,15 +225,8 @@ pub async fn loop_consume_events(
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if num_of_events == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.program()
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
let ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: {
|
||||
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
|
||||
|
@ -241,15 +237,15 @@ pub async fn loop_consume_events(
|
|||
},
|
||||
None,
|
||||
);
|
||||
ams.append(&mut ams_);
|
||||
ams.append(&mut event_ams);
|
||||
ams
|
||||
},
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::PerpConsumeEvents { limit: 10 },
|
||||
),
|
||||
})
|
||||
.send()
|
||||
.map_err(prettify_client_error);
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpConsumeEvents {
|
||||
limit: 10,
|
||||
}),
|
||||
};
|
||||
|
||||
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::info!(
|
||||
|
@ -269,21 +265,6 @@ pub async fn loop_consume_events(
|
|||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,12 +279,9 @@ pub async fn loop_update_funding(
|
|||
interval.tick().await;
|
||||
|
||||
let client = mango_client.clone();
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
|
||||
let pre = Instant::now();
|
||||
let sig_result = client
|
||||
.program()
|
||||
.request()
|
||||
.instruction(Instruction {
|
||||
let ix = Instruction {
|
||||
program_id: mango_v4::id(),
|
||||
accounts: anchor_lang::ToAccountMetas::to_account_metas(
|
||||
&mango_v4::accounts::PerpUpdateFunding {
|
||||
|
@ -315,12 +293,10 @@ pub async fn loop_update_funding(
|
|||
},
|
||||
None,
|
||||
),
|
||||
data: anchor_lang::InstructionData::data(
|
||||
&mango_v4::instruction::PerpUpdateFunding {},
|
||||
),
|
||||
})
|
||||
.send()
|
||||
.map_err(prettify_client_error);
|
||||
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}),
|
||||
};
|
||||
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).await;
|
||||
|
||||
if let Err(e) = sig_result {
|
||||
log::error!(
|
||||
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
|
||||
|
@ -337,20 +313,5 @@ pub async fn loop_update_funding(
|
|||
);
|
||||
log::info!("{:?}", sig_result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,9 @@ enum Command {
|
|||
Crank {},
|
||||
Taker {},
|
||||
}
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
@ -87,7 +89,8 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
Command::Taker { .. } => CommitmentConfig::confirmed(),
|
||||
};
|
||||
|
||||
let mango_client = Arc::new(MangoClient::new_for_existing_account(
|
||||
let mango_client = Arc::new(
|
||||
MangoClient::new_for_existing_account(
|
||||
Client::new(
|
||||
cluster,
|
||||
commitment,
|
||||
|
@ -96,12 +99,9 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
),
|
||||
cli.mango_account,
|
||||
owner,
|
||||
)?);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
|
||||
let debugging_handle = async {
|
||||
let mut interval = time::interval(time::Duration::from_secs(5));
|
||||
|
@ -120,17 +120,18 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
match cli.command {
|
||||
Command::Crank { .. } => {
|
||||
let client = mango_client.clone();
|
||||
rt.block_on(crank::runner(
|
||||
crank::runner(
|
||||
client,
|
||||
debugging_handle,
|
||||
cli.interval_update_banks,
|
||||
cli.interval_consume_events,
|
||||
cli.interval_update_funding,
|
||||
))
|
||||
)
|
||||
.await
|
||||
}
|
||||
Command::Taker { .. } => {
|
||||
let client = mango_client.clone();
|
||||
rt.block_on(taker::runner(client, debugging_handle))
|
||||
taker::runner(client, debugging_handle).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,8 @@ pub async fn runner(
|
|||
mango_client: Arc<MangoClient>,
|
||||
_debugging_handle: impl Future,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
ensure_deposit(&mango_client)?;
|
||||
|
||||
ensure_oo(&mango_client)?;
|
||||
ensure_deposit(&mango_client).await?;
|
||||
ensure_oo(&mango_client).await?;
|
||||
|
||||
let mut price_arcs = HashMap::new();
|
||||
for market_name in mango_client.context.serum3_market_indexes_by_name.keys() {
|
||||
|
@ -30,6 +29,7 @@ pub async fn runner(
|
|||
.first()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
price_arcs.insert(
|
||||
market_name.to_owned(),
|
||||
|
@ -73,23 +73,25 @@ pub async fn runner(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let account = mango_client.mango_account()?;
|
||||
async fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let account = mango_client.mango_account().await?;
|
||||
|
||||
for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() {
|
||||
if account.serum3_orders(*market_index).is_err() {
|
||||
mango_client.serum3_create_open_orders(serum3_market.market.name())?;
|
||||
mango_client
|
||||
.serum3_create_open_orders(serum3_market.market.name())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let mango_account = mango_client.mango_account()?;
|
||||
async fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
|
||||
let mango_account = mango_client.mango_account().await?;
|
||||
|
||||
for &token_index in mango_client.context.tokens.keys() {
|
||||
let bank = mango_client.first_bank(token_index)?;
|
||||
let bank = mango_client.first_bank(token_index).await?;
|
||||
let desired_balance = I80F48::from_num(10_000 * 10u64.pow(bank.mint_decimals as u32));
|
||||
|
||||
let token_account_opt = mango_account.token_position(token_index).ok();
|
||||
|
@ -114,7 +116,9 @@ fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error>
|
|||
}
|
||||
|
||||
log::info!("Depositing {} {}", deposit_native, bank.name());
|
||||
mango_client.token_deposit(bank.mint, desired_balance.to_num())?;
|
||||
mango_client
|
||||
.token_deposit(bank.mint, desired_balance.to_num())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -126,34 +130,16 @@ pub async fn loop_blocking_price_update(
|
|||
price: Arc<RwLock<I80F48>>,
|
||||
) {
|
||||
let mut interval = time::interval(Duration::from_secs(1));
|
||||
let token_name = market_name.split('/').collect::<Vec<&str>>()[0];
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
let client1 = mango_client.clone();
|
||||
let market_name1 = market_name.clone();
|
||||
let price = price.clone();
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
let token_name = market_name1.split('/').collect::<Vec<&str>>()[0];
|
||||
let fresh_price = client1.get_oracle_price(token_name).unwrap();
|
||||
let fresh_price = mango_client.get_oracle_price(token_name).await.unwrap();
|
||||
log::info!("{} Updated price is {:?}", token_name, fresh_price.price);
|
||||
if let Ok(mut price) = price.write() {
|
||||
*price = I80F48::from_num(fresh_price.price)
|
||||
/ I80F48::from_num(10u64.pow(-fresh_price.expo as u32));
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,7 +151,10 @@ pub async fn loop_blocking_orders(
|
|||
let mut interval = time::interval(Duration::from_secs(5));
|
||||
|
||||
// Cancel existing orders
|
||||
let orders: Vec<u128> = mango_client.serum3_cancel_all_orders(&market_name).unwrap();
|
||||
let orders: Vec<u128> = mango_client
|
||||
.serum3_cancel_all_orders(&market_name)
|
||||
.await
|
||||
.unwrap();
|
||||
log::info!("Cancelled orders - {:?} for {}", orders, market_name);
|
||||
|
||||
loop {
|
||||
|
@ -175,8 +164,8 @@ pub async fn loop_blocking_orders(
|
|||
let market_name = market_name.clone();
|
||||
let price = price.clone();
|
||||
|
||||
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
|
||||
client.serum3_settle_funds(&market_name)?;
|
||||
let res = (|| async move {
|
||||
client.serum3_settle_funds(&market_name).await?;
|
||||
|
||||
let fresh_price = match price.read() {
|
||||
Ok(price) => *price,
|
||||
|
@ -188,7 +177,8 @@ pub async fn loop_blocking_orders(
|
|||
let fresh_price = fresh_price.to_num::<f64>();
|
||||
|
||||
let bid_price = fresh_price + fresh_price * 0.1;
|
||||
let res = client.serum3_place_order(
|
||||
let res = client
|
||||
.serum3_place_order(
|
||||
&market_name,
|
||||
Serum3Side::Bid,
|
||||
bid_price,
|
||||
|
@ -197,7 +187,8 @@ pub async fn loop_blocking_orders(
|
|||
Serum3OrderType::ImmediateOrCancel,
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
|
||||
10,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
log::error!("Error while placing taker bid {:#?}", e)
|
||||
} else {
|
||||
|
@ -205,7 +196,8 @@ pub async fn loop_blocking_orders(
|
|||
}
|
||||
|
||||
let ask_price = fresh_price - fresh_price * 0.1;
|
||||
let res = client.serum3_place_order(
|
||||
let res = client
|
||||
.serum3_place_order(
|
||||
&market_name,
|
||||
Serum3Side::Ask,
|
||||
ask_price,
|
||||
|
@ -214,7 +206,8 @@ pub async fn loop_blocking_orders(
|
|||
Serum3OrderType::ImmediateOrCancel,
|
||||
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
|
||||
10,
|
||||
);
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = res {
|
||||
log::error!("Error while placing taker ask {:#?}", e)
|
||||
} else {
|
||||
|
@ -222,18 +215,11 @@ pub async fn loop_blocking_orders(
|
|||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})()
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(inner_res) => {
|
||||
if inner_res.is_err() {
|
||||
log::error!("{}", inner_res.unwrap_err());
|
||||
}
|
||||
}
|
||||
Err(join_error) => {
|
||||
log::error!("{}", join_error);
|
||||
}
|
||||
if let Err(err) = res {
|
||||
log::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError};
|
||||
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||||
use mango_v4::health::{HealthCache, HealthType};
|
||||
use mango_v4::state::{
|
||||
Bank, MangoAccountValue, PerpMarketIndex, Serum3Orders, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
Bank, MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX,
|
||||
};
|
||||
use solana_sdk::signature::Signature;
|
||||
|
||||
use itertools::Itertools;
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use rand::seq::SliceRandom;
|
||||
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
|
@ -17,7 +18,7 @@ pub struct Config {
|
|||
pub refresh_timeout: Duration,
|
||||
}
|
||||
|
||||
pub fn jupiter_market_can_buy(
|
||||
pub async fn jupiter_market_can_buy(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
|
@ -41,10 +42,11 @@ pub fn jupiter_market_can_buy(
|
|||
slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn jupiter_market_can_sell(
|
||||
pub async fn jupiter_market_can_sell(
|
||||
mango_client: &MangoClient,
|
||||
token: TokenIndex,
|
||||
quote_token: TokenIndex,
|
||||
|
@ -68,6 +70,7 @@ pub fn jupiter_market_can_sell(
|
|||
slippage,
|
||||
client::JupiterSwapMode::ExactOut,
|
||||
)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
|
@ -79,40 +82,50 @@ struct LiquidateHelper<'a> {
|
|||
health_cache: &'a HealthCache,
|
||||
maint_health: I80F48,
|
||||
liqor_min_health_ratio: I80F48,
|
||||
allowed_asset_tokens: HashSet<Pubkey>,
|
||||
allowed_liab_tokens: HashSet<Pubkey>,
|
||||
}
|
||||
|
||||
impl<'a> LiquidateHelper<'a> {
|
||||
fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn serum3_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
// look for any open serum orders or settleable balances
|
||||
let serum_force_cancels = self
|
||||
.liqee
|
||||
.active_serum3_orders()
|
||||
.map(|orders| {
|
||||
let serum_oos: anyhow::Result<Vec<_>> = stream::iter(self.liqee.active_serum3_orders())
|
||||
.then(|orders| async {
|
||||
let open_orders_account = self
|
||||
.account_fetcher
|
||||
.fetch_raw_account(&orders.open_orders)?;
|
||||
.fetch_raw_account(&orders.open_orders)
|
||||
.await?;
|
||||
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
|
||||
Ok((*orders, *open_orders))
|
||||
})
|
||||
.try_collect()
|
||||
.await;
|
||||
let serum_force_cancels = serum_oos?
|
||||
.into_iter()
|
||||
.filter_map(|(orders, open_orders)| {
|
||||
let can_force_cancel = open_orders.native_coin_total > 0
|
||||
|| open_orders.native_pc_total > 0
|
||||
|| open_orders.referrer_rebates_accrued > 0;
|
||||
if can_force_cancel {
|
||||
Ok(Some(*orders))
|
||||
Some(orders)
|
||||
} else {
|
||||
Ok(None)
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map_ok(|v| v)
|
||||
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
|
||||
.collect::<Vec<_>>();
|
||||
if serum_force_cancels.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
// Cancel all orders on a random serum market
|
||||
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||
let sig = self.client.serum3_liq_force_cancel_orders(
|
||||
let sig = self
|
||||
.client
|
||||
.serum3_liq_force_cancel_orders(
|
||||
(self.pubkey, &self.liqee),
|
||||
serum_orders.market_index,
|
||||
&serum_orders.open_orders,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -123,7 +136,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let perp_force_cancels = self
|
||||
.liqee
|
||||
.active_perp_positions()
|
||||
|
@ -137,7 +150,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let perp_market_index = *perp_force_cancels.choose(&mut rand::thread_rng()).unwrap();
|
||||
let sig = self
|
||||
.client
|
||||
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)?;
|
||||
.perp_liq_force_cancel_orders((self.pubkey, &self.liqee), perp_market_index)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -148,11 +162,11 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn perp_liq_base_position(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let mut perp_base_positions = self
|
||||
.liqee
|
||||
.active_perp_positions()
|
||||
.map(|pp| {
|
||||
async fn perp_liq_base_position(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let all_perp_base_positions: anyhow::Result<
|
||||
Vec<Option<(PerpMarketIndex, i64, I80F48, I80F48)>>,
|
||||
> = stream::iter(self.liqee.active_perp_positions())
|
||||
.then(|pp| async {
|
||||
let base_lots = pp.base_position_lots();
|
||||
if base_lots == 0 {
|
||||
return Ok(None);
|
||||
|
@ -160,7 +174,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let perp = self.client.context.perp(pp.market_index);
|
||||
let oracle = self
|
||||
.account_fetcher
|
||||
.fetch_raw_account(&perp.market.oracle)?;
|
||||
.fetch_raw_account(&perp.market.oracle)
|
||||
.await?;
|
||||
let price = perp.market.oracle_price(
|
||||
&KeyedAccountSharedData::new(perp.market.oracle, oracle.into()),
|
||||
None,
|
||||
|
@ -172,8 +187,12 @@ impl<'a> LiquidateHelper<'a> {
|
|||
I80F48::from(base_lots.abs()) * price,
|
||||
)))
|
||||
})
|
||||
.filter_map_ok(|v| v)
|
||||
.collect::<anyhow::Result<Vec<(PerpMarketIndex, i64, I80F48, I80F48)>>>()?;
|
||||
.try_collect()
|
||||
.await;
|
||||
let mut perp_base_positions = all_perp_base_positions?
|
||||
.into_iter()
|
||||
.filter_map(|x| x)
|
||||
.collect::<Vec<_>>();
|
||||
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
|
||||
|
||||
if perp_base_positions.is_empty() {
|
||||
|
@ -194,10 +213,12 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let mut liqor = self
|
||||
.account_fetcher
|
||||
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
||||
.await
|
||||
.context("getting liquidator account")?;
|
||||
liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?;
|
||||
let health_cache =
|
||||
health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
||||
.await
|
||||
.expect("always ok");
|
||||
health_cache.max_perp_for_health_ratio(
|
||||
*perp_market_index,
|
||||
|
@ -208,11 +229,14 @@ impl<'a> LiquidateHelper<'a> {
|
|||
};
|
||||
log::info!("computed max_base_transfer to be {max_base_transfer_abs}");
|
||||
|
||||
let sig = self.client.perp_liq_base_position(
|
||||
let sig = self
|
||||
.client
|
||||
.perp_liq_base_position(
|
||||
(self.pubkey, &self.liqee),
|
||||
*perp_market_index,
|
||||
side_signum * max_base_transfer_abs,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -223,7 +247,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn perp_settle_pnl(&self) -> anyhow::Result<Option<Signature>> {
|
||||
let perp_settle_health = self.health_cache.perp_settle_health();
|
||||
let mut perp_settleable_pnl = self
|
||||
.liqee
|
||||
|
@ -262,7 +286,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
perp_index,
|
||||
direction,
|
||||
2,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
if counters.is_empty() {
|
||||
// If we can't settle some positive PNL because we're lacking a suitable counterparty,
|
||||
// then liquidation should continue, even though this step produced no transaction
|
||||
|
@ -270,7 +295,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
self.pubkey);
|
||||
continue;
|
||||
}
|
||||
let (counter_key, counter_acc, _) = counters.first().unwrap();
|
||||
let (counter_key, counter_acc, counter_pnl) = counters.first().unwrap();
|
||||
|
||||
log::info!("Trying to settle perp pnl account: {} market_index: {perp_index} amount: {pnl} against {counter_key} with pnl: {counter_pnl}", self.pubkey);
|
||||
|
||||
let (account_a, account_b) = if pnl > 0 {
|
||||
((self.pubkey, self.liqee), (counter_key, counter_acc))
|
||||
|
@ -279,7 +306,8 @@ impl<'a> LiquidateHelper<'a> {
|
|||
};
|
||||
let sig = self
|
||||
.client
|
||||
.perp_settle_pnl(perp_index, account_a, account_b)?;
|
||||
.perp_settle_pnl(perp_index, account_a, account_b)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}",
|
||||
self.pubkey,
|
||||
|
@ -290,7 +318,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
return Ok(None);
|
||||
}
|
||||
|
||||
fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if self.health_cache.has_liquidatable_assets() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
@ -312,12 +340,15 @@ impl<'a> LiquidateHelper<'a> {
|
|||
}
|
||||
let (perp_market_index, _) = perp_bankruptcies.first().unwrap();
|
||||
|
||||
let sig = self.client.perp_liq_bankruptcy(
|
||||
let sig = self
|
||||
.client
|
||||
.perp_liq_bankruptcy(
|
||||
(self.pubkey, &self.liqee),
|
||||
*perp_market_index,
|
||||
// Always use the max amount, since the health effect is always positive
|
||||
u64::MAX,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
|
||||
self.pubkey,
|
||||
|
@ -328,18 +359,18 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
|
||||
let mut tokens = self
|
||||
.liqee
|
||||
.active_token_positions()
|
||||
.map(|token_position| {
|
||||
async fn tokens(&self) -> anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> {
|
||||
let tokens_maybe: anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>> =
|
||||
stream::iter(self.liqee.active_token_positions())
|
||||
.then(|token_position| async {
|
||||
let token = self.client.context.token(token_position.token_index);
|
||||
let bank = self
|
||||
.account_fetcher
|
||||
.fetch::<Bank>(&token.mint_info.first_bank())?;
|
||||
let oracle = self
|
||||
.account_fetcher
|
||||
.fetch_raw_account(&token.mint_info.oracle)?;
|
||||
.fetch_raw_account(&token.mint_info.oracle)
|
||||
.await?;
|
||||
let price = bank.oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
None,
|
||||
|
@ -350,12 +381,14 @@ impl<'a> LiquidateHelper<'a> {
|
|||
token_position.native(&bank) * price,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
|
||||
.try_collect()
|
||||
.await;
|
||||
let mut tokens = tokens_maybe?;
|
||||
tokens.sort_by(|a, b| a.2.cmp(&b.2));
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
fn max_token_liab_transfer(
|
||||
async fn max_token_liab_transfer(
|
||||
&self,
|
||||
source: TokenIndex,
|
||||
target: TokenIndex,
|
||||
|
@ -363,6 +396,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
let mut liqor = self
|
||||
.account_fetcher
|
||||
.fetch_fresh_mango_account(&self.client.mango_account_address)
|
||||
.await
|
||||
.context("getting liquidator account")?;
|
||||
|
||||
// Ensure the tokens are activated, so they appear in the health cache and
|
||||
|
@ -371,10 +405,11 @@ impl<'a> LiquidateHelper<'a> {
|
|||
liqor.ensure_token_position(target)?;
|
||||
|
||||
let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor)
|
||||
.await
|
||||
.expect("always ok");
|
||||
|
||||
let source_bank = self.client.first_bank(source)?;
|
||||
let target_bank = self.client.first_bank(target)?;
|
||||
let source_bank = self.client.first_bank(source).await?;
|
||||
let target_bank = self.client.first_bank(target).await?;
|
||||
|
||||
let source_price = health_cache.token_info(source).unwrap().prices.oracle;
|
||||
let target_price = health_cache.token_info(target).unwrap().prices.oracle;
|
||||
|
@ -394,19 +429,21 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(amount)
|
||||
}
|
||||
|
||||
fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() {
|
||||
async fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if !self.health_cache.has_spot_assets() || !self.health_cache.has_spot_borrows() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let tokens = self.tokens()?;
|
||||
let tokens = self.tokens().await?;
|
||||
|
||||
let asset_token_index = tokens
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
|
||||
asset_usdc_equivalent.is_positive()
|
||||
&& jupiter_market_can_sell(self.client, *asset_token_index, QUOTE_TOKEN_INDEX)
|
||||
&& self
|
||||
.allowed_asset_tokens
|
||||
.contains(&self.client.context.token(*asset_token_index).mint_info.mint)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -420,7 +457,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||
&& self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -433,6 +472,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
|
||||
let max_liab_transfer = self
|
||||
.max_token_liab_transfer(liab_token_index, asset_token_index)
|
||||
.await
|
||||
.context("getting max_liab_transfer")?;
|
||||
|
||||
//
|
||||
|
@ -447,6 +487,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
liab_token_index,
|
||||
max_liab_transfer,
|
||||
)
|
||||
.await
|
||||
.context("sending liq_token_with_token")?;
|
||||
log::info!(
|
||||
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
|
||||
|
@ -457,12 +498,12 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
async fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
|
||||
if !self.health_cache.can_call_spot_bankruptcy() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let tokens = self.tokens()?;
|
||||
let tokens = self.tokens().await?;
|
||||
|
||||
if tokens.is_empty() {
|
||||
anyhow::bail!(
|
||||
|
@ -474,7 +515,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.iter()
|
||||
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
|
||||
liab_usdc_equivalent.is_negative()
|
||||
&& jupiter_market_can_buy(self.client, *liab_token_index, QUOTE_TOKEN_INDEX)
|
||||
&& self
|
||||
.allowed_liab_tokens
|
||||
.contains(&self.client.context.token(*liab_token_index).mint_info.mint)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
|
@ -486,8 +529,9 @@ impl<'a> LiquidateHelper<'a> {
|
|||
.0;
|
||||
|
||||
let quote_token_index = 0;
|
||||
let max_liab_transfer =
|
||||
self.max_token_liab_transfer(liab_token_index, quote_token_index)?;
|
||||
let max_liab_transfer = self
|
||||
.max_token_liab_transfer(liab_token_index, quote_token_index)
|
||||
.await?;
|
||||
|
||||
let sig = self
|
||||
.client
|
||||
|
@ -496,6 +540,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
liab_token_index,
|
||||
max_liab_transfer,
|
||||
)
|
||||
.await
|
||||
.context("sending liq_token_bankruptcy")?;
|
||||
log::info!(
|
||||
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
|
||||
|
@ -506,7 +551,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
Ok(Some(sig))
|
||||
}
|
||||
|
||||
fn send_liq_tx(&self) -> anyhow::Result<Signature> {
|
||||
async fn send_liq_tx(&self) -> anyhow::Result<Signature> {
|
||||
// TODO: Should we make an attempt to settle positive PNL first?
|
||||
// The problem with it is that small market movements can continuously create
|
||||
// small amounts of new positive PNL while base_position > 0.
|
||||
|
@ -517,14 +562,14 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// }
|
||||
|
||||
// Try to close orders before touching the user's positions
|
||||
if let Some(txsig) = self.perp_close_orders()? {
|
||||
if let Some(txsig) = self.perp_close_orders().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
if let Some(txsig) = self.serum3_close_orders()? {
|
||||
if let Some(txsig) = self.serum3_close_orders().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
if let Some(txsig) = self.perp_liq_base_position()? {
|
||||
if let Some(txsig) = self.perp_liq_base_position().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
|
@ -533,21 +578,21 @@ impl<'a> LiquidateHelper<'a> {
|
|||
// It's possible that some positive pnl can't be settled (if there's
|
||||
// no liquid counterparty) and that some negative pnl can't be settled
|
||||
// (if the liqee isn't liquid enough).
|
||||
if let Some(txsig) = self.perp_settle_pnl()? {
|
||||
if let Some(txsig) = self.perp_settle_pnl().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
if let Some(txsig) = self.token_liq()? {
|
||||
if let Some(txsig) = self.token_liq().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
// Socialize/insurance fund unsettleable negative pnl
|
||||
if let Some(txsig) = self.perp_liq_bankruptcy()? {
|
||||
if let Some(txsig) = self.perp_liq_bankruptcy().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
// Socialize/insurance fund unliquidatable borrows
|
||||
if let Some(txsig) = self.token_liq_bankruptcy()? {
|
||||
if let Some(txsig) = self.token_liq_bankruptcy().await? {
|
||||
return Ok(txsig);
|
||||
}
|
||||
|
||||
|
@ -562,7 +607,7 @@ impl<'a> LiquidateHelper<'a> {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn maybe_liquidate_account(
|
||||
pub async fn maybe_liquidate_account(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
pubkey: &Pubkey,
|
||||
|
@ -571,8 +616,9 @@ pub fn maybe_liquidate_account(
|
|||
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
|
||||
|
||||
let account = account_fetcher.fetch_mango_account(pubkey)?;
|
||||
let health_cache =
|
||||
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
|
||||
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
||||
.await
|
||||
.expect("always ok");
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
if !health_cache.is_liquidatable() {
|
||||
return Ok(false);
|
||||
|
@ -588,15 +634,24 @@ pub fn maybe_liquidate_account(
|
|||
// Fetch a fresh account and re-compute
|
||||
// This is -- unfortunately -- needed because the websocket streams seem to not
|
||||
// be great at providing timely updates to the account data.
|
||||
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
|
||||
let health_cache =
|
||||
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
|
||||
let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?;
|
||||
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
||||
.await
|
||||
.expect("always ok");
|
||||
if !health_cache.is_liquidatable() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let maint_health = health_cache.health(HealthType::Maint);
|
||||
|
||||
let all_token_mints = HashSet::from_iter(
|
||||
mango_client
|
||||
.context
|
||||
.tokens
|
||||
.values()
|
||||
.map(|c| c.mint_info.mint),
|
||||
);
|
||||
|
||||
// try liquidating
|
||||
let txsig = LiquidateHelper {
|
||||
client: mango_client,
|
||||
|
@ -606,15 +661,21 @@ pub fn maybe_liquidate_account(
|
|||
health_cache: &health_cache,
|
||||
maint_health,
|
||||
liqor_min_health_ratio,
|
||||
allowed_asset_tokens: all_token_mints.clone(),
|
||||
allowed_liab_tokens: all_token_mints,
|
||||
}
|
||||
.send_liq_tx()?;
|
||||
.send_liq_tx()
|
||||
.await?;
|
||||
|
||||
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
||||
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
||||
let slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
|
||||
if let Err(e) = account_fetcher
|
||||
.refresh_accounts_via_rpc_until_slot(
|
||||
&[*pubkey, mango_client.mango_account_address],
|
||||
slot,
|
||||
config.refresh_timeout,
|
||||
) {
|
||||
)
|
||||
.await
|
||||
{
|
||||
log::info!("could not refresh after liquidation: {}", e);
|
||||
}
|
||||
|
||||
|
@ -622,14 +683,14 @@ pub fn maybe_liquidate_account(
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn maybe_liquidate_one<'a>(
|
||||
pub async fn maybe_liquidate_one<'a>(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
accounts: impl Iterator<Item = &'a Pubkey>,
|
||||
config: &Config,
|
||||
) -> bool {
|
||||
for pubkey in accounts {
|
||||
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
|
||||
match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config).await {
|
||||
Err(err) => {
|
||||
// Not all errors need to be raised to the user's attention.
|
||||
let mut log_level = log::Level::Error;
|
||||
|
|
|
@ -114,13 +114,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
// Reading accounts from chain_data
|
||||
let account_fetcher = Arc::new(chain_data::AccountFetcher {
|
||||
chain_data: chain_data.clone(),
|
||||
rpc: client.rpc(),
|
||||
rpc: client.rpc_async(),
|
||||
});
|
||||
|
||||
let mango_account = account_fetcher.fetch_fresh_mango_account(&cli.liqor_mango_account)?;
|
||||
let mango_account = account_fetcher
|
||||
.fetch_fresh_mango_account(&cli.liqor_mango_account)
|
||||
.await?;
|
||||
let mango_group = mango_account.fixed.group;
|
||||
|
||||
let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?;
|
||||
let group_context = MangoGroupContext::new_from_rpc(&client.rpc_async(), mango_group).await?;
|
||||
|
||||
let mango_oracles = group_context
|
||||
.tokens
|
||||
|
@ -258,7 +260,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
std::iter::once(&account_write.pubkey),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
).await?;
|
||||
}
|
||||
|
||||
if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) {
|
||||
|
@ -280,7 +282,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
mango_accounts.iter(),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
).await?;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -313,12 +315,12 @@ async fn main() -> anyhow::Result<()> {
|
|||
mango_accounts.iter(),
|
||||
&liq_config,
|
||||
&rebalance_config,
|
||||
)?;
|
||||
).await?;
|
||||
},
|
||||
|
||||
_ = rebalance_interval.tick() => {
|
||||
if one_snapshot_done {
|
||||
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config) {
|
||||
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config).await {
|
||||
log::error!("failed to rebalance liqor: {:?}", err);
|
||||
|
||||
// Workaround: We really need a sequence enforcer in the liquidator since we don't want to
|
||||
|
@ -332,20 +334,20 @@ async fn main() -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
fn liquidate<'a>(
|
||||
async fn liquidate<'a>(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
accounts: impl Iterator<Item = &'a Pubkey>,
|
||||
config: &liquidate::Config,
|
||||
rebalance_config: &rebalance::Config,
|
||||
) -> anyhow::Result<()> {
|
||||
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config) {
|
||||
if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let liqor = &mango_client.mango_account_address;
|
||||
if let Err(err) =
|
||||
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config)
|
||||
rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config).await
|
||||
{
|
||||
log::error!("failed to rebalance liqor: {:?}", err);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
|
|||
|
||||
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||||
|
||||
use futures::{stream, StreamExt, TryStreamExt};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
|
||||
pub struct Config {
|
||||
|
@ -20,14 +21,14 @@ struct TokenState {
|
|||
}
|
||||
|
||||
impl TokenState {
|
||||
fn new_position(
|
||||
async fn new_position(
|
||||
token: &TokenContext,
|
||||
position: &TokenPosition,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<Self> {
|
||||
let bank = Self::bank(token, account_fetcher)?;
|
||||
Ok(Self {
|
||||
price: Self::fetch_price(token, &bank, account_fetcher)?,
|
||||
price: Self::fetch_price(token, &bank, account_fetcher).await?,
|
||||
native_position: position.native(&bank),
|
||||
})
|
||||
}
|
||||
|
@ -39,12 +40,14 @@ impl TokenState {
|
|||
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
|
||||
}
|
||||
|
||||
fn fetch_price(
|
||||
async fn fetch_price(
|
||||
token: &TokenContext,
|
||||
bank: &Bank,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
) -> anyhow::Result<I80F48> {
|
||||
let oracle = account_fetcher.fetch_raw_account(&token.mint_info.oracle)?;
|
||||
let oracle = account_fetcher
|
||||
.fetch_raw_account(&token.mint_info.oracle)
|
||||
.await?;
|
||||
bank.oracle_price(
|
||||
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
|
||||
None,
|
||||
|
@ -54,7 +57,7 @@ impl TokenState {
|
|||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn zero_all_non_quote(
|
||||
pub async fn zero_all_non_quote(
|
||||
mango_client: &MangoClient,
|
||||
account_fetcher: &chain_data::AccountFetcher,
|
||||
mango_account_address: &Pubkey,
|
||||
|
@ -67,27 +70,32 @@ pub fn zero_all_non_quote(
|
|||
|
||||
let account = account_fetcher.fetch_mango_account(mango_account_address)?;
|
||||
|
||||
let tokens = account
|
||||
.active_token_positions()
|
||||
.map(|token_position| {
|
||||
let tokens: anyhow::Result<HashMap<TokenIndex, TokenState>> =
|
||||
stream::iter(account.active_token_positions())
|
||||
.then(|token_position| async {
|
||||
let token = mango_client.context.token(token_position.token_index);
|
||||
Ok((
|
||||
token.token_index,
|
||||
TokenState::new_position(token, token_position, account_fetcher)?,
|
||||
TokenState::new_position(token, token_position, account_fetcher).await?,
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?;
|
||||
.try_collect()
|
||||
.await;
|
||||
let tokens = tokens?;
|
||||
log::trace!("account tokens: {:?}", tokens);
|
||||
|
||||
// Function to refresh the mango account after the txsig confirmed. Returns false on timeout.
|
||||
let refresh_mango_account =
|
||||
|account_fetcher: &chain_data::AccountFetcher, txsig| -> anyhow::Result<bool> {
|
||||
let max_slot = account_fetcher.transaction_max_slot(&[txsig])?;
|
||||
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
|
||||
let refresh_mango_account = |txsig| async move {
|
||||
let res: anyhow::Result<bool> = {
|
||||
let max_slot = account_fetcher.transaction_max_slot(&[txsig]).await?;
|
||||
if let Err(e) = account_fetcher
|
||||
.refresh_accounts_via_rpc_until_slot(
|
||||
&[*mango_account_address],
|
||||
max_slot,
|
||||
config.refresh_timeout,
|
||||
) {
|
||||
)
|
||||
.await
|
||||
{
|
||||
// If we don't get fresh data, maybe the tx landed on a fork?
|
||||
// Rebalance is technically still ok.
|
||||
log::info!("could not refresh account data: {}", e);
|
||||
|
@ -95,6 +103,8 @@ pub fn zero_all_non_quote(
|
|||
}
|
||||
Ok(true)
|
||||
};
|
||||
res
|
||||
};
|
||||
|
||||
for (token_index, token_state) in tokens {
|
||||
let token = mango_client.context.token(token_index);
|
||||
|
@ -120,13 +130,15 @@ pub fn zero_all_non_quote(
|
|||
|
||||
if amount > dust_threshold {
|
||||
// Sell
|
||||
let txsig = mango_client.jupiter_swap(
|
||||
let txsig = mango_client
|
||||
.jupiter_swap(
|
||||
token_mint,
|
||||
quote_mint,
|
||||
amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactIn,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"sold {} {} for {} in tx {}",
|
||||
token.native_to_ui(amount),
|
||||
|
@ -134,12 +146,13 @@ pub fn zero_all_non_quote(
|
|||
quote_token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
if !refresh_mango_account(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, account_fetcher)?;
|
||||
amount = mango_client
|
||||
.mango_account()?
|
||||
.mango_account()
|
||||
.await?
|
||||
.token_position_and_raw_index(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
|
@ -147,13 +160,15 @@ pub fn zero_all_non_quote(
|
|||
// Buy
|
||||
let buy_amount = (-token_state.native_position).ceil()
|
||||
+ (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
|
||||
let txsig = mango_client.jupiter_swap(
|
||||
let txsig = mango_client
|
||||
.jupiter_swap(
|
||||
quote_mint,
|
||||
token_mint,
|
||||
buy_amount.to_num::<u64>(),
|
||||
config.slippage,
|
||||
client::JupiterSwapMode::ExactOut,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
log::info!(
|
||||
"bought {} {} for {} in tx {}",
|
||||
token.native_to_ui(buy_amount),
|
||||
|
@ -161,12 +176,13 @@ pub fn zero_all_non_quote(
|
|||
quote_token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
if !refresh_mango_account(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
let bank = TokenState::bank(token, account_fetcher)?;
|
||||
amount = mango_client
|
||||
.mango_account()?
|
||||
.mango_account()
|
||||
.await?
|
||||
.token_position_and_raw_index(token_index)
|
||||
.map(|(position, _)| position.native(&bank))
|
||||
.unwrap_or(I80F48::ZERO);
|
||||
|
@ -176,15 +192,16 @@ pub fn zero_all_non_quote(
|
|||
// TokenPosition is freed up
|
||||
if amount > 0 && amount <= dust_threshold {
|
||||
let allow_borrow = false;
|
||||
let txsig =
|
||||
mango_client.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)?;
|
||||
let txsig = mango_client
|
||||
.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)
|
||||
.await?;
|
||||
log::info!(
|
||||
"withdrew {} {} to liqor wallet in {}",
|
||||
token.native_to_ui(amount),
|
||||
token.name,
|
||||
txsig
|
||||
);
|
||||
if !refresh_mango_account(account_fetcher, txsig)? {
|
||||
if !refresh_mango_account(txsig).await? {
|
||||
return Ok(());
|
||||
}
|
||||
} else if amount > dust_threshold {
|
||||
|
|
|
@ -175,7 +175,10 @@ pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::
|
|||
loop {
|
||||
info!("connecting to solana websocket streams");
|
||||
let out = feed_data(&config, mango_oracles.clone(), sender.clone());
|
||||
let _ = out.await;
|
||||
let result = out.await;
|
||||
if let Err(err) = result {
|
||||
warn!("websocket stream error: {err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::state::{
|
||||
|
@ -12,8 +11,6 @@ use crate::util::checked_math as cm;
|
|||
|
||||
use super::*;
|
||||
|
||||
const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001);
|
||||
|
||||
/// Information about prices for a bank or perp market.
|
||||
#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)]
|
||||
pub struct Prices {
|
||||
|
@ -442,11 +439,15 @@ impl HealthCache {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self.token_infos.iter().any(|ti| {
|
||||
pub fn has_spot_assets(&self) -> bool {
|
||||
self.token_infos.iter().any(|ti| {
|
||||
// can use token_liq_with_token
|
||||
ti.balance_native.is_positive()
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_liquidatable_assets(&self) -> bool {
|
||||
let spot_liquidatable = self.has_spot_assets();
|
||||
// can use serum3_liq_force_cancel_orders
|
||||
let serum3_cancelable = self
|
||||
.serum3_infos
|
||||
|
@ -455,10 +456,11 @@ impl HealthCache {
|
|||
let perp_liquidatable = self.perp_infos.iter().any(|p| {
|
||||
// can use perp_liq_base_position
|
||||
p.base_lots != 0
|
||||
// can use perp_settle_pnl
|
||||
|| p.quote > ONE_NATIVE_USDC_IN_USD // TODO: we're not guaranteed to be able to settle positive perp pnl!
|
||||
// can use perp_liq_force_cancel_orders
|
||||
|| p.has_open_orders
|
||||
// A remaining quote position can be reduced with perp_settle_pnl and that can improve health.
|
||||
// However, since it's not guaranteed that there is a counterparty, a positive perp quote position
|
||||
// does not prevent bankruptcy.
|
||||
});
|
||||
spot_liquidatable || serum3_cancelable || perp_liquidatable
|
||||
}
|
||||
|
|
|
@ -67,7 +67,6 @@ async function main() {
|
|||
|
||||
// close all banks
|
||||
for (const banks of group.banksMapByMint.values()) {
|
||||
console.log(banks[0].toString());
|
||||
sig = await client.tokenDeregister(group, banks[0].mint);
|
||||
console.log(
|
||||
`Removed token ${banks[0].name}, sig https://explorer.solana.com/tx/${sig}`,
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import {
|
||||
AddressLookupTableProgram,
|
||||
Connection,
|
||||
Keypair,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
import { buildVersionedTx } from '../utils';
|
||||
|
||||
//
|
||||
// Script which depoys a new mango group, and registers 3 tokens
|
||||
|
@ -14,23 +20,23 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
|
|||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
|
||||
]);
|
||||
|
||||
const STUB_PRICES = new Map([
|
||||
['USDC', 1.0],
|
||||
['BTC', 20000.0], // btc and usdc both have 6 decimals
|
||||
['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL
|
||||
['MNGO', 0.04], // same price/decimals as SOL for convenience
|
||||
['ETH', 1200.0], // eth and usdc both have 6 decimals
|
||||
['SOL', 0.015], // sol has 9 decimals, equivalent to $15 per SOL
|
||||
['MNGO', 0.02], // same price/decimals as SOL for convenience
|
||||
]);
|
||||
|
||||
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
|
||||
// and verified to have best liquidity for pair on https://openserum.io/
|
||||
const MAINNET_SERUM3_MARKETS = new Map([
|
||||
['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'],
|
||||
['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'],
|
||||
['ETH/USDC', 'FZxi3yWkE5mMjyaZj6utmYL54QQYfMCKMcLaQZq4UwnA'],
|
||||
['SOL/USDC', '8BnEgHoWFysVcuFFX7QztDmzuH8r5ZFvyP3sYwn1XTh6'],
|
||||
]);
|
||||
|
||||
const MIN_VAULT_TO_DEPOSITS_RATIO = 0.2;
|
||||
|
@ -130,17 +136,17 @@ async function main() {
|
|||
}
|
||||
|
||||
// register token 1
|
||||
console.log(`Registering BTC...`);
|
||||
const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!);
|
||||
const btcMainnetOracle = oracles.get('BTC');
|
||||
console.log(`Registering ETH...`);
|
||||
const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!);
|
||||
const ethMainnetOracle = oracles.get('ETH');
|
||||
try {
|
||||
await client.tokenRegister(
|
||||
group,
|
||||
btcMainnetMint,
|
||||
btcMainnetOracle,
|
||||
ethMainnetMint,
|
||||
ethMainnetOracle,
|
||||
defaultOracleConfig,
|
||||
1,
|
||||
'BTC',
|
||||
'ETH',
|
||||
defaultInterestRate,
|
||||
0.0,
|
||||
0.0001,
|
||||
|
@ -215,7 +221,7 @@ async function main() {
|
|||
0,
|
||||
'MNGO-PERP',
|
||||
defaultOracleConfig,
|
||||
9,
|
||||
6,
|
||||
10,
|
||||
100000, // base lots
|
||||
0.9,
|
||||
|
@ -235,14 +241,134 @@ async function main() {
|
|||
0,
|
||||
0,
|
||||
0,
|
||||
1.0,
|
||||
-1.0,
|
||||
2 * 60 * 60,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
await createAndPopulateAlt(client, admin);
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
async function createAndPopulateAlt(client: MangoClient, admin: Keypair) {
|
||||
let group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
|
||||
const connection = client.program.provider.connection;
|
||||
|
||||
// Create ALT, and set to group at index 0
|
||||
if (group.addressLookupTables[0].equals(PublicKey.default)) {
|
||||
try {
|
||||
console.log(`ALT: Creating`);
|
||||
const createIx = AddressLookupTableProgram.createLookupTable({
|
||||
authority: admin.publicKey,
|
||||
payer: admin.publicKey,
|
||||
recentSlot: await connection.getSlot('finalized'),
|
||||
});
|
||||
const createTx = await buildVersionedTx(
|
||||
client.program.provider as AnchorProvider,
|
||||
[createIx[0]],
|
||||
);
|
||||
let sig = await connection.sendTransaction(createTx);
|
||||
console.log(
|
||||
`...created ALT ${createIx[1]} https://explorer.solana.com/tx/${sig}`,
|
||||
);
|
||||
|
||||
console.log(`ALT: set at index 0 for group...`);
|
||||
sig = await client.altSet(group, createIx[1], 0);
|
||||
console.log(`...https://explorer.solana.com/tx/${sig}`);
|
||||
|
||||
group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Extend using mango v4 relevant pub keys
|
||||
try {
|
||||
let bankAddresses = Array.from(group.banksMapByMint.values())
|
||||
.flat()
|
||||
.map((bank) => [bank.publicKey, bank.oracle, bank.vault])
|
||||
.flat()
|
||||
.concat(
|
||||
Array.from(group.banksMapByMint.values())
|
||||
.flat()
|
||||
.map((mintInfo) => mintInfo.publicKey),
|
||||
);
|
||||
|
||||
let serum3MarketAddresses = Array.from(
|
||||
group.serum3MarketsMapByExternal.values(),
|
||||
)
|
||||
.flat()
|
||||
.map((serum3Market) => serum3Market.publicKey);
|
||||
|
||||
let serum3ExternalMarketAddresses = Array.from(
|
||||
group.serum3ExternalMarketsMap.values(),
|
||||
)
|
||||
.flat()
|
||||
.map((serum3ExternalMarket) => [
|
||||
serum3ExternalMarket.publicKey,
|
||||
serum3ExternalMarket.bidsAddress,
|
||||
serum3ExternalMarket.asksAddress,
|
||||
])
|
||||
.flat();
|
||||
|
||||
let perpMarketAddresses = Array.from(
|
||||
group.perpMarketsMapByMarketIndex.values(),
|
||||
)
|
||||
.flat()
|
||||
.map((perpMarket) => [
|
||||
perpMarket.publicKey,
|
||||
perpMarket.oracle,
|
||||
perpMarket.bids,
|
||||
perpMarket.asks,
|
||||
perpMarket.eventQueue,
|
||||
])
|
||||
.flat();
|
||||
|
||||
async function extendTable(addresses: PublicKey[]) {
|
||||
await group.reloadAll(client);
|
||||
const alt =
|
||||
await client.program.provider.connection.getAddressLookupTable(
|
||||
group.addressLookupTables[0],
|
||||
);
|
||||
|
||||
addresses = addresses.filter(
|
||||
(newAddress) =>
|
||||
alt.value?.state.addresses &&
|
||||
alt.value?.state.addresses.findIndex((addressInALt) =>
|
||||
addressInALt.equals(newAddress),
|
||||
) === -1,
|
||||
);
|
||||
if (addresses.length === 0) {
|
||||
return;
|
||||
}
|
||||
const extendIx = AddressLookupTableProgram.extendLookupTable({
|
||||
lookupTable: group.addressLookupTables[0],
|
||||
payer: admin.publicKey,
|
||||
authority: admin.publicKey,
|
||||
addresses,
|
||||
});
|
||||
const extendTx = await buildVersionedTx(
|
||||
client.program.provider as AnchorProvider,
|
||||
[extendIx],
|
||||
);
|
||||
let sig = await client.program.provider.connection.sendTransaction(
|
||||
extendTx,
|
||||
);
|
||||
console.log(`https://explorer.solana.com/tx/${sig}`);
|
||||
}
|
||||
|
||||
console.log(`ALT: extending using mango v4 relevant public keys`);
|
||||
await extendTable(bankAddresses);
|
||||
await extendTable(serum3MarketAddresses);
|
||||
await extendTable(serum3ExternalMarketAddresses);
|
||||
await extendTable(perpMarketAddresses);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +0,0 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
//
|
||||
// This script deposits some tokens, so other liquidation scripts can borrow.
|
||||
//
|
||||
|
||||
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
|
||||
const ACCOUNT_NUM = Number(process.env.ACCOUNT_NUM || 0);
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL!, options);
|
||||
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(admin);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'mainnet-beta',
|
||||
MANGO_V4_ID['mainnet-beta'],
|
||||
{
|
||||
idsSource: 'get-program-accounts',
|
||||
},
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = (await client.createAndFetchMangoAccount(
|
||||
group,
|
||||
ACCOUNT_NUM,
|
||||
'LIQTEST, FUNDING',
|
||||
8,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
))!;
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
const usdcMint = group.banksMapByName.get('USDC')![0].mint;
|
||||
const btcMint = group.banksMapByName.get('BTC')![0].mint;
|
||||
const solMint = group.banksMapByName.get('SOL')![0].mint;
|
||||
|
||||
// deposit
|
||||
try {
|
||||
console.log(`...depositing 5 USDC`);
|
||||
await client.tokenDeposit(group, mangoAccount, usdcMint, 5);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(`...depositing 0.0002 BTC`);
|
||||
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
console.log(`...depositing 0.15 SOL`);
|
||||
await client.tokenDeposit(group, mangoAccount, solMint, 0.15);
|
||||
await mangoAccount.reload(client);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -1,8 +1,9 @@
|
|||
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Bank } from '../accounts/bank';
|
||||
import { MangoAccount } from '../accounts/mangoAccount';
|
||||
import { PerpOrderSide, PerpOrderType } from '../accounts/perp';
|
||||
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../accounts/perp';
|
||||
import {
|
||||
Serum3OrderType,
|
||||
Serum3SelfTradeBehavior,
|
||||
|
@ -19,24 +20,30 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
|
|||
|
||||
// native prices
|
||||
const PRICES = {
|
||||
BTC: 20000.0,
|
||||
SOL: 0.04,
|
||||
ETH: 1200.0,
|
||||
SOL: 0.015,
|
||||
USDC: 1,
|
||||
MNGO: 0.04,
|
||||
MNGO: 0.02,
|
||||
};
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
|
||||
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
|
||||
['SOL', 'So11111111111111111111111111111111111111112'],
|
||||
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
|
||||
]);
|
||||
|
||||
const TOKEN_SCENARIOS: [string, string, number, string, number][] = [
|
||||
['LIQTEST, LIQOR', 'USDC', 1000000, 'USDC', 0],
|
||||
['LIQTEST, A: USDC, L: SOL', 'USDC', 1000 * PRICES.SOL, 'SOL', 920],
|
||||
['LIQTEST, A: SOL, L: USDC', 'SOL', 1000, 'USDC', 920 * PRICES.SOL],
|
||||
['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', (18 * PRICES.BTC) / PRICES.SOL],
|
||||
const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [
|
||||
[
|
||||
'LIQTEST, FUNDING',
|
||||
[
|
||||
['USDC', 5000000],
|
||||
['ETH', 100000],
|
||||
['SOL', 150000000],
|
||||
],
|
||||
[],
|
||||
],
|
||||
['LIQTEST, LIQOR', [['USDC', 1000000]], []],
|
||||
['LIQTEST, A: USDC, L: SOL', [['USDC', 1000 * PRICES.SOL]], [['SOL', 920]]],
|
||||
['LIQTEST, A: SOL, L: USDC', [['SOL', 1000]], [['USDC', 990 * PRICES.SOL]]],
|
||||
[
|
||||
'LIQTEST, A: ETH, L: SOL',
|
||||
[['ETH', 20]],
|
||||
[['SOL', (18 * PRICES.ETH) / PRICES.SOL]],
|
||||
],
|
||||
];
|
||||
|
||||
async function main() {
|
||||
|
@ -66,17 +73,17 @@ async function main() {
|
|||
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
|
||||
console.log(group.toString());
|
||||
|
||||
const MAINNET_MINTS = new Map([
|
||||
['USDC', group.banksMapByName.get('USDC')![0].mint],
|
||||
['ETH', group.banksMapByName.get('ETH')![0].mint],
|
||||
['SOL', group.banksMapByName.get('SOL')![0].mint],
|
||||
]);
|
||||
|
||||
const accounts = await client.getMangoAccountsForOwner(
|
||||
group,
|
||||
admin.publicKey,
|
||||
);
|
||||
let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum));
|
||||
const fundingAccount = accounts.find(
|
||||
(account) => account.name == 'LIQTEST, FUNDING',
|
||||
);
|
||||
if (!fundingAccount) {
|
||||
throw new Error('could not find funding account');
|
||||
}
|
||||
|
||||
async function createMangoAccount(name: string): Promise<MangoAccount> {
|
||||
const accountNum = maxAccountNum + 1;
|
||||
|
@ -89,8 +96,73 @@ async function main() {
|
|||
))!;
|
||||
}
|
||||
|
||||
async function setBankPrice(bank: Bank, price: number): Promise<void> {
|
||||
await client.stubOracleSet(group, bank.oracle, price);
|
||||
// reset stable price
|
||||
await client.tokenEdit(
|
||||
group,
|
||||
bank.mint,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
);
|
||||
}
|
||||
async function setPerpPrice(
|
||||
perpMarket: PerpMarket,
|
||||
price: number,
|
||||
): Promise<void> {
|
||||
await client.stubOracleSet(group, perpMarket.oracle, price);
|
||||
// reset stable price
|
||||
await client.perpEditMarket(
|
||||
group,
|
||||
perpMarket.perpMarketIndex,
|
||||
perpMarket.oracle,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
for (const scenario of TOKEN_SCENARIOS) {
|
||||
const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
|
||||
const [name, assets, liabs] = scenario;
|
||||
|
||||
// create account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
|
@ -99,9 +171,8 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
for (let [assetName, assetAmount] of assets) {
|
||||
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
|
||||
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
@ -109,12 +180,15 @@ async function main() {
|
|||
new BN(assetAmount),
|
||||
);
|
||||
await mangoAccount.reload(client);
|
||||
}
|
||||
|
||||
for (let [liabName, liabAmount] of liabs) {
|
||||
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
|
||||
|
||||
if (liabAmount > 0) {
|
||||
// temporarily drop the borrowed token value, so the borrow goes through
|
||||
const oracle = group.banksMapByName.get(liabName)![0].oracle;
|
||||
const bank = group.banksMapByName.get(liabName)![0];
|
||||
try {
|
||||
await client.stubOracleSet(group, oracle, PRICES[liabName] / 2);
|
||||
await setBankPrice(bank, PRICES[liabName] / 2);
|
||||
|
||||
await client.tokenWithdrawNative(
|
||||
group,
|
||||
|
@ -125,11 +199,22 @@ async function main() {
|
|||
);
|
||||
} finally {
|
||||
// restore the oracle
|
||||
await client.stubOracleSet(group, oracle, PRICES[liabName]);
|
||||
await setBankPrice(bank, PRICES[liabName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const accounts2 = await client.getMangoAccountsForOwner(
|
||||
group,
|
||||
admin.publicKey,
|
||||
);
|
||||
const fundingAccount = accounts2.find(
|
||||
(account) => account.name == 'LIQTEST, FUNDING',
|
||||
);
|
||||
if (!fundingAccount) {
|
||||
throw new Error('could not find funding account');
|
||||
}
|
||||
|
||||
// Serum order scenario
|
||||
{
|
||||
const name = 'LIQTEST, serum orders';
|
||||
|
@ -233,19 +318,18 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
|
||||
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
|
||||
const collateralBank = group.banksMapByName.get('SOL')![0];
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
new BN(100000),
|
||||
); // valued as $0.004 maint collateral
|
||||
new BN(300000),
|
||||
); // valued as 0.0003 SOL, $0.0045 maint collateral
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4);
|
||||
await setBankPrice(collateralBank, PRICES['SOL'] * 4);
|
||||
|
||||
try {
|
||||
await client.perpPlaceOrder(
|
||||
|
@ -253,9 +337,9 @@ async function main() {
|
|||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
PerpOrderSide.bid,
|
||||
1, // ui price that won't get hit
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.001, // ui price that won't get hit
|
||||
1.1, // ui base quantity, 11 base lots, 1.1 MNGO, $0.022
|
||||
0.022, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -263,7 +347,7 @@ async function main() {
|
|||
5,
|
||||
);
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
await setBankPrice(collateralBank, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,19 +361,18 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
|
||||
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
|
||||
const collateralBank = group.banksMapByName.get('SOL')![0];
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
new BN(100000),
|
||||
); // valued as $0.004 maint collateral
|
||||
new BN(300000),
|
||||
); // valued as 0.0003 SOL, $0.0045 maint collateral
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5);
|
||||
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
|
||||
|
||||
try {
|
||||
await client.perpPlaceOrder(
|
||||
|
@ -297,9 +380,9 @@ async function main() {
|
|||
fundingAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
PerpOrderSide.ask,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.03,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022 value, gain $0.033
|
||||
0.033, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -312,9 +395,9 @@ async function main() {
|
|||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
PerpOrderSide.bid,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.03,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022 value, cost $0.033
|
||||
0.033, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.market,
|
||||
false,
|
||||
|
@ -327,7 +410,7 @@ async function main() {
|
|||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
);
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
await setBankPrice(collateralBank, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,23 +424,22 @@ async function main() {
|
|||
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
|
||||
);
|
||||
|
||||
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
|
||||
const baseOracle = (await client.getStubOracle(group, baseMint))[0]
|
||||
.publicKey;
|
||||
const perpMarket = group.perpMarketsMapByName.get('MNGO-PERP')!;
|
||||
const perpIndex = perpMarket.perpMarketIndex;
|
||||
const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
|
||||
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
|
||||
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
|
||||
const collateralBank = group.banksMapByName.get('SOL')![0];
|
||||
|
||||
await client.tokenDepositNative(
|
||||
group,
|
||||
mangoAccount,
|
||||
collateralMint,
|
||||
new BN(100000),
|
||||
); // valued as $0.004 maint collateral
|
||||
new BN(300000),
|
||||
); // valued as $0.0045 maint collateral
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
try {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10);
|
||||
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
|
||||
|
||||
// Spot-borrow more than the collateral is worth
|
||||
await client.tokenWithdrawNative(
|
||||
|
@ -369,16 +451,16 @@ async function main() {
|
|||
);
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
// Execute two trades that leave the account with +$0.022 positive pnl
|
||||
await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2);
|
||||
// Execute two trades that leave the account with +$0.011 positive pnl
|
||||
await setPerpPrice(perpMarket, PRICES['MNGO'] / 2);
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
fundingAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.ask,
|
||||
20,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
0.01,
|
||||
1.1, // ui base quantity, 11 base lots, $0.011
|
||||
0.011, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -388,32 +470,29 @@ async function main() {
|
|||
await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.bid,
|
||||
20,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
0.01,
|
||||
1.1, // ui base quantity, 11 base lots, $0.011
|
||||
0.011, // ui quote quantity
|
||||
4200,
|
||||
PerpOrderType.market,
|
||||
false,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
await client.perpConsumeAllEvents(
|
||||
group,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
);
|
||||
await client.perpConsumeAllEvents(group, perpIndex);
|
||||
|
||||
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
|
||||
await setPerpPrice(perpMarket, PRICES['MNGO']);
|
||||
|
||||
await client.perpPlaceOrder(
|
||||
group,
|
||||
fundingAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.bid,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.02,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
4201,
|
||||
PerpOrderType.limit,
|
||||
false,
|
||||
|
@ -423,24 +502,21 @@ async function main() {
|
|||
await client.perpPlaceOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
perpIndex,
|
||||
PerpOrderSide.ask,
|
||||
40,
|
||||
0.0011, // ui base quantity, 11 base lots, $0.044
|
||||
0.044, // ui quote quantity
|
||||
0.02,
|
||||
1.1, // ui base quantity, 11 base lots, $0.022
|
||||
0.022, // ui quote quantity
|
||||
4201,
|
||||
PerpOrderType.market,
|
||||
false,
|
||||
0,
|
||||
5,
|
||||
);
|
||||
await client.perpConsumeAllEvents(
|
||||
group,
|
||||
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
|
||||
);
|
||||
await client.perpConsumeAllEvents(group, perpIndex);
|
||||
} finally {
|
||||
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
|
||||
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
|
||||
await setPerpPrice(perpMarket, PRICES['MNGO']);
|
||||
await setBankPrice(collateralBank, PRICES['SOL']);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue