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:
Christian Kamm 2022-12-16 13:10:46 +01:00 committed by GitHub
parent 1c36b1b493
commit 5a38506f04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1665 additions and 1343 deletions

3
Cargo.lock generated
View File

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

View File

@ -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(
input_mint,
output_mint,
cmd.amount,
cmd.slippage,
client::JupiterSwapMode::ExactIn,
)?;
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 } => {

View File

@ -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"
@ -29,4 +32,4 @@ tokio = { version = "1", features = ["full"] }
serde = "1.0.141"
serde_json = "1.0.82"
base64 = "0.13.0"
bincode = "1.3.3"
bincode = "1.3.3"

View File

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

View File

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

View File

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

View File

@ -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(
8,
&group.to_bytes(),
))])
) -> 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(
8,
&group.to_bytes(),
))])
) -> 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
}

View File

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

View File

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

View File

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

View File

@ -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,79 +92,63 @@ 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
let token_names = token_indices_clone
.iter()
.map(|token_index| client.context.token(*token_index).name.to_owned())
.join(",");
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();
let oracle = token.mint_info.oracle;
let mut ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenUpdateIndexAndRate {
group: token.mint_info.group,
mint_info: token.mint_info_address,
oracle,
instructions: solana_program::sysvar::instructions::id(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::TokenUpdateIndexAndRate {},
),
};
let mut banks = banks_for_a_token
.iter()
.map(|token_index| client.context.token(*token_index).name.to_owned())
.join(",");
.map(|bank_pubkey| AccountMeta {
pubkey: *bank_pubkey,
is_signer: false,
is_writable: true,
})
.collect::<Vec<_>>();
ix.accounts.append(&mut banks);
instructions.push(ix);
}
let pre = Instant::now();
let sig_result = client
.send_and_confirm_permissionless_tx(instructions)
.await;
let program = client.program();
let mut req = program.request();
req = req.instruction(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();
let oracle = token.mint_info.oracle;
let mut ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenUpdateIndexAndRate {
group: token.mint_info.group,
mint_info: token.mint_info_address,
oracle,
instructions: solana_program::sysvar::instructions::id(),
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::TokenUpdateIndexAndRate {},
),
};
let mut banks = banks_for_a_token
.iter()
.map(|bank_pubkey| AccountMeta {
pubkey: *bank_pubkey,
is_signer: false,
is_writable: true,
})
.collect::<Vec<_>>();
ix.accounts.append(&mut banks);
req = req.instruction(ix);
}
let pre = Instant::now();
let sig_result = req.send().map_err(prettify_client_error);
if let Err(e) = sig_result {
log::info!(
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
token_names,
pre.elapsed().as_millis(),
e
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
token_names,
pre.elapsed().as_millis(),
);
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);
}
if let Err(e) = sig_result {
log::info!(
"metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}",
token_names,
pre.elapsed().as_millis(),
e
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateTokensV4Success tokens={} durationMs={}",
token_names,
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
}
}
@ -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,67 +225,45 @@ 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 {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpConsumeEvents {
group: perp_market.group,
perp_market: pk,
event_queue: perp_market.event_queue,
},
None,
);
ams.append(&mut ams_);
ams
let pre = Instant::now();
let ix = Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpConsumeEvents {
group: perp_market.group,
perp_market: pk,
event_queue: perp_market.event_queue,
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpConsumeEvents { limit: 10 },
),
})
.send()
.map_err(prettify_client_error);
if let Err(e) = sig_result {
log::info!(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
e.to_string()
None,
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
);
log::info!("{:?}", sig_result);
}
ams.append(&mut event_ams);
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpConsumeEvents {
limit: 10,
}),
};
Ok(())
})
.await;
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).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(e) = sig_result {
log::info!(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name(),
pre.elapsed().as_millis(),
num_of_events,
);
log::info!("{:?}", sig_result);
}
}
}
@ -298,59 +279,39 @@ 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 {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpUpdateFunding {
group: perp_market.group,
perp_market: pk,
bids: perp_market.bids,
asks: perp_market.asks,
oracle: perp_market.oracle,
},
None,
),
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpUpdateFunding {},
),
})
.send()
.map_err(prettify_client_error);
if let Err(e) = sig_result {
log::error!(
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateFundingV4Success market={} durationMs={}",
perp_market.name(),
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
Ok(())
})
.await;
let pre = Instant::now();
let ix = Instruction {
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpUpdateFunding {
group: perp_market.group,
perp_market: pk,
bids: perp_market.bids,
asks: perp_market.asks,
oracle: perp_market.oracle,
},
None,
),
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpUpdateFunding {}),
};
let sig_result = client.send_and_confirm_permissionless_tx(vec![ix]).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(e) = sig_result {
log::error!(
"metricName=UpdateFundingV4Error market={} durationMs={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
e.to_string()
);
log::error!("{:?}", e)
} else {
log::info!(
"metricName=UpdateFundingV4Success market={} durationMs={}",
perp_market.name(),
pre.elapsed().as_millis(),
);
log::info!("{:?}", sig_result);
}
}
}

View File

@ -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,21 +89,19 @@ fn main() -> Result<(), anyhow::Error> {
Command::Taker { .. } => CommitmentConfig::confirmed(),
};
let mango_client = Arc::new(MangoClient::new_for_existing_account(
Client::new(
cluster,
commitment,
&owner,
Some(Duration::from_secs(cli.timeout)),
),
cli.mango_account,
owner,
)?);
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
let mango_client = Arc::new(
MangoClient::new_for_existing_account(
Client::new(
cluster,
commitment,
&owner,
Some(Duration::from_secs(cli.timeout)),
),
cli.mango_account,
owner,
)
.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
}
}
}

View File

@ -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,33 +130,15 @@ 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();
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);
}
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));
}
}
}
@ -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,16 +177,18 @@ 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(
&market_name,
Serum3Side::Bid,
bid_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
);
let res = client
.serum3_place_order(
&market_name,
Serum3Side::Bid,
bid_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
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,16 +196,18 @@ pub async fn loop_blocking_orders(
}
let ask_price = fresh_price - fresh_price * 0.1;
let res = client.serum3_place_order(
&market_name,
Serum3Side::Ask,
ask_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
Serum3OrderType::ImmediateOrCancel,
SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64,
10,
);
let res = client
.serum3_place_order(
&market_name,
Serum3Side::Ask,
ask_price,
0.0001,
Serum3SelfTradeBehavior::DecrementTake,
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);
}
}
}

View File

@ -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(
(self.pubkey, &self.liqee),
serum_orders.market_index,
&serum_orders.open_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(
(self.pubkey, &self.liqee),
*perp_market_index,
side_signum * max_base_transfer_abs,
)?;
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(
(self.pubkey, &self.liqee),
*perp_market_index,
// Always use the max amount, since the health effect is always positive
u64::MAX,
)?;
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,34 +359,36 @@ 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| {
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)?;
let price = bank.oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
None,
)?;
Ok((
token_position.token_index,
price,
token_position.native(&bank) * price,
))
})
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
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)
.await?;
let price = bank.oracle_price(
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
None,
)?;
Ok((
token_position.token_index,
price,
token_position.native(&bank) * price,
))
})
.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(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
) {
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;

View File

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

View File

@ -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 token = mango_client.context.token(token_position.token_index);
Ok((
token.token_index,
TokenState::new_position(token, token_position, account_fetcher)?,
))
})
.collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?;
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).await?,
))
})
.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(
&[*mango_account_address],
max_slot,
config.refresh_timeout,
) {
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(
token_mint,
quote_mint,
amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactIn,
)?;
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(
quote_mint,
token_mint,
buy_amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactOut,
)?;
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,22 +171,24 @@ async function main() {
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
for (let [assetName, assetAmount] of assets) {
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
await client.tokenDepositNative(
group,
mangoAccount,
assetMint,
new BN(assetAmount),
);
await mangoAccount.reload(client);
}
await client.tokenDepositNative(
group,
mangoAccount,
assetMint,
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']);
}
}