Merge remote-tracking branch 'origin/dev' into main

This commit is contained in:
tjs 2022-10-07 13:21:47 -04:00
commit 73039e1b39
99 changed files with 7667 additions and 2815 deletions

View File

@ -14,12 +14,21 @@
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"plugins": [
"@typescript-eslint"
],
"rules": {
"linebreak-style": ["error", "unix"],
"semi": ["error", "always"],
"linebreak-style": [
"error",
"unix"
],
"semi": [
"error",
"always"
],
"@typescript-eslint/no-non-null-assertion": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-explicit-any": 0
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-function-return-type": "warn"
}
}
}

View File

@ -184,9 +184,7 @@ describe('mango-v4', () => {
0,
'BTC-PERP',
0.1,
1,
6,
1,
10,
100,
0.975,
@ -196,9 +194,16 @@ describe('mango-v4', () => {
0.012,
0.0002,
0.0,
0,
0.05,
0.05,
100,
true,
true,
0,
0,
0,
0,
);
await group.reloadAll(envClient);
});

View File

@ -7,22 +7,27 @@ use anchor_client::ClientError;
use anchor_lang::AccountDeserialize;
use solana_client::rpc_client::RpcClient;
use solana_sdk::account::Account;
use solana_sdk::account::{AccountSharedData, ReadableAccount};
use solana_sdk::pubkey::Pubkey;
use mango_v4::state::MangoAccountValue;
pub trait AccountFetcher: Sync + Send {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<Account>;
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData>;
fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>;
}
// Can't be in the trait, since then it would no longer be object-safe...
pub fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
fetcher: &dyn AccountFetcher,
address: Pubkey,
address: &Pubkey,
) -> anyhow::Result<T> {
let account = fetcher.fetch_raw_account(address)?;
let mut data: &[u8] = &account.data;
let mut data: &[u8] = &account.data();
T::try_deserialize(&mut data)
.with_context(|| format!("deserializing anchor account {}", address))
}
@ -30,10 +35,10 @@ pub fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
// Can't be in the trait, since then it would no longer be object-safe...
pub fn account_fetcher_fetch_mango_account(
fetcher: &dyn AccountFetcher,
address: Pubkey,
address: &Pubkey,
) -> anyhow::Result<MangoAccountValue> {
let account = fetcher.fetch_raw_account(address)?;
let data: &[u8] = &account.data;
let data: &[u8] = &account.data();
MangoAccountValue::from_bytes(&data[8..])
.with_context(|| format!("deserializing mango account {}", address))
}
@ -43,26 +48,73 @@ pub struct RpcAccountFetcher {
}
impl AccountFetcher for RpcAccountFetcher {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<Account> {
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
self.rpc
.get_account_with_commitment(&address, self.rpc.commitment())
.with_context(|| format!("fetch account {}", address))?
.get_account_with_commitment(address, self.rpc.commitment())
.with_context(|| format!("fetch account {}", *address))?
.value
.ok_or(ClientError::AccountNotFound)
.with_context(|| format!("fetch account {}", address))
.with_context(|| format!("fetch account {}", *address))
.map(Into::into)
}
fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
use solana_account_decoder::UiAccountEncoding;
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use solana_client::rpc_filter::{
Memcmp, MemcmpEncodedBytes, MemcmpEncoding, RpcFilterType,
};
let config = RpcProgramAccountsConfig {
filters: Some(vec![RpcFilterType::Memcmp(Memcmp {
offset: 0,
bytes: MemcmpEncodedBytes::Bytes(discriminator.to_vec()),
encoding: Some(MemcmpEncoding::Binary),
})]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
commitment: Some(self.rpc.commitment()),
..RpcAccountInfoConfig::default()
},
with_context: Some(true),
};
let accs = self.rpc.get_program_accounts_with_config(program, config)?;
// convert Account -> AccountSharedData
Ok(accs
.into_iter()
.map(|(pk, acc)| (pk, acc.into()))
.collect::<Vec<_>>())
}
}
struct AccountCache {
accounts: HashMap<Pubkey, AccountSharedData>,
keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec<Pubkey>>,
}
impl AccountCache {
fn clear(&mut self) {
self.accounts.clear();
self.keys_for_program_and_discriminator.clear();
}
}
pub struct CachedAccountFetcher<T: AccountFetcher> {
fetcher: T,
cache: Mutex<HashMap<Pubkey, Account>>,
cache: Mutex<AccountCache>,
}
impl<T: AccountFetcher> CachedAccountFetcher<T> {
pub fn new(fetcher: T) -> Self {
Self {
fetcher,
cache: Mutex::new(HashMap::new()),
cache: Mutex::new(AccountCache {
accounts: HashMap::new(),
keys_for_program_and_discriminator: HashMap::new(),
}),
}
}
@ -73,13 +125,38 @@ impl<T: AccountFetcher> CachedAccountFetcher<T> {
}
impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<Account> {
fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
let mut cache = self.cache.lock().unwrap();
if let Some(account) = cache.get(&address) {
if let Some(account) = cache.accounts.get(address) {
return Ok(account.clone());
}
let account = self.fetcher.fetch_raw_account(address)?;
cache.insert(address, account.clone());
cache.accounts.insert(*address, account.clone());
Ok(account)
}
fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let cache_key = (*program, discriminator);
let mut cache = self.cache.lock().unwrap();
if let Some(accounts) = cache.keys_for_program_and_discriminator.get(&cache_key) {
return Ok(accounts
.iter()
.map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone()))
.collect::<Vec<_>>());
}
let accounts = self
.fetcher
.fetch_program_accounts(program, discriminator)?;
cache
.keys_for_program_and_discriminator
.insert(cache_key, accounts.iter().map(|(pk, _)| *pk).collect());
for (pk, acc) in accounts.iter() {
cache.accounts.insert(*pk, acc.clone());
}
Ok(accounts)
}
}

View File

@ -224,6 +224,16 @@ impl ChainData {
.ok_or_else(|| anyhow::anyhow!("account {} has no live data", pubkey))
}
pub fn iter_accounts<'a>(&'a self) -> impl Iterator<Item = (&'a Pubkey, &'a AccountAndSlot)> {
self.accounts.iter().filter_map(|(pk, writes)| {
writes
.iter()
.rev()
.find(|w| self.is_account_write_live(w))
.map(|latest_write| (pk, latest_write))
})
}
pub fn slots_count(&self) -> usize {
self.slots.len()
}

View File

@ -138,7 +138,31 @@ impl AccountFetcher {
}
impl crate::AccountFetcher for AccountFetcher {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<solana_sdk::account::Account> {
self.fetch_raw(&address).map(|a| a.into())
fn fetch_raw_account(
&self,
address: &Pubkey,
) -> anyhow::Result<solana_sdk::account::AccountSharedData> {
self.fetch_raw(&address)
}
fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let chain_data = self.chain_data.read().unwrap();
Ok(chain_data
.iter_accounts()
.filter_map(|(pk, data)| {
if data.account.owner() != program {
return None;
}
let acc_data = data.account.data();
if acc_data.len() < 8 || acc_data[..8] != discriminator {
return None;
}
Some((*pk, data.account.clone()))
})
.collect::<Vec<_>>())
}
}

View File

@ -14,8 +14,11 @@ use anchor_spl::token::Token;
use bincode::Options;
use fixed::types::I80F48;
use itertools::Itertools;
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::state::{Bank, Group, MangoAccountValue, Serum3MarketIndex, TokenIndex};
use mango_v4::state::{
Bank, Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex,
};
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_client::rpc_client::RpcClient;
@ -28,6 +31,7 @@ use crate::jupiter;
use crate::util::MyClone;
use anyhow::Context;
use solana_sdk::account::ReadableAccount;
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::signature::{Keypair, Signature};
use solana_sdk::sysvar;
@ -228,7 +232,7 @@ impl MangoClient {
) -> anyhow::Result<Self> {
let rpc = client.rpc();
let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc }));
let mango_account = account_fetcher_fetch_mango_account(&*account_fetcher, account)?;
let mango_account = account_fetcher_fetch_mango_account(&*account_fetcher, &account)?;
let group = mango_account.fixed.group;
if mango_account.fixed.owner != owner.pubkey() {
anyhow::bail!(
@ -288,12 +292,12 @@ impl MangoClient {
}
pub fn mango_account(&self) -> anyhow::Result<MangoAccountValue> {
account_fetcher_fetch_mango_account(&*self.account_fetcher, self.mango_account_address)
account_fetcher_fetch_mango_account(&*self.account_fetcher, &self.mango_account_address)
}
pub fn first_bank(&self, token_index: TokenIndex) -> anyhow::Result<Bank> {
let bank_address = self.context.mint_info(token_index).first_bank();
account_fetcher_fetch_anchor_account(&*self.account_fetcher, bank_address)
account_fetcher_fetch_anchor_account(&*self.account_fetcher, &bank_address)
}
pub fn derive_health_check_remaining_account_metas(
@ -312,47 +316,15 @@ impl MangoClient {
pub fn derive_liquidation_health_check_remaining_account_metas(
&self,
liqee: &MangoAccountValue,
asset_token_index: TokenIndex,
liab_token_index: TokenIndex,
writable_banks: &[TokenIndex],
) -> anyhow::Result<Vec<AccountMeta>> {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
let account = self.mango_account()?;
let token_indexes = liqee
.active_token_positions()
.chain(account.active_token_positions())
.map(|ta| ta.token_index)
.unique();
for token_index in token_indexes {
let mint_info = self.context.mint_info(token_index);
let writable_bank = token_index == asset_token_index || token_index == liab_token_index;
banks.push((mint_info.first_bank(), writable_bank));
oracles.push(mint_info.oracle);
}
let serum_oos = liqee
.active_serum3_orders()
.chain(account.active_serum3_orders())
.map(|&s| s.open_orders);
let perp_markets = liqee
.active_perp_positions()
.chain(account.active_perp_positions())
.map(|&pa| self.context.perp_market_address(pa.market_index));
Ok(banks
.iter()
.map(|(pubkey, is_writable)| AccountMeta {
pubkey: *pubkey,
is_writable: *is_writable,
is_signer: false,
})
.chain(oracles.into_iter().map(to_readonly_account_meta))
.chain(perp_markets.map(to_readonly_account_meta))
.chain(serum_oos.map(to_readonly_account_meta))
.collect())
self.context
.derive_health_check_remaining_account_metas_two_accounts(
&account,
liqee,
writable_banks,
)
}
pub fn token_deposit(&self, mint: Pubkey, amount: u64) -> anyhow::Result<Signature> {
@ -372,6 +344,7 @@ impl MangoClient {
&mango_v4::accounts::TokenDeposit {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
oracle: mint_info.oracle,
@ -454,8 +427,8 @@ impl MangoClient {
) -> Result<pyth_sdk_solana::Price, anyhow::Error> {
let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap();
let mint_info = self.context.mint_info(token_index);
let oracle_account = self.account_fetcher.fetch_raw_account(mint_info.oracle)?;
Ok(pyth_sdk_solana::load_price(&oracle_account.data).unwrap())
let oracle_account = self.account_fetcher.fetch_raw_account(&mint_info.oracle)?;
Ok(pyth_sdk_solana::load_price(&oracle_account.data()).unwrap())
}
//
@ -712,8 +685,8 @@ impl MangoClient {
.unwrap();
let account = self.mango_account()?;
let open_orders = account.serum3_orders(market_index).unwrap().open_orders;
let open_orders_bytes = self.account_fetcher.fetch_raw_account(open_orders)?.data;
let open_orders_acc = self.account_fetcher.fetch_raw_account(&open_orders)?;
let open_orders_bytes = open_orders_acc.data();
let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes(
&open_orders_bytes[5..5 + std::mem::size_of::<serum_dex::state::OpenOrders>()],
);
@ -831,12 +804,183 @@ impl MangoClient {
//
// Perps
//
pub fn perp_settle_pnl(
&self,
market_index: PerpMarketIndex,
account_a: (&Pubkey, &MangoAccountValue),
account_b: (&Pubkey, &MangoAccountValue),
) -> anyhow::Result<Signature> {
let perp = self.context.perp(market_index);
let settlement_token = self.context.token(perp.market.settle_token_index);
let health_remaining_ams = self
.context
.derive_health_check_remaining_account_metas_two_accounts(account_a.1, account_b.1, &[])
.unwrap();
self.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpSettlePnl {
group: self.group(),
settler: self.mango_account_address,
settler_owner: self.owner(),
perp_market: perp.address,
account_a: *account_a.0,
account_b: *account_b.0,
oracle: perp.market.oracle,
settle_bank: settlement_token.mint_info.first_bank(),
settle_oracle: settlement_token.mint_info.oracle,
},
None,
);
ams.extend(health_remaining_ams.into_iter());
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::PerpSettlePnl {}),
})
.signer(&self.owner)
.send()
.map_err(prettify_client_error)
}
pub fn perp_liq_force_cancel_orders(
&self,
liqee: (&Pubkey, &MangoAccountValue),
market_index: PerpMarketIndex,
) -> anyhow::Result<Signature> {
let perp = self.context.perp(market_index);
let health_remaining_ams = self
.context
.derive_health_check_remaining_account_metas(liqee.1, vec![], false)
.unwrap();
self.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpLiqForceCancelOrders {
group: self.group(),
account: *liqee.0,
perp_market: perp.address,
asks: perp.market.asks,
bids: perp.market.bids,
oracle: perp.market.oracle,
},
None,
);
ams.extend(health_remaining_ams.into_iter());
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpLiqForceCancelOrders { limit: 5 },
),
})
.send()
.map_err(prettify_client_error)
}
pub fn perp_liq_base_position(
&self,
liqee: (&Pubkey, &MangoAccountValue),
market_index: PerpMarketIndex,
max_base_transfer: i64,
) -> anyhow::Result<Signature> {
let perp = self.context.perp(market_index);
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(liqee.1, &[])
.unwrap();
self.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpLiqBasePosition {
group: self.group(),
perp_market: perp.address,
oracle: perp.market.oracle,
liqor: self.mango_account_address,
liqor_owner: self.owner(),
liqee: *liqee.0,
},
None,
);
ams.extend(health_remaining_ams.into_iter());
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpLiqBasePosition { max_base_transfer },
),
})
.signer(&self.owner)
.send()
.map_err(prettify_client_error)
}
pub fn perp_liq_bankruptcy(
&self,
liqee: (&Pubkey, &MangoAccountValue),
market_index: PerpMarketIndex,
max_liab_transfer: u64,
) -> anyhow::Result<Signature> {
let group = account_fetcher_fetch_anchor_account::<Group>(
&*self.account_fetcher,
&self.context.group,
)?;
let perp = self.context.perp(market_index);
let settle_token_info = self.context.token(perp.market.settle_token_index);
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(liqee.1, &[])
.unwrap();
self.program()
.request()
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::PerpLiqBankruptcy {
group: self.group(),
perp_market: perp.address,
liqor: self.mango_account_address,
liqor_owner: self.owner(),
liqee: *liqee.0,
settle_bank: settle_token_info.mint_info.first_bank(),
settle_vault: settle_token_info.mint_info.first_vault(),
settle_oracle: settle_token_info.mint_info.oracle,
insurance_vault: group.insurance_vault,
token_program: Token::id(),
},
None,
);
ams.extend(health_remaining_ams.into_iter());
ams
},
data: anchor_lang::InstructionData::data(
&mango_v4::instruction::PerpLiqBankruptcy { max_liab_transfer },
),
})
.signer(&self.owner)
.send()
.map_err(prettify_client_error)
}
//
// Liquidation
//
pub fn liq_token_with_token(
pub fn token_liq_with_token(
&self,
liqee: (&Pubkey, &MangoAccountValue),
asset_token_index: TokenIndex,
@ -846,8 +990,7 @@ impl MangoClient {
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(
liqee.1,
asset_token_index,
liab_token_index,
&[asset_token_index, liab_token_index],
)
.unwrap();
@ -881,7 +1024,7 @@ impl MangoClient {
.map_err(prettify_client_error)
}
pub fn liq_token_bankruptcy(
pub fn token_liq_bankruptcy(
&self,
liqee: (&Pubkey, &MangoAccountValue),
liab_token_index: TokenIndex,
@ -902,14 +1045,13 @@ impl MangoClient {
let health_remaining_ams = self
.derive_liquidation_health_check_remaining_account_metas(
liqee.1,
quote_token_index,
liab_token_index,
&[quote_token_index, liab_token_index],
)
.unwrap();
let group = account_fetcher_fetch_anchor_account::<Group>(
&*self.account_fetcher,
self.context.group,
&self.context.group,
)?;
self.program()

View File

@ -10,6 +10,7 @@ use mango_v4::state::{
};
use fixed::types::I80F48;
use itertools::Itertools;
use crate::gpa::*;
@ -78,6 +79,10 @@ impl MangoGroupContext {
self.tokens.get(&token_index).unwrap()
}
pub fn perp(&self, perp_market_index: PerpMarketIndex) -> &PerpMarketContext {
self.perp_markets.get(&perp_market_index).unwrap()
}
pub fn token_by_mint(&self, mint: &Pubkey) -> anyhow::Result<&TokenContext> {
self.tokens
.iter()
@ -86,7 +91,7 @@ impl MangoGroupContext {
}
pub fn perp_market_address(&self, perp_market_index: PerpMarketIndex) -> Pubkey {
self.perp_markets.get(&perp_market_index).unwrap().address
self.perp(perp_market_index).address
}
pub fn new_from_rpc(
@ -237,6 +242,15 @@ impl MangoGroupContext {
let perp_markets = account
.active_perp_positions()
.map(|&pa| self.perp_market_address(pa.market_index));
let perp_oracles = account
.active_perp_positions()
.map(|&pa| self.perp(pa.market_index).market.oracle);
let to_account_meta = |pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
};
Ok(banks
.iter()
@ -245,21 +259,66 @@ impl MangoGroupContext {
is_writable: writable_banks,
is_signer: false,
})
.chain(oracles.iter().map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
.chain(oracles.into_iter().map(to_account_meta))
.chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.collect())
}
pub fn derive_health_check_remaining_account_metas_two_accounts(
&self,
account1: &MangoAccountValue,
account2: &MangoAccountValue,
writable_banks: &[TokenIndex],
) -> anyhow::Result<Vec<AccountMeta>> {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
let token_indexes = account2
.active_token_positions()
.chain(account1.active_token_positions())
.map(|ta| ta.token_index)
.unique();
for token_index in token_indexes {
let mint_info = self.mint_info(token_index);
let writable_bank = writable_banks.iter().contains(&token_index);
banks.push((mint_info.first_bank(), writable_bank));
oracles.push(mint_info.oracle);
}
let serum_oos = account2
.active_serum3_orders()
.chain(account1.active_serum3_orders())
.map(|&s| s.open_orders);
let perp_markets = account2
.active_perp_positions()
.chain(account1.active_perp_positions())
.map(|&pa| self.perp_market_address(pa.market_index));
let perp_oracles = account2
.active_perp_positions()
.chain(account1.active_perp_positions())
.map(|&pa| self.perp(pa.market_index).market.oracle);
let to_account_meta = |pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
};
Ok(banks
.iter()
.map(|(pubkey, is_writable)| AccountMeta {
pubkey: *pubkey,
is_writable: *is_writable,
is_signer: false,
}))
.chain(perp_markets.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(serum_oos.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
})
.chain(oracles.into_iter().map(to_account_meta))
.chain(perp_markets.map(to_account_meta))
.chain(perp_oracles.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.collect())
}
}

View File

@ -0,0 +1,33 @@
use crate::{AccountFetcher, MangoGroupContext};
use anyhow::Context;
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::state::{FixedOrderAccountRetriever, HealthCache, MangoAccountValue};
pub fn new(
context: &MangoGroupContext,
account_fetcher: &impl AccountFetcher,
account: &MangoAccountValue,
) -> anyhow::Result<HealthCache> {
let active_token_len = account.active_token_positions().count();
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| {
Ok(KeyedAccountSharedData::new(
meta.pubkey,
account_fetcher.fetch_raw_account(&meta.pubkey)?,
))
})
.collect::<anyhow::Result<Vec<_>>>()?;
let retriever = FixedOrderAccountRetriever {
ais: accounts,
n_banks: active_token_len,
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len,
};
mango_v4::state::new_health_cache(&account.borrow(), &retriever).context("make health cache")
}

View File

@ -9,5 +9,7 @@ mod chain_data_fetcher;
mod client;
mod context;
mod gpa;
pub mod health_cache;
mod jupiter;
pub mod perp_pnl;
mod util;

103
client/src/perp_pnl.rs Normal file
View File

@ -0,0 +1,103 @@
use anchor_lang::prelude::*;
use anchor_lang::Discriminator;
use fixed::types::I80F48;
use solana_sdk::account::ReadableAccount;
use crate::*;
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::state::*;
#[derive(Debug, PartialEq)]
pub enum Direction {
MaxPositive,
MaxNegative,
}
/// Returns up to `count` accounts with highest abs pnl (by `direction`) in descending order.
pub fn fetch_top(
context: &crate::context::MangoGroupContext,
account_fetcher: &impl AccountFetcher,
perp_market_index: PerpMarketIndex,
direction: Direction,
count: usize,
) -> anyhow::Result<Vec<(Pubkey, MangoAccountValue, I80F48)>> {
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)?;
let oracle_price =
perp_market.oracle_price(&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc))?;
let accounts =
account_fetcher.fetch_program_accounts(&mango_v4::id(), MangoAccount::discriminator())?;
let mut accounts_pnl = accounts
.iter()
.filter_map(|(pk, acc)| {
let data = acc.data();
let mango_acc = MangoAccountValue::from_bytes(&data[8..]);
if mango_acc.is_err() {
return None;
}
let mango_acc = mango_acc.unwrap();
let perp_pos = mango_acc.perp_position(perp_market_index);
if perp_pos.is_err() {
return None;
}
let perp_pos = perp_pos.unwrap();
let pnl = perp_pos.base_position_native(&perp_market) * oracle_price
+ perp_pos.quote_position_native();
if pnl >= 0 && direction == Direction::MaxNegative
|| pnl <= 0 && direction == Direction::MaxPositive
{
return None;
}
Some((*pk, mango_acc, pnl))
})
.collect::<Vec<_>>();
// Sort the top accounts to the front
match direction {
Direction::MaxPositive => {
accounts_pnl.sort_by(|a, b| b.2.cmp(&a.2));
}
Direction::MaxNegative => {
accounts_pnl.sort_by(|a, b| a.2.cmp(&b.2));
}
}
// Negative pnl needs to be limited by perp_settle_health.
// We're doing it in a second step, because it's pretty expensive and we don't
// want to run this for all accounts.
if direction == Direction::MaxNegative {
let mut stable = 0;
for i in 0..accounts_pnl.len() {
let (_, acc, pnl) = &accounts_pnl[i];
let next_pnl = if i + 1 < accounts_pnl.len() {
accounts_pnl[i + 1].2
} else {
I80F48::ZERO
};
let perp_settle_health =
crate::health_cache::new(context, account_fetcher, &acc)?.perp_settle_health();
let settleable_pnl = if perp_settle_health > 0 && !acc.being_liquidated() {
(*pnl).max(-perp_settle_health)
} else {
I80F48::ZERO
};
accounts_pnl[i].2 = settleable_pnl;
// if the ordering was unchanged `count` times we know we have the top `count` accounts
if settleable_pnl <= next_pnl {
stable += 1;
if stable >= count {
break;
}
}
}
accounts_pnl.sort_by(|a, b| a.2.cmp(&b.2));
}
// return highest abs pnl accounts
Ok(accounts_pnl.into_iter().take(count).collect::<Vec<_>>())
}

View File

@ -164,6 +164,7 @@ pub async fn loop_consume_events(
client.program().account(perp_market.event_queue).unwrap();
let mut ams_ = vec![];
let mut num_of_events = 0;
// 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
@ -197,6 +198,11 @@ pub async fn loop_consume_events(
EventType::Liquidate => {}
}
event_queue.pop_front()?;
num_of_events+=1;
}
if num_of_events == 0 {
return Ok(());
}
let pre = Instant::now();
@ -229,7 +235,7 @@ pub async fn loop_consume_events(
"metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}",
perp_market.name(),
pre.elapsed().as_millis(),
ams_.len(),
num_of_events,
e.to_string()
);
log::error!("{:?}", e)
@ -238,7 +244,7 @@ pub async fn loop_consume_events(
"metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}",
perp_market.name(),
pre.elapsed().as_millis(),
ams_.len(),
num_of_events,
);
log::info!("{:?}", sig_result);
}

View File

@ -1,30 +0,0 @@
use mango_v4::accounts_zerocopy::{AccountReader, KeyedAccountReader};
use solana_sdk::{account::AccountSharedData, pubkey::Pubkey};
#[derive(Clone)]
pub struct KeyedAccountSharedData {
pub key: Pubkey,
pub data: AccountSharedData,
}
impl KeyedAccountSharedData {
pub fn new(key: Pubkey, data: AccountSharedData) -> Self {
Self { key, data }
}
}
impl AccountReader for KeyedAccountSharedData {
fn owner(&self) -> &Pubkey {
AccountReader::owner(&self.data)
}
fn data(&self) -> &[u8] {
AccountReader::data(&self.data)
}
}
impl KeyedAccountReader for KeyedAccountSharedData {
fn key(&self) -> &Pubkey {
&self.key
}
}

View File

@ -1,12 +1,12 @@
use std::time::Duration;
use crate::account_shared_data::KeyedAccountSharedData;
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
use client::{chain_data, health_cache, AccountFetcher, MangoClient, MangoClientError};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::state::{
new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue,
Serum3Orders, TokenIndex, QUOTE_TOKEN_INDEX,
Bank, HealthCache, HealthType, MangoAccountValue, PerpMarketIndex, Serum3Orders, Side,
TokenIndex, QUOTE_TOKEN_INDEX,
};
use solana_sdk::signature::Signature;
use itertools::Itertools;
use rand::seq::SliceRandom;
@ -17,35 +17,6 @@ pub struct Config {
pub refresh_timeout: Duration,
}
pub fn new_health_cache_(
context: &MangoGroupContext,
account_fetcher: &chain_data::AccountFetcher,
account: &MangoAccountValue,
) -> anyhow::Result<HealthCache> {
let active_token_len = account.active_token_positions().count();
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| {
Ok(KeyedAccountSharedData::new(
meta.pubkey,
account_fetcher.fetch_raw(&meta.pubkey)?,
))
})
.collect::<anyhow::Result<Vec<_>>>()?;
let retriever = FixedOrderAccountRetriever {
ais: accounts,
n_banks: active_token_len,
n_perps: active_perp_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len,
};
new_health_cache(&account.borrow(), &retriever).context("make health cache")
}
pub fn jupiter_market_can_buy(
mango_client: &MangoClient,
token: TokenIndex,
@ -100,6 +71,493 @@ pub fn jupiter_market_can_sell(
.is_ok()
}
struct LiquidateHelper<'a> {
client: &'a MangoClient,
account_fetcher: &'a chain_data::AccountFetcher,
pubkey: &'a Pubkey,
liqee: &'a MangoAccountValue,
health_cache: &'a HealthCache,
maint_health: I80F48,
liqor_min_health_ratio: I80F48,
}
impl<'a> LiquidateHelper<'a> {
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 open_orders_account = self
.account_fetcher
.fetch_raw_account(&orders.open_orders)?;
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
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))
} else {
Ok(None)
}
})
.filter_map_ok(|v| v)
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
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,
)?;
log::info!(
"Force cancelled serum orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
serum_orders.market_index,
self.maint_health,
sig
);
Ok(Some(sig))
}
fn perp_close_orders(&self) -> anyhow::Result<Option<Signature>> {
let perp_force_cancels = self
.liqee
.active_perp_positions()
.filter_map(|pp| pp.has_open_orders().then(|| pp.market_index))
.collect::<Vec<PerpMarketIndex>>();
if perp_force_cancels.is_empty() {
return Ok(None);
}
// Cancel all orders on a random perp market
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)?;
log::info!(
"Force cancelled perp orders on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
perp_market_index,
self.maint_health,
sig
);
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| {
let base_lots = pp.base_position_lots();
if base_lots == 0 {
return Ok(None);
}
let perp = self.client.context.perp(pp.market_index);
let oracle = self
.account_fetcher
.fetch_raw_account(&perp.market.oracle)?;
let price = perp.market.oracle_price(&KeyedAccountSharedData::new(
perp.market.oracle,
oracle.into(),
))?;
Ok(Some((
pp.market_index,
base_lots,
price,
I80F48::from(base_lots.abs()) * price,
)))
})
.filter_map_ok(|v| v)
.collect::<anyhow::Result<Vec<(PerpMarketIndex, i64, I80F48, I80F48)>>>()?;
perp_base_positions.sort_by(|a, b| a.3.cmp(&b.3));
if perp_base_positions.is_empty() {
return Ok(None);
}
// Liquidate the highest-value perp base position
let (perp_market_index, base_lots, price, _) = perp_base_positions.last().unwrap();
let perp = self.client.context.perp(*perp_market_index);
let (side, side_signum) = if *base_lots > 0 {
(Side::Bid, 1)
} else {
(Side::Ask, -1)
};
// Compute the max number of base_lots the liqor is willing to take
let max_base_transfer_abs = {
let mut liqor = self
.account_fetcher
.fetch_fresh_mango_account(&self.client.mango_account_address)
.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)
.expect("always ok");
health_cache.max_perp_for_health_ratio(
*perp_market_index,
*price,
perp.market.base_lot_size,
side,
self.liqor_min_health_ratio,
)?
};
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,
)?;
log::info!(
"Liquidated base position for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
perp_market_index,
self.maint_health,
sig
);
Ok(Some(sig))
}
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
.active_perp_positions()
.filter_map(|pp| {
if pp.base_position_lots() != 0 {
return None;
}
let pnl = pp.quote_position_native();
let settleable_pnl = if pnl > 0 {
pnl
} else if pnl < 0 && perp_settle_health > 0 {
pnl.max(-perp_settle_health)
} else {
return None;
};
Some((pp.market_index, settleable_pnl))
})
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
// sort by pnl, descending
perp_settleable_pnl.sort_by(|a, b| b.1.cmp(&a.1));
if perp_settleable_pnl.is_empty() {
return Ok(None);
}
for (perp_index, pnl) in perp_settleable_pnl {
let direction = if pnl > 0 {
client::perp_pnl::Direction::MaxNegative
} else {
client::perp_pnl::Direction::MaxPositive
};
let counters = client::perp_pnl::fetch_top(
&self.client.context,
self.account_fetcher,
perp_index,
direction,
2,
)?;
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
log::info!("Could not settle perp pnl {pnl} for account {}, perp market {perp_index}: no counterparty",
self.pubkey);
continue;
}
let (counter_key, counter_acc, _) = counters.first().unwrap();
let (account_a, account_b) = if pnl > 0 {
((self.pubkey, self.liqee), (counter_key, counter_acc))
} else {
((counter_key, counter_acc), (self.pubkey, self.liqee))
};
let sig = self
.client
.perp_settle_pnl(perp_index, account_a, account_b)?;
log::info!(
"Settled perp pnl for perp market on account {}, market index {perp_index}, maint_health was {}, tx sig {sig:?}",
self.pubkey,
self.maint_health,
);
return Ok(Some(sig));
}
return Ok(None);
}
fn perp_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
if self.health_cache.has_liquidatable_assets() {
return Ok(None);
}
let mut perp_bankruptcies = self
.liqee
.active_perp_positions()
.filter_map(|pp| {
let quote = pp.quote_position_native();
if quote >= 0 {
return None;
}
Some((pp.market_index, quote))
})
.collect::<Vec<(PerpMarketIndex, I80F48)>>();
perp_bankruptcies.sort_by(|a, b| a.1.cmp(&b.1));
if perp_bankruptcies.is_empty() {
return Ok(None);
}
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,
)?;
log::info!(
"Liquidated bankruptcy for perp market on account {}, market index {}, maint_health was {}, tx sig {:?}",
self.pubkey,
perp_market_index,
self.maint_health,
sig
);
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(),
))?;
Ok((
token_position.token_index,
price,
token_position.native(&bank) * price,
))
})
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
tokens.sort_by(|a, b| a.2.cmp(&b.2));
Ok(tokens)
}
fn max_token_liab_transfer(
&self,
source: TokenIndex,
target: TokenIndex,
) -> anyhow::Result<I80F48> {
let mut liqor = self
.account_fetcher
.fetch_fresh_mango_account(&self.client.mango_account_address)
.context("getting liquidator account")?;
// Ensure the tokens are activated, so they appear in the health cache and
// max_swap_source() will work.
liqor.ensure_token_position(source)?;
liqor.ensure_token_position(target)?;
let health_cache = health_cache::new(&self.client.context, self.account_fetcher, &liqor)
.expect("always ok");
let source_price = health_cache.token_info(source).unwrap().oracle_price;
let target_price = health_cache.token_info(target).unwrap().oracle_price;
// TODO: This is where we could multiply in the liquidation fee factors
let oracle_swap_price = source_price / target_price;
let amount = health_cache
.max_swap_source_for_health_ratio(
source,
target,
oracle_swap_price,
self.liqor_min_health_ratio,
)
.context("getting max_swap_source")?;
Ok(amount)
}
fn token_liq(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.has_borrows() || self.health_cache.can_call_spot_bankruptcy() {
return Ok(None);
}
let tokens = self.tokens()?;
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)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
self.pubkey,
tokens
)
})?
.0;
let liab_token_index = tokens
.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)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
self.pubkey,
tokens
)
})?
.0;
let max_liab_transfer = self
.max_token_liab_transfer(liab_token_index, asset_token_index)
.context("getting max_liab_transfer")?;
//
// TODO: log liqor's assets in UI form
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
//
let sig = self
.client
.token_liq_with_token(
(self.pubkey, &self.liqee),
asset_token_index,
liab_token_index,
max_liab_transfer,
)
.context("sending liq_token_with_token")?;
log::info!(
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
self.pubkey,
self.maint_health,
sig
);
Ok(Some(sig))
}
fn token_liq_bankruptcy(&self) -> anyhow::Result<Option<Signature>> {
if !self.health_cache.can_call_spot_bankruptcy() {
return Ok(None);
}
let tokens = self.tokens()?;
if tokens.is_empty() {
anyhow::bail!(
"mango account {}, is bankrupt has no active tokens",
self.pubkey
);
}
let liab_token_index = tokens
.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)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
self.pubkey,
tokens
)
})?
.0;
let quote_token_index = 0;
let max_liab_transfer =
self.max_token_liab_transfer(liab_token_index, quote_token_index)?;
let sig = self
.client
.token_liq_bankruptcy(
(self.pubkey, &self.liqee),
liab_token_index,
max_liab_transfer,
)
.context("sending liq_token_bankruptcy")?;
log::info!(
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
self.pubkey,
self.maint_health,
sig
);
Ok(Some(sig))
}
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.
// We shouldn't get stuck on this step, particularly if it's of limited value
// to the liquidators.
// if let Some(txsig) = self.perp_settle_positive_pnl()? {
// return Ok(txsig);
// }
// Try to close orders before touching the user's positions
if let Some(txsig) = self.perp_close_orders()? {
return Ok(txsig);
}
if let Some(txsig) = self.serum3_close_orders()? {
return Ok(txsig);
}
if let Some(txsig) = self.perp_liq_base_position()? {
return Ok(txsig);
}
// Now that the perp base positions are zeroed the perp pnl won't
// fluctuate with the oracle price anymore.
// 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()? {
return Ok(txsig);
}
if let Some(txsig) = self.token_liq()? {
return Ok(txsig);
}
// Socialize/insurance fund unsettleable negative pnl
if let Some(txsig) = self.perp_liq_bankruptcy()? {
return Ok(txsig);
}
// Socialize/insurance fund unliquidatable borrows
if let Some(txsig) = self.token_liq_bankruptcy()? {
return Ok(txsig);
}
// TODO: What about unliquidatable positive perp pnl?
anyhow::bail!(
"Don't know what to do with liquidatable account {}, maint_health was {}",
self.pubkey,
self.maint_health
);
}
}
#[allow(clippy::too_many_arguments)]
pub fn maybe_liquidate_account(
mango_client: &MangoClient,
@ -107,12 +565,11 @@ pub fn maybe_liquidate_account(
pubkey: &Pubkey,
config: &Config,
) -> anyhow::Result<bool> {
let min_health_ratio = I80F48::from_num(config.min_health_ratio);
let quote_token_index = 0;
let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio);
let account = account_fetcher.fetch_mango_account(pubkey)?;
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
let maint_health = health_cache.health(HealthType::Maint);
if !health_cache.is_liquidatable() {
return Ok(false);
@ -130,185 +587,24 @@ pub fn maybe_liquidate_account(
// be great at providing timely updates to the account data.
let account = account_fetcher.fetch_fresh_mango_account(pubkey)?;
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &account).expect("always ok");
health_cache::new(&mango_client.context, account_fetcher, &account).expect("always ok");
if !health_cache.is_liquidatable() {
return Ok(false);
}
let maint_health = health_cache.health(HealthType::Maint);
let is_spot_bankrupt = health_cache.can_call_spot_bankruptcy();
let is_spot_liquidatable = health_cache.has_borrows() && !is_spot_bankrupt;
// find asset and liab tokens
let mut tokens = account
.active_token_positions()
.map(|token_position| {
let token = mango_client.context.token(token_position.token_index);
let bank = account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())?;
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
let price = bank.oracle_price(&KeyedAccountSharedData::new(
token.mint_info.oracle,
oracle.into(),
))?;
Ok((
token_position.token_index,
price,
token_position.native(&bank) * price,
))
})
.collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
tokens.sort_by(|a, b| a.2.cmp(&b.2));
// look for any open serum orders or settleable balances
let serum_force_cancels = account
.active_serum3_orders()
.map(|orders| {
let open_orders_account = account_fetcher.fetch_raw_account(orders.open_orders)?;
let open_orders = mango_v4::serum3_cpi::load_open_orders(&open_orders_account)?;
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))
} else {
Ok(None)
}
})
.filter_map_ok(|v| v)
.collect::<anyhow::Result<Vec<Serum3Orders>>>()?;
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
let mut liqor = account_fetcher
.fetch_fresh_mango_account(&mango_client.mango_account_address)
.context("getting liquidator account")?;
// Ensure the tokens are activated, so they appear in the health cache and
// max_swap_source() will work.
liqor.ensure_token_position(source)?;
liqor.ensure_token_position(target)?;
let health_cache =
new_health_cache_(&mango_client.context, account_fetcher, &liqor).expect("always ok");
let source_price = health_cache.token_info(source).unwrap().oracle_price;
let target_price = health_cache.token_info(target).unwrap().oracle_price;
// TODO: This is where we could multiply in the liquidation fee factors
let oracle_swap_price = source_price / target_price;
let amount = health_cache
.max_swap_source_for_health_ratio(source, target, oracle_swap_price, min_health_ratio)
.context("getting max_swap_source")?;
Ok(amount)
};
// try liquidating
let txsig = if !serum_force_cancels.is_empty() {
// pick a random market to force-cancel orders on
let serum_orders = serum_force_cancels.choose(&mut rand::thread_rng()).unwrap();
let sig = mango_client.serum3_liq_force_cancel_orders(
(pubkey, &account),
serum_orders.market_index,
&serum_orders.open_orders,
)?;
log::info!(
"Force cancelled serum market on account {}, market index {}, maint_health was {}, tx sig {:?}",
pubkey,
serum_orders.market_index,
maint_health,
sig
);
sig
} else if is_spot_bankrupt {
if tokens.is_empty() {
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
}
let liab_token_index = tokens
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?;
let sig = mango_client
.liq_token_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer)
.context("sending liq_token_bankruptcy")?;
log::info!(
"Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
pubkey,
maint_health,
sig
);
sig
} else if is_spot_liquidatable {
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(mango_client, *asset_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
let liab_token_index = tokens
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
let max_liab_transfer = get_max_liab_transfer(liab_token_index, asset_token_index)
.context("getting max_liab_transfer")?;
//
// TODO: log liqor's assets in UI form
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
//
let sig = mango_client
.liq_token_with_token(
(pubkey, &account),
asset_token_index,
liab_token_index,
max_liab_transfer,
)
.context("sending liq_token_with_token")?;
log::info!(
"Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
pubkey,
maint_health,
sig
);
sig
} else {
anyhow::bail!(
"Don't know what to do with liquidatable account {}, maint_health was {}",
pubkey,
maint_health
);
};
let txsig = LiquidateHelper {
client: mango_client,
account_fetcher,
pubkey,
liqee: &account,
health_cache: &health_cache,
maint_health,
liqor_min_health_ratio,
}
.send_liq_tx()?;
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(

View File

@ -8,11 +8,11 @@ use client::{chain_data, keypair_from_cli, Client, MangoClient, MangoGroupContex
use log::*;
use mango_v4::state::{PerpMarketIndex, TokenIndex};
use itertools::Itertools;
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use std::collections::HashSet;
pub mod account_shared_data;
pub mod liquidate;
pub mod metrics;
pub mod rebalance;
@ -126,6 +126,8 @@ async fn main() -> anyhow::Result<()> {
.tokens
.values()
.map(|value| value.mint_info.oracle)
.chain(group_context.perp_markets.values().map(|p| p.market.oracle))
.unique()
.collect::<Vec<Pubkey>>();
//

View File

@ -1,6 +1,7 @@
use crate::{account_shared_data::KeyedAccountSharedData, AnyhowWrap};
use crate::AnyhowWrap;
use client::{chain_data, AccountFetcher, MangoClient, TokenContext};
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX};
use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
@ -43,7 +44,7 @@ impl TokenState {
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)?;
bank.oracle_price(&KeyedAccountSharedData::new(
token.mint_info.oracle,
oracle.into(),

View File

@ -19,6 +19,7 @@
"build": "npm run build:esm; npm run build:cjs",
"build:cjs": "tsc -p tsconfig.cjs.json",
"build:esm": "tsc -p tsconfig.esm.json",
"test": "ts-mocha ts/client/**/*.spec.ts",
"clean": "rm -rf dist",
"example1-user": "ts-node ts/client/src/scripts/example1-user.ts",
"example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts",
@ -63,10 +64,14 @@
"@project-serum/serum": "^0.13.65",
"@pythnetwork/client": "^2.7.0",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.63.1",
"@switchboard-xyz/switchboard-v2": "^0.0.129",
"big.js": "^6.1.1",
"bs58": "^5.0.0"
},
"resolutions": {
"@project-serum/anchor/@solana/web3.js": "1.63.1"
},
"peerDependencies": {
"@solana/spl-token-swap": "^0.2.0"
},

View File

@ -110,6 +110,63 @@ impl<T: solana_sdk::account::ReadableAccount> AccountReader for T {
}
}
#[cfg(feature = "solana-sdk")]
#[derive(Clone)]
pub struct KeyedAccount {
pub key: Pubkey,
pub account: solana_sdk::account::Account,
}
#[cfg(feature = "solana-sdk")]
impl AccountReader for KeyedAccount {
fn owner(&self) -> &Pubkey {
self.account.owner()
}
fn data(&self) -> &[u8] {
self.account.data()
}
}
#[cfg(feature = "solana-sdk")]
impl KeyedAccountReader for KeyedAccount {
fn key(&self) -> &Pubkey {
&self.key
}
}
#[cfg(feature = "solana-sdk")]
#[derive(Clone)]
pub struct KeyedAccountSharedData {
pub key: Pubkey,
pub data: solana_sdk::account::AccountSharedData,
}
#[cfg(feature = "solana-sdk")]
impl KeyedAccountSharedData {
pub fn new(key: Pubkey, data: solana_sdk::account::AccountSharedData) -> Self {
Self { key, data }
}
}
#[cfg(feature = "solana-sdk")]
impl AccountReader for KeyedAccountSharedData {
fn owner(&self) -> &Pubkey {
AccountReader::owner(&self.data)
}
fn data(&self) -> &[u8] {
AccountReader::data(&self.data)
}
}
#[cfg(feature = "solana-sdk")]
impl KeyedAccountReader for KeyedAccountSharedData {
fn key(&self) -> &Pubkey {
&self.key
}
}
//
// Common traits for loading from account data.
//

View File

@ -377,7 +377,6 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
indexed_position: position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: price.to_bits(),
});
}
@ -395,7 +394,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
// Deactivate inactive token accounts after health check
for raw_token_index in deactivated_token_positions {
account.deactivate_token_position(raw_token_index);
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
}
Ok(())

View File

@ -64,7 +64,6 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
ctx.accounts.group.key(),
fill.maker,
perp_market.perp_market_index as u64,
fill.price,
ma.perp_position(perp_market.perp_market_index).unwrap(),
&perp_market,
);
@ -105,7 +104,6 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
ctx.accounts.group.key(),
fill.maker,
perp_market.perp_market_index as u64,
fill.price,
maker.perp_position(perp_market.perp_market_index).unwrap(),
&perp_market,
);
@ -113,7 +111,6 @@ pub fn perp_consume_events(ctx: Context<PerpConsumeEvents>, limit: usize) -> Res
ctx.accounts.group.key(),
fill.taker,
perp_market.perp_market_index as u64,
fill.price,
taker.perp_position(perp_market.perp_market_index).unwrap(),
&perp_market,
);

View File

@ -1,8 +1,7 @@
use anchor_lang::prelude::*;
use fixed::types::I80F48;
use crate::error::MangoError;
use crate::error::*;
use crate::state::*;
use crate::util::fill_from_str;
@ -46,6 +45,7 @@ pub struct PerpCreateMarket<'info> {
#[allow(clippy::too_many_arguments)]
pub fn perp_create_market(
ctx: Context<PerpCreateMarket>,
settle_token_index: TokenIndex,
perp_market_index: PerpMarketIndex,
name: String,
oracle_config: OracleConfig,
@ -64,11 +64,30 @@ pub fn perp_create_market(
impact_quantity: i64,
group_insurance_fund: bool,
trusted_market: bool,
fee_penalty: f32,
settle_fee_flat: f32,
settle_fee_amount_threshold: f32,
settle_fee_fraction_low_health: f32,
) -> Result<()> {
// Settlement tokens that aren't USDC aren't fully implemented, the main missing steps are:
// - In health: the perp health needs to be adjusted by the settlement token weights.
// Otherwise settling perp pnl could decrease health.
// - In settle pnl and settle fees: use the settle oracle to convert the pnl from USD to token.
// - In perp bankruptcy: fix the assumption that the insurance fund has the same mint as
// the settlement token.
require_msg!(
settle_token_index == QUOTE_TOKEN_INDEX,
"settlement tokens != USDC are not fully implemented"
);
let mut perp_market = ctx.accounts.perp_market.load_init()?;
*perp_market = PerpMarket {
name: fill_from_str(&name)?,
group: ctx.accounts.group.key(),
settle_token_index,
perp_market_index,
group_insurance_fund: if group_insurance_fund { 1 } else { 0 },
trusted_market: if trusted_market { 1 } else { 0 },
name: fill_from_str(&name)?,
oracle: ctx.accounts.oracle.key(),
oracle_config,
bids: ctx.accounts.bids.key(),
@ -93,17 +112,16 @@ pub fn perp_create_market(
seq_num: 0,
fees_accrued: I80F48::ZERO,
fees_settled: I80F48::ZERO,
// Why optional - Perp could be based purely on an oracle
bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?,
base_decimals,
perp_market_index,
registration_time: Clock::get()?.unix_timestamp,
group_insurance_fund: if group_insurance_fund { 1 } else { 0 },
trusted_market: if trusted_market { 1 } else { 0 },
padding0: Default::default(),
padding1: Default::default(),
padding2: Default::default(),
reserved: [0; 112],
fee_penalty,
settle_fee_flat,
settle_fee_amount_threshold,
settle_fee_fraction_low_health,
reserved: [0; 92],
};
let mut bids = ctx.accounts.bids.load_init()?;

View File

@ -50,7 +50,10 @@ pub fn perp_deactivate_position(ctx: Context<PerpDeactivatePosition>) -> Result<
"perp position still has events on event queue"
);
account.deactivate_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)?;
account.deactivate_perp_position(
perp_market.perp_market_index,
perp_market.settle_token_index,
)?;
Ok(())
}

View File

@ -35,6 +35,10 @@ pub fn perp_edit_market(
impact_quantity_opt: Option<i64>,
group_insurance_fund_opt: Option<bool>,
trusted_market_opt: Option<bool>,
fee_penalty_opt: Option<f32>,
settle_fee_flat_opt: Option<f32>,
settle_fee_amount_threshold_opt: Option<f32>,
settle_fee_fraction_low_health_opt: Option<f32>,
) -> Result<()> {
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
@ -91,6 +95,9 @@ pub fn perp_edit_market(
if let Some(impact_quantity) = impact_quantity_opt {
perp_market.impact_quantity = impact_quantity;
}
if let Some(fee_penalty) = fee_penalty_opt {
perp_market.fee_penalty = fee_penalty;
}
// unchanged -
// long_funding
@ -118,5 +125,15 @@ pub fn perp_edit_market(
perp_market.trusted_market = if trusted_market { 1 } else { 0 };
}
if let Some(settle_fee_flat) = settle_fee_flat_opt {
perp_market.settle_fee_flat = settle_fee_flat;
}
if let Some(settle_fee_amount_threshold) = settle_fee_amount_threshold_opt {
perp_market.settle_fee_amount_threshold = settle_fee_amount_threshold;
}
if let Some(settle_fee_fraction_low_health) = settle_fee_fraction_low_health_opt {
perp_market.settle_fee_fraction_low_health = settle_fee_fraction_low_health;
}
Ok(())
}

View File

@ -38,13 +38,19 @@ pub struct PerpLiqBankruptcy<'info> {
#[account(
mut,
has_one = group,
constraint = quote_bank.load()?.vault == quote_vault.key()
// address is checked at #2
)]
pub quote_bank: AccountLoader<'info, Bank>,
pub settle_bank: AccountLoader<'info, Bank>,
#[account(mut)]
pub quote_vault: Account<'info, TokenAccount>,
#[account(
mut,
address = settle_bank.load()?.vault
)]
pub settle_vault: Account<'info, TokenAccount>,
/// CHECK: Oracle can have different account types
#[account(address = settle_bank.load()?.oracle)]
pub settle_oracle: UncheckedAccount<'info>,
// future: this would be an insurance fund vault specific to a
// trustless token, separate from the shared one on the group
@ -59,7 +65,7 @@ impl<'info> PerpLiqBankruptcy<'info> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.insurance_vault.to_account_info(),
to: self.quote_vault.to_account_info(),
to: self.settle_vault.to_account_info(),
authority: self.group.to_account_info(),
};
CpiContext::new(program, accounts)
@ -96,6 +102,7 @@ pub fn perp_liq_bankruptcy(ctx: Context<PerpLiqBankruptcy>, max_liab_transfer: u
// Find bankrupt liab amount
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
let settle_token_index = perp_market.settle_token_index;
let liqee_perp_position = liqee.perp_position_mut(perp_market.perp_market_index)?;
require_msg!(
liqee_perp_position.base_position_lots() == 0,
@ -137,9 +144,9 @@ pub fn perp_liq_bankruptcy(ctx: Context<PerpLiqBankruptcy>, max_liab_transfer: u
// Try using the insurance fund if possible
if insurance_transfer > 0 {
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
require_eq!(quote_bank.token_index, QUOTE_TOKEN_INDEX);
require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint);
let mut settle_bank = ctx.accounts.settle_bank.load_mut()?;
require_eq!(settle_bank.token_index, settle_token_index);
require_keys_eq!(settle_bank.mint, ctx.accounts.insurance_vault.mint);
// move insurance assets into quote bank
let group_seeds = group_seeds!(group);
@ -149,12 +156,12 @@ pub fn perp_liq_bankruptcy(ctx: Context<PerpLiqBankruptcy>, max_liab_transfer: u
)?;
// credit the liqor with quote tokens
let (liqor_quote, _, _) = liqor.ensure_token_position(QUOTE_TOKEN_INDEX)?;
quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?;
let (liqor_quote, _, _) = liqor.ensure_token_position(settle_token_index)?;
settle_bank.deposit(liqor_quote, insurance_transfer_i80f48)?;
// transfer perp quote loss from the liqee to the liqor
let liqor_perp_position = liqor
.ensure_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)?
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)?
.0;
liqee_perp_position.change_quote_position(insurance_liab_transfer);
liqor_perp_position.change_quote_position(-insurance_liab_transfer);

View File

@ -87,7 +87,7 @@ pub fn perp_liq_base_position(
// Fetch perp positions for accounts, creating for the liqor if needed
let liqee_perp_position = liqee.perp_position_mut(perp_market_index)?;
let liqor_perp_position = liqor
.ensure_perp_position(perp_market_index, QUOTE_TOKEN_INDEX)?
.ensure_perp_position(perp_market_index, perp_market.settle_token_index)?
.0;
let liqee_base_lots = liqee_perp_position.base_position_lots();
@ -112,7 +112,8 @@ pub fn perp_liq_base_position(
// and increased by `base * price * (1 - liq_fee) * quote_init_asset_weight`
let quote_asset_weight = I80F48::ONE;
let health_per_lot = cm!(price_per_lot
* (quote_asset_weight - perp_market.init_asset_weight - perp_market.liquidation_fee));
* (quote_asset_weight * (I80F48::ONE - perp_market.liquidation_fee)
- perp_market.init_asset_weight));
// number of lots to transfer to bring health to zero, rounded up
let base_transfer_for_zero: i64 = cm!(-liqee_init_health / health_per_lot)
@ -141,7 +142,8 @@ pub fn perp_liq_base_position(
// and reduced by `base * price * (1 + liq_fee) * quote_init_liab_weight`
let quote_liab_weight = I80F48::ONE;
let health_per_lot = cm!(price_per_lot
* (perp_market.init_liab_weight - quote_liab_weight + perp_market.liquidation_fee));
* (perp_market.init_liab_weight * (I80F48::ONE + perp_market.liquidation_fee)
- quote_liab_weight));
// (negative) number of lots to transfer to bring health to zero, rounded away from zero
let base_transfer_for_zero: i64 = cm!(liqee_init_health / health_per_lot)

View File

@ -5,7 +5,7 @@ use crate::error::*;
use crate::state::MangoAccount;
use crate::state::{
new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, Book, BookSide,
EventQueue, Group, OrderType, PerpMarket, Side, QUOTE_TOKEN_INDEX,
EventQueue, Group, OrderType, PerpMarket, Side,
};
#[derive(Accounts)]
@ -83,12 +83,18 @@ pub fn perp_place_order(
let account_pk = ctx.accounts.account.key();
let perp_market_index = ctx.accounts.perp_market.load()?.perp_market_index;
let (perp_market_index, settle_token_index) = {
let perp_market = ctx.accounts.perp_market.load()?;
(
perp_market.perp_market_index,
perp_market.settle_token_index,
)
};
//
// Create the perp position if needed
//
account.ensure_perp_position(perp_market_index, QUOTE_TOKEN_INDEX)?;
account.ensure_perp_position(perp_market_index, settle_token_index)?;
//
// Pre-health computation, _after_ perp position is created

View File

@ -9,7 +9,6 @@ use crate::state::new_fixed_order_account_retriever;
use crate::state::Bank;
use crate::state::HealthType;
use crate::state::MangoAccount;
use crate::state::QUOTE_TOKEN_INDEX;
use crate::state::{AccountLoaderDynamic, Group, PerpMarket};
#[derive(Accounts)]
@ -27,7 +26,11 @@ pub struct PerpSettleFees<'info> {
pub oracle: UncheckedAccount<'info>,
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
pub settle_bank: AccountLoader<'info, Bank>,
/// CHECK: Oracle can have different account types
#[account(address = settle_bank.load()?.oracle)]
pub settle_oracle: UncheckedAccount<'info>,
}
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {
@ -38,12 +41,13 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
);
let mut account = ctx.accounts.account.load_mut()?;
let mut bank = ctx.accounts.quote_bank.load_mut()?;
let mut bank = ctx.accounts.settle_bank.load_mut()?;
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
// Verify that the bank is the quote currency bank
require!(
bank.token_index == QUOTE_TOKEN_INDEX,
require_eq!(
bank.token_index,
perp_market.settle_token_index,
MangoError::InvalidBank
);
@ -81,8 +85,9 @@ pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) ->
account.fixed.net_settled = cm!(account.fixed.net_settled - settlement_i64);
// Transfer token balances
// TODO: Need to guarantee that QUOTE_TOKEN_INDEX token exists at this point. I.E. create it when placing perp order.
let token_position = account.ensure_token_position(QUOTE_TOKEN_INDEX)?.0;
let token_position = account
.token_position_mut(perp_market.settle_token_index)?
.0;
bank.withdraw_with_fee(token_position, settlement)?;
// Update the settled balance on the market itself
perp_market.fees_settled = cm!(perp_market.fees_settled + settlement);

View File

@ -4,19 +4,25 @@ use fixed::types::I80F48;
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::state::new_fixed_order_account_retriever;
use crate::state::new_health_cache;
use crate::state::Bank;
use crate::state::HealthType;
use crate::state::MangoAccount;
use crate::state::TokenPosition;
use crate::state::QUOTE_TOKEN_INDEX;
use crate::state::ScanningAccountRetriever;
use crate::state::{AccountLoaderDynamic, Group, PerpMarket};
#[derive(Accounts)]
pub struct PerpSettlePnl<'info> {
pub group: AccountLoader<'info, Group>,
#[account(
mut,
has_one = group,
// settler_owner is checked at #1
)]
pub settler: AccountLoaderDynamic<'info, MangoAccount>,
pub settler_owner: Signer<'info>,
#[account(has_one = group, has_one = oracle)]
pub perp_market: AccountLoader<'info, PerpMarket>,
@ -31,26 +37,26 @@ pub struct PerpSettlePnl<'info> {
pub oracle: UncheckedAccount<'info>,
#[account(mut, has_one = group)]
pub quote_bank: AccountLoader<'info, Bank>,
pub settle_bank: AccountLoader<'info, Bank>,
/// CHECK: Oracle can have different account types
#[account(address = settle_bank.load()?.oracle)]
pub settle_oracle: UncheckedAccount<'info>,
}
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: u64) -> Result<()> {
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
// Cannot settle with yourself
require!(
ctx.accounts.account_a.to_account_info().key
!= ctx.accounts.account_b.to_account_info().key,
ctx.accounts.account_a.key() != ctx.accounts.account_b.key(),
MangoError::CannotSettleWithSelf
);
// max_settle_amount must greater than zero
require!(
max_settle_amount > 0,
MangoError::MaxSettleAmountMustBeGreaterThanZero
);
let perp_market_index = {
let (perp_market_index, settle_token_index) = {
let perp_market = ctx.accounts.perp_market.load()?;
perp_market.perp_market_index
(
perp_market.perp_market_index,
perp_market.settle_token_index,
)
};
let mut account_a = ctx.accounts.account_a.load_mut()?;
@ -59,29 +65,37 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: u64) -> R
// check positions exist, for nicer error messages
{
account_a.perp_position(perp_market_index)?;
account_a.token_position(QUOTE_TOKEN_INDEX)?;
account_a.token_position(settle_token_index)?;
account_b.perp_position(perp_market_index)?;
account_b.token_position(QUOTE_TOKEN_INDEX)?;
account_b.token_position(settle_token_index)?;
}
let a_init_health;
let a_maint_health;
let b_settle_health;
{
let retriever =
ScanningAccountRetriever::new(ctx.remaining_accounts, &ctx.accounts.group.key())
.context("create account retriever")?;
b_settle_health = new_health_cache(&account_b.borrow(), &retriever)?.perp_settle_health();
let a_cache = new_health_cache(&account_a.borrow(), &retriever)?;
a_init_health = a_cache.health(HealthType::Init);
a_maint_health = a_cache.health(HealthType::Maint);
};
// Account B is the one that must have negative pnl. Check how much of that may be actualized
// given the account's health. In that, we only care about the health of spot assets on the account.
// Example: With +100 USDC and -2 SOL (-80 USD) and -500 USD PNL the account may still settle
// 100 - 1.1*80 = 12 USD perp pnl, even though the overall health is already negative.
// Afterwards the account is perp-bankrupt.
let b_spot_health = {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account_b.borrow())?;
new_health_cache(&account_b.borrow(), &retriever)?.spot_health(HealthType::Maint)
};
require!(b_spot_health >= 0, MangoError::HealthMustBePositive);
// Further settlement would convert perp-losses into token-losses and isn't allowed.
require!(b_settle_health >= 0, MangoError::HealthMustBePositive);
let mut bank = ctx.accounts.quote_bank.load_mut()?;
let mut bank = ctx.accounts.settle_bank.load_mut()?;
let perp_market = ctx.accounts.perp_market.load()?;
// Verify that the bank is the quote currency bank
require!(
bank.token_index == QUOTE_TOKEN_INDEX,
bank.token_index == settle_token_index,
MangoError::InvalidBank
);
@ -108,36 +122,67 @@ pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: u64) -> R
require!(a_pnl.is_positive(), MangoError::ProfitabilityMismatch);
require!(b_pnl.is_negative(), MangoError::ProfitabilityMismatch);
// Settle for the maximum possible capped to max_settle_amount and b's spot health
let settlement = a_pnl
.abs()
.min(b_pnl.abs())
.min(b_spot_health)
.min(I80F48::from(max_settle_amount));
// Settle for the maximum possible capped to b's settle health
let settlement = a_pnl.abs().min(b_pnl.abs()).min(b_settle_health);
a_perp_position.change_quote_position(-settlement);
b_perp_position.change_quote_position(settlement);
// Update the account's net_settled with the new PnL
// A percentage fee is paid to the settler when account_a's health is low.
// That's because the settlement could avoid it getting liquidated.
let low_health_fee = if a_init_health < 0 {
let fee_fraction = I80F48::from_num(perp_market.settle_fee_fraction_low_health);
if a_maint_health < 0 {
cm!(settlement * fee_fraction)
} else {
cm!(settlement * fee_fraction * (-a_init_health / (a_maint_health - a_init_health)))
}
} else {
I80F48::ZERO
};
// The settler receives a flat fee
let flat_fee = I80F48::from_num(perp_market.settle_fee_flat);
// Fees only apply when the settlement is large enough
let fee = if settlement >= perp_market.settle_fee_amount_threshold {
cm!(low_health_fee + flat_fee).min(settlement)
} else {
I80F48::ZERO
};
// Update the account's net_settled with the new PnL.
// Applying the fee here means that it decreases the displayed perp pnl.
let settlement_i64 = settlement.checked_to_num::<i64>().unwrap();
cm!(account_a.fixed.net_settled += settlement_i64);
let fee_i64 = fee.checked_to_num::<i64>().unwrap();
cm!(account_a.fixed.net_settled += settlement_i64 - fee_i64);
cm!(account_b.fixed.net_settled -= settlement_i64);
// Transfer token balances
let a_token_position = account_a.token_position_mut(QUOTE_TOKEN_INDEX)?.0;
let b_token_position = account_b.token_position_mut(QUOTE_TOKEN_INDEX)?.0;
transfer_token_internal(&mut bank, b_token_position, a_token_position, settlement)?;
// The fee is paid by the account with positive unsettled pnl
let a_token_position = account_a.token_position_mut(settle_token_index)?.0;
let b_token_position = account_b.token_position_mut(settle_token_index)?.0;
bank.deposit(a_token_position, cm!(settlement - fee))?;
bank.withdraw_with_fee(b_token_position, settlement)?;
msg!("settled pnl = {}", settlement);
Ok(())
}
fn transfer_token_internal(
bank: &mut Bank,
from_position: &mut TokenPosition,
to_position: &mut TokenPosition,
native_amount: I80F48,
) -> Result<()> {
bank.deposit(to_position, native_amount)?;
bank.withdraw_with_fee(from_position, native_amount)?;
// settler might be the same as account a or b
drop(account_a);
drop(account_b);
let mut settler = ctx.accounts.settler.load_mut()?;
// account constraint #1
require!(
settler
.fixed
.is_owner_or_delegate(ctx.accounts.settler_owner.key()),
MangoError::SomeError
);
let (settler_token_position, settler_token_raw_index, _) =
settler.ensure_token_position(settle_token_index)?;
if !bank.deposit(settler_token_position, fee)? {
settler.deactivate_token_position(settler_token_raw_index);
}
msg!("settled pnl = {}, fee = {}", settlement, fee);
Ok(())
}

View File

@ -1,6 +1,9 @@
use anchor_lang::prelude::*;
use super::{OpenOrdersAmounts, OpenOrdersSlim};
use crate::error::*;
use crate::logs::Serum3OpenOrdersBalanceLog;
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
#[derive(Accounts)]
@ -72,6 +75,22 @@ pub fn serum3_cancel_all_orders(ctx: Context<Serum3CancelAllOrders>, limit: u8)
//
cpi_cancel_all_orders(ctx.accounts, limit)?;
let serum_market = ctx.accounts.serum_market.load()?;
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
emit!(Serum3OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
Ok(())
}

View File

@ -7,6 +7,10 @@ use crate::state::*;
use super::Serum3Side;
use super::{OpenOrdersAmounts, OpenOrdersSlim};
use crate::logs::Serum3OpenOrdersBalanceLog;
use crate::serum3_cpi::load_open_orders_ref;
#[derive(Accounts)]
pub struct Serum3CancelOrder<'info> {
pub group: AccountLoader<'info, Group>,
@ -85,6 +89,21 @@ pub fn serum3_cancel_order(
};
cpi_cancel_order(ctx.accounts, order)?;
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
emit!(Serum3OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
Ok(())
}

View File

@ -4,8 +4,10 @@ use fixed::types::I80F48;
use crate::error::*;
use crate::instructions::{
apply_vault_difference, charge_loan_origination_fees, OODifference, OpenOrdersSlim,
apply_vault_difference, charge_loan_origination_fees, OODifference, OpenOrdersAmounts,
OpenOrdersSlim,
};
use crate::logs::Serum3OpenOrdersBalanceLog;
use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
@ -178,6 +180,19 @@ pub fn serum3_liq_force_cancel_orders(
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
emit!(Serum3OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
OODifference::new(&before_oo, &after_oo)
.adjust_health_cache(&mut health_cache, &serum_market)?;
};
@ -196,6 +211,7 @@ pub fn serum3_liq_force_cancel_orders(
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank,
@ -204,6 +220,7 @@ pub fn serum3_liq_force_cancel_orders(
)?
.adjust_health_cache(&mut health_cache)?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
serum_market.market_index,
&mut quote_bank,

View File

@ -1,5 +1,6 @@
use crate::error::*;
use crate::logs::{Serum3OpenOrdersBalanceLog, TokenBalanceLog};
use crate::serum3_cpi::{load_market_state, load_open_orders_ref};
use crate::state::*;
use anchor_lang::prelude::*;
@ -39,7 +40,9 @@ pub trait OpenOrdersAmounts {
fn native_quote_free(&self) -> u64;
fn native_quote_free_plus_rebates(&self) -> u64;
fn native_base_total(&self) -> u64;
fn native_quote_total(&self) -> u64;
fn native_quote_total_plus_rebates(&self) -> u64;
fn native_rebates(&self) -> u64;
}
impl OpenOrdersAmounts for OpenOrdersSlim {
@ -61,9 +64,15 @@ impl OpenOrdersAmounts for OpenOrdersSlim {
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total(&self) -> u64 {
self.native_pc_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
fn native_rebates(&self) -> u64 {
self.referrer_rebates_accrued
}
}
impl OpenOrdersAmounts for OpenOrders {
@ -85,9 +94,15 @@ impl OpenOrdersAmounts for OpenOrders {
fn native_base_total(&self) -> u64 {
self.native_coin_total
}
fn native_quote_total(&self) -> u64 {
self.native_pc_total
}
fn native_quote_total_plus_rebates(&self) -> u64 {
cm!(self.native_pc_total + self.referrer_rebates_accrued)
}
fn native_rebates(&self) -> u64 {
self.referrer_rebates_accrued
}
}
/// Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them
@ -298,6 +313,19 @@ pub fn serum3_place_order(
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
emit!(Serum3OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
base_total: after_oo.native_coin_total,
base_free: after_oo.native_coin_free,
quote_total: after_oo.native_pc_total,
quote_free: after_oo.native_pc_free,
referrer_rebates_accrued: after_oo.referrer_rebates_accrued,
});
OODifference::new(&before_oo, &after_oo)
};
@ -314,6 +342,7 @@ pub fn serum3_place_order(
let vault_difference = {
let mut payer_bank = ctx.accounts.payer_bank.load_mut()?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
serum_market.market_index,
&mut payer_bank,
@ -386,7 +415,9 @@ impl VaultDifference {
/// Called in settle_funds, place_order, liq_force_cancel to adjust token positions after
/// changing the vault balances
/// Also logs changes to token balances
pub fn apply_vault_difference(
account_pk: Pubkey,
account: &mut MangoAccountRefMut,
serum_market_index: Serum3MarketIndex,
bank: &mut Bank,
@ -406,6 +437,7 @@ pub fn apply_vault_difference(
.abs()
.to_num::<u64>();
let indexed_position = position.indexed_position;
let market = account.serum3_orders_mut(serum_market_index).unwrap();
let borrows_without_fee = if bank.token_index == market.base_token_index {
&mut market.base_borrows_without_fee
@ -426,6 +458,15 @@ pub fn apply_vault_difference(
*borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::<u64>());
}
emit!(TokenBalanceLog {
mango_group: bank.group,
mango_account: account_pk,
token_index: bank.token_index,
indexed_position: indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
});
Ok(VaultDifference {
token_index: bank.token_index,
native_change,

View File

@ -8,6 +8,7 @@ use crate::serum3_cpi::load_open_orders_ref;
use crate::state::*;
use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim};
use crate::logs::Serum3OpenOrdersBalanceLog;
use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog};
#[derive(Accounts)]
@ -158,6 +159,7 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
let mut base_bank = ctx.accounts.base_bank.load_mut()?;
let mut quote_bank = ctx.accounts.quote_bank.load_mut()?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
serum_market.market_index,
&mut base_bank,
@ -165,6 +167,7 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
before_base_vault,
)?;
apply_vault_difference(
ctx.accounts.account.key(),
&mut account.borrow_mut(),
serum_market.market_index,
&mut quote_bank,
@ -173,6 +176,21 @@ pub fn serum3_settle_funds(ctx: Context<Serum3SettleFunds>) -> Result<()> {
)?;
}
let oo_ai = &ctx.accounts.open_orders.as_ref();
let open_orders = load_open_orders_ref(oo_ai)?;
let after_oo = OpenOrdersSlim::from_oo(&open_orders);
emit!(Serum3OpenOrdersBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
base_token_index: serum_market.base_token_index,
quote_token_index: serum_market.quote_token_index,
base_total: after_oo.native_base_total(),
base_free: after_oo.native_base_free(),
quote_total: after_oo.native_quote_total(),
quote_free: after_oo.native_quote_free(),
referrer_rebates_accrued: after_oo.native_rebates(),
});
Ok(())
}

View File

@ -11,8 +11,9 @@ use crate::util::checked_math as cm;
use crate::logs::{DepositLog, TokenBalanceLog};
// Same as TokenDeposit, but without the owner signing
#[derive(Accounts)]
pub struct TokenDeposit<'info> {
pub struct TokenDepositIntoExisting<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group)]
@ -41,8 +42,50 @@ pub struct TokenDeposit<'info> {
pub token_program: Program<'info, Token>,
}
impl<'info> TokenDeposit<'info> {
pub fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
#[derive(Accounts)]
pub struct TokenDeposit<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group, has_one = owner)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
#[account(
mut,
has_one = group,
has_one = vault,
has_one = oracle,
// the mints of bank/vault/token_account are implicitly the same because
// spl::token::transfer succeeds between token_account and vault
)]
pub bank: AccountLoader<'info, Bank>,
#[account(mut)]
pub vault: Account<'info, TokenAccount>,
/// CHECK: The oracle can be one of several different account types
pub oracle: UncheckedAccount<'info>,
#[account(mut)]
pub token_account: Box<Account<'info, TokenAccount>>,
pub token_authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
struct DepositCommon<'a, 'info> {
pub group: &'a AccountLoader<'info, Group>,
pub account: &'a AccountLoaderDynamic<'info, MangoAccount>,
pub bank: &'a AccountLoader<'info, Bank>,
pub vault: &'a Account<'info, TokenAccount>,
pub oracle: &'a UncheckedAccount<'info>,
pub token_account: &'a Box<Account<'info, TokenAccount>>,
pub token_authority: &'a Signer<'info>,
pub token_program: &'a Program<'info, Token>,
}
impl<'a, 'info> DepositCommon<'a, 'info> {
fn transfer_ctx(&self) -> CpiContext<'_, '_, '_, 'info, token::Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = token::Transfer {
from: self.token_account.to_account_info(),
@ -51,79 +94,119 @@ impl<'info> TokenDeposit<'info> {
};
CpiContext::new(program, accounts)
}
fn deposit_into_existing(
&self,
remaining_accounts: &[AccountInfo],
amount: u64,
allow_token_account_closure: bool,
) -> Result<()> {
require_msg!(amount > 0, "deposit amount must be positive");
let token_index = self.bank.load()?.token_index;
// Get the account's position for that token index
let mut account = self.account.load_mut()?;
let (position, raw_token_index) = account.token_position_mut(token_index)?;
let amount_i80f48 = I80F48::from(amount);
let position_is_active = {
let mut bank = self.bank.load_mut()?;
bank.deposit(position, amount_i80f48)?
};
// Transfer the actual tokens
token::transfer(self.transfer_ctx(), amount)?;
let indexed_position = position.indexed_position;
let bank = self.bank.load()?;
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(self.oracle.as_ref())?)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
cm!(account.fixed.net_deposits += amount_usd);
emit!(TokenBalanceLog {
mango_group: self.group.key(),
mango_account: self.account.key(),
token_index,
indexed_position: indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
});
//
// Health computation
//
// Since depositing can only increase health, we can skip the usual pre-health computation.
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
//
if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
.context("post-deposit init health")?;
msg!("health: {}", health);
account.fixed.maybe_recover_from_being_liquidated(health);
}
//
// Deactivate the position only after the health check because the user passed in
// remaining_accounts for all banks/oracles, including the account that will now be
// deactivated.
// Deposits can deactivate a position if they cancel out a previous borrow.
//
if allow_token_account_closure && !position_is_active {
account.deactivate_token_position_and_log(raw_token_index, self.account.key());
}
emit!(DepositLog {
mango_group: self.group.key(),
mango_account: self.account.key(),
signer: self.token_authority.key(),
token_index,
quantity: amount,
price: oracle_price.to_bits(),
});
Ok(())
}
}
pub fn token_deposit(ctx: Context<TokenDeposit>, amount: u64) -> Result<()> {
require_msg!(amount > 0, "deposit amount must be positive");
let token_index = ctx.accounts.bank.load()?.token_index;
// Get the account's position for that token index
let mut account = ctx.accounts.account.load_mut()?;
let (position, raw_token_index, _active_token_index) =
{
let token_index = ctx.accounts.bank.load()?.token_index;
let mut account = ctx.accounts.account.load_mut()?;
account.ensure_token_position(token_index)?;
let amount_i80f48 = I80F48::from(amount);
let position_is_active = {
let mut bank = ctx.accounts.bank.load_mut()?;
bank.deposit(position, amount_i80f48)?
};
// Transfer the actual tokens
token::transfer(ctx.accounts.transfer_ctx(), amount)?;
let indexed_position = position.indexed_position;
let bank = ctx.accounts.bank.load()?;
let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?;
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::<i64>();
cm!(account.fixed.net_deposits += amount_usd);
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
token_index,
indexed_position: indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: oracle_price.to_bits(),
});
//
// Health computation
//
// Since depositing can only increase health, we can skip the usual pre-health computation.
// Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated.
//
if !account.fixed.is_in_health_region() {
let retriever =
new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?;
let health = compute_health(&account.borrow(), HealthType::Init, &retriever)
.context("post-deposit init health")?;
msg!("health: {}", health);
account.fixed.maybe_recover_from_being_liquidated(health);
}
//
// Deactivate the position only after the health check because the user passed in
// remaining_accounts for all banks/oracles, including the account that will now be
// deactivated.
// Deposits can deactivate a position if they cancel out a previous borrow.
//
if !position_is_active {
account.deactivate_token_position(raw_token_index);
DepositCommon {
group: &ctx.accounts.group,
account: &ctx.accounts.account,
bank: &ctx.accounts.bank,
vault: &ctx.accounts.vault,
oracle: &ctx.accounts.oracle,
token_account: &ctx.accounts.token_account,
token_authority: &ctx.accounts.token_authority,
token_program: &ctx.accounts.token_program,
}
emit!(DepositLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.account.key(),
signer: ctx.accounts.token_authority.key(),
token_index,
quantity: amount,
price: oracle_price.to_bits(),
});
Ok(())
.deposit_into_existing(ctx.remaining_accounts, amount, true)
}
pub fn token_deposit_into_existing(
ctx: Context<TokenDepositIntoExisting>,
amount: u64,
) -> Result<()> {
DepositCommon {
group: &ctx.accounts.group,
account: &ctx.accounts.account,
bank: &ctx.accounts.bank,
vault: &ctx.accounts.vault,
oracle: &ctx.accounts.oracle,
token_account: &ctx.accounts.token_account,
token_authority: &ctx.accounts.token_authority,
token_program: &ctx.accounts.token_program,
}
.deposit_into_existing(ctx.remaining_accounts, amount, false)
}

View File

@ -158,7 +158,7 @@ pub fn token_liq_bankruptcy(
)?;
// move quote assets into liqor and withdraw liab assets
if let Some((quote_bank, quote_price)) = opt_quote_bank_and_price {
if let Some((quote_bank, _)) = opt_quote_bank_and_price {
// account constraint #2 a)
require_keys_eq!(quote_bank.vault, ctx.accounts.quote_vault.key());
require_keys_eq!(quote_bank.mint, ctx.accounts.insurance_vault.mint);
@ -170,7 +170,16 @@ pub fn token_liq_bankruptcy(
let (liqor_quote, liqor_quote_raw_token_index, _) =
liqor.ensure_token_position(QUOTE_TOKEN_INDEX)?;
let liqor_quote_active = quote_bank.deposit(liqor_quote, insurance_transfer_i80f48)?;
let liqor_quote_indexed_position = liqor_quote.indexed_position;
// liqor quote
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqor.key(),
token_index: QUOTE_TOKEN_INDEX,
indexed_position: liqor_quote.indexed_position.to_bits(),
deposit_index: quote_deposit_index.to_bits(),
borrow_index: quote_borrow_index.to_bits(),
});
// transfer liab from liqee to liqor
let (liqor_liab, liqor_liab_raw_token_index, _) =
@ -178,6 +187,16 @@ pub fn token_liq_bankruptcy(
let (liqor_liab_active, loan_origination_fee) =
liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?;
// liqor liab
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqor.key(),
token_index: liab_token_index,
indexed_position: liqor_liab.indexed_position.to_bits(),
deposit_index: liab_deposit_index.to_bits(),
borrow_index: liab_borrow_index.to_bits(),
});
// Check liqor's health
if !liqor.fixed.is_in_health_region() {
let liqor_health =
@ -185,17 +204,6 @@ pub fn token_liq_bankruptcy(
require!(liqor_health >= 0, MangoError::HealthMustBePositive);
}
// liqor quote
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqor.key(),
token_index: QUOTE_TOKEN_INDEX,
indexed_position: liqor_quote_indexed_position.to_bits(),
deposit_index: quote_deposit_index.to_bits(),
borrow_index: quote_borrow_index.to_bits(),
price: quote_price.to_bits(),
});
if loan_origination_fee.is_positive() {
emit!(WithdrawLoanOriginationFeeLog {
mango_group: ctx.accounts.group.key(),
@ -207,10 +215,16 @@ pub fn token_liq_bankruptcy(
}
if !liqor_quote_active {
liqor.deactivate_token_position(liqor_quote_raw_token_index);
liqor.deactivate_token_position_and_log(
liqor_quote_raw_token_index,
ctx.accounts.liqor.key(),
);
}
if !liqor_liab_active {
liqor.deactivate_token_position(liqor_liab_raw_token_index);
liqor.deactivate_token_position_and_log(
liqor_liab_raw_token_index,
ctx.accounts.liqor.key(),
);
}
} else {
// For liab_token_index == QUOTE_TOKEN_INDEX: the insurance fund deposits directly into liqee,
@ -266,17 +280,6 @@ pub fn token_liq_bankruptcy(
require_eq!(liqee_liab.indexed_position, I80F48::ZERO);
}
// liqor liab
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqor.key(),
token_index: liab_token_index,
indexed_position: liqee_liab.indexed_position.to_bits(),
deposit_index: liab_deposit_index.to_bits(),
borrow_index: liab_borrow_index.to_bits(),
price: liab_price.to_bits(),
});
// liqee liab
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
@ -285,7 +288,6 @@ pub fn token_liq_bankruptcy(
indexed_position: liqee_liab.indexed_position.to_bits(),
deposit_index: liab_deposit_index.to_bits(),
borrow_index: liab_borrow_index.to_bits(),
price: liab_price.to_bits(),
});
let liab_bank = bank_ais[0].load::<Bank>()?;
@ -300,7 +302,7 @@ pub fn token_liq_bankruptcy(
.maybe_recover_from_being_liquidated(liqee_init_health);
if !liqee_liab_active {
liqee.deactivate_token_position(liqee_raw_token_index);
liqee.deactivate_token_position_and_log(liqee_raw_token_index, ctx.accounts.liqee.key());
}
emit!(LiquidateTokenBankruptcyLog {

View File

@ -143,24 +143,24 @@ pub fn token_liq_with_token(
// Apply the balance changes to the liqor and liqee accounts
let liqee_liab_position = liqee.token_position_mut_by_raw_index(liqee_liab_raw_index);
let liqee_liab_active = liab_bank.deposit_with_dusting(liqee_liab_position, liab_transfer)?;
let liqee_liab_position_indexed = liqee_liab_position.indexed_position;
let liqee_liab_indexed_position = liqee_liab_position.indexed_position;
let (liqor_liab_position, liqor_liab_raw_index, _) =
liqor.ensure_token_position(liab_token_index)?;
let (liqor_liab_active, loan_origination_fee) =
liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?;
let liqor_liab_position_indexed = liqor_liab_position.indexed_position;
let liqor_liab_indexed_position = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(liab_bank);
let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.ensure_token_position(asset_token_index)?;
let liqor_asset_active = asset_bank.deposit(liqor_asset_position, asset_transfer)?;
let liqor_asset_position_indexed = liqor_asset_position.indexed_position;
let liqor_asset_indexed_position = liqor_asset_position.indexed_position;
let liqee_asset_position = liqee.token_position_mut_by_raw_index(liqee_asset_raw_index);
let liqee_asset_active =
asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?;
let liqee_asset_position_indexed = liqee_asset_position.indexed_position;
let liqee_asset_indexed_position = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(asset_bank);
// Update the health cache
@ -184,40 +184,36 @@ pub fn token_liq_with_token(
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqee.key(),
token_index: asset_token_index,
indexed_position: liqee_asset_position_indexed.to_bits(),
indexed_position: liqee_asset_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
price: asset_price.to_bits(),
});
// liqee liab
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqee.key(),
token_index: liab_token_index,
indexed_position: liqee_liab_position_indexed.to_bits(),
indexed_position: liqee_liab_indexed_position.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
price: liab_price.to_bits(),
});
// liqor asset
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqor.key(),
token_index: asset_token_index,
indexed_position: liqor_asset_position_indexed.to_bits(),
indexed_position: liqor_asset_indexed_position.to_bits(),
deposit_index: asset_bank.deposit_index.to_bits(),
borrow_index: asset_bank.borrow_index.to_bits(),
price: asset_price.to_bits(),
});
// liqor liab
emit!(TokenBalanceLog {
mango_group: ctx.accounts.group.key(),
mango_account: ctx.accounts.liqor.key(),
token_index: liab_token_index,
indexed_position: liqor_liab_position_indexed.to_bits(),
indexed_position: liqor_liab_indexed_position.to_bits(),
deposit_index: liab_bank.deposit_index.to_bits(),
borrow_index: liab_bank.borrow_index.to_bits(),
price: liab_price.to_bits(),
});
if loan_origination_fee.is_positive() {
@ -232,16 +228,16 @@ pub fn token_liq_with_token(
// Since we use a scanning account retriever, it's safe to deactivate inactive token positions
if !liqee_asset_active {
liqee.deactivate_token_position(liqee_asset_raw_index);
liqee.deactivate_token_position_and_log(liqee_asset_raw_index, ctx.accounts.liqee.key());
}
if !liqee_liab_active {
liqee.deactivate_token_position(liqee_liab_raw_index);
liqee.deactivate_token_position_and_log(liqee_liab_raw_index, ctx.accounts.liqee.key());
}
if !liqor_asset_active {
liqor.deactivate_token_position(liqor_asset_raw_index);
liqor.deactivate_token_position_and_log(liqor_asset_raw_index, ctx.accounts.liqor.key());
}
if !liqor_liab_active {
liqor.deactivate_token_position(liqor_liab_raw_index)
liqor.deactivate_token_position_and_log(liqor_liab_raw_index, ctx.accounts.liqor.key())
}
// Check liqee health again

View File

@ -131,7 +131,6 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
indexed_position: position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: oracle_price.to_bits(),
});
// Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms)
@ -153,7 +152,7 @@ pub fn token_withdraw(ctx: Context<TokenWithdraw>, amount: u64, allow_borrow: bo
// deactivated.
//
if !position_is_active {
account.deactivate_token_position(raw_token_index);
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
}
emit!(WithdrawLog {

View File

@ -211,6 +211,13 @@ pub mod mango_v4 {
instructions::token_deposit(ctx, amount)
}
pub fn token_deposit_into_existing(
ctx: Context<TokenDepositIntoExisting>,
amount: u64,
) -> Result<()> {
instructions::token_deposit_into_existing(ctx, amount)
}
pub fn token_withdraw(
ctx: Context<TokenWithdraw>,
amount: u64,
@ -268,8 +275,6 @@ pub mod mango_v4 {
instructions::serum3_deregister_market(ctx)
}
// TODO serum3_change_spot_market_params
pub fn serum3_create_open_orders(ctx: Context<Serum3CreateOpenOrders>) -> Result<()> {
instructions::serum3_create_open_orders(ctx)
}
@ -326,8 +331,6 @@ pub mod mango_v4 {
instructions::serum3_liq_force_cancel_orders(ctx, limit)
}
// TODO serum3_cancel_all_spot_orders
// DEPRECATED: use token_liq_with_token
pub fn liq_token_with_token(
ctx: Context<TokenLiqWithToken>,
@ -397,9 +400,15 @@ pub mod mango_v4 {
impact_quantity: i64,
group_insurance_fund: bool,
trusted_market: bool,
fee_penalty: f32,
settle_fee_flat: f32,
settle_fee_amount_threshold: f32,
settle_fee_fraction_low_health: f32,
settle_token_index: TokenIndex,
) -> Result<()> {
instructions::perp_create_market(
ctx,
settle_token_index,
perp_market_index,
name,
oracle_config,
@ -413,11 +422,15 @@ pub mod mango_v4 {
liquidation_fee,
maker_fee,
taker_fee,
max_funding,
min_funding,
max_funding,
impact_quantity,
group_insurance_fund,
trusted_market,
fee_penalty,
settle_fee_flat,
settle_fee_amount_threshold,
settle_fee_fraction_low_health,
)
}
@ -439,6 +452,10 @@ pub mod mango_v4 {
impact_quantity_opt: Option<i64>,
group_insurance_fund_opt: Option<bool>,
trusted_market_opt: Option<bool>,
fee_penalty_opt: Option<f32>,
settle_fee_flat_opt: Option<f32>,
settle_fee_amount_threshold_opt: Option<f32>,
settle_fee_fraction_low_health_opt: Option<f32>,
) -> Result<()> {
instructions::perp_edit_market(
ctx,
@ -457,6 +474,10 @@ pub mod mango_v4 {
impact_quantity_opt,
group_insurance_fund_opt,
trusted_market_opt,
fee_penalty_opt,
settle_fee_flat_opt,
settle_fee_amount_threshold_opt,
settle_fee_fraction_low_health_opt,
)
}
@ -526,8 +547,8 @@ pub mod mango_v4 {
instructions::perp_update_funding(ctx)
}
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>, max_settle_amount: u64) -> Result<()> {
instructions::perp_settle_pnl(ctx, max_settle_amount)
pub fn perp_settle_pnl(ctx: Context<PerpSettlePnl>) -> Result<()> {
instructions::perp_settle_pnl(ctx)
}
pub fn perp_settle_fees(ctx: Context<PerpSettleFees>, max_settle_amount: u64) -> Result<()> {

View File

@ -10,7 +10,6 @@ pub fn emit_perp_balances(
mango_group: Pubkey,
mango_account: Pubkey,
market_index: u64,
price: i64,
pp: &PerpPosition,
pm: &PerpMarket,
) {
@ -22,7 +21,6 @@ pub fn emit_perp_balances(
quote_position: pp.quote_position_native().to_bits(),
long_settled_funding: pp.long_settled_funding.to_bits(),
short_settled_funding: pp.short_settled_funding.to_bits(),
price,
long_funding: pm.long_funding.to_bits(),
short_funding: pm.short_funding.to_bits(),
});
@ -37,9 +35,8 @@ pub struct PerpBalanceLog {
pub quote_position: i128, // I80F48
pub long_settled_funding: i128, // I80F48
pub short_settled_funding: i128, // I80F48
pub price: i64,
pub long_funding: i128, // I80F48
pub short_funding: i128, // I80F48
pub long_funding: i128, // I80F48
pub short_funding: i128, // I80F48
}
#[event]
@ -50,7 +47,6 @@ pub struct TokenBalanceLog {
pub indexed_position: i128, // on client convert i128 to I80F48 easily by passing in the BN to I80F48 ctor
pub deposit_index: i128, // I80F48
pub borrow_index: i128, // I80F48
pub price: i128, // I80F48
}
#[derive(AnchorSerialize, AnchorDeserialize)]
@ -165,17 +161,17 @@ pub struct LiquidateTokenAndTokenLog {
}
#[event]
pub struct OpenOrdersBalanceLog {
pub struct Serum3OpenOrdersBalanceLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub market_index: u16,
pub base_token_index: u16,
pub quote_token_index: u16,
pub base_total: u64,
pub base_free: u64,
/// this field does not include the referrer_rebates; need to add that in to get true total
pub quote_total: u64,
pub quote_free: u64,
pub referrer_rebates_accrued: u64,
pub price: i128, // I80F48
}
#[derive(PartialEq, Copy, Clone, Debug, AnchorSerialize, AnchorDeserialize)]
@ -211,3 +207,12 @@ pub struct LiquidateTokenBankruptcyLog {
pub insurance_transfer: i128,
pub socialized_loss: i128,
}
#[event]
pub struct DeactivateTokenPositionLog {
pub mango_group: Pubkey,
pub mango_account: Pubkey,
pub token_index: u16,
pub cumulative_deposit_interest: f32,
pub cumulative_borrow_interest: f32,
}

View File

@ -210,6 +210,7 @@ impl Bank {
) -> Result<bool> {
require_gte!(native_amount, 0);
let native_position = position.native(self);
let opening_indexed_position = position.indexed_position;
// Adding DELTA to amount/index helps because (amount/index)*index <= amount, but
// we want to ensure that users can withdraw the same amount they have deposited, so
@ -235,12 +236,14 @@ impl Bank {
// pay back borrows only, leaving a negative position
cm!(self.indexed_borrows -= indexed_change);
position.indexed_position = new_indexed_value;
self.update_cumulative_interest(position, opening_indexed_position);
return Ok(true);
} else if new_native_position < I80F48::ONE && allow_dusting {
// if there's less than one token deposited, zero the position
cm!(self.dust += new_native_position);
cm!(self.indexed_borrows += position.indexed_position);
position.indexed_position = I80F48::ZERO;
self.update_cumulative_interest(position, opening_indexed_position);
return Ok(false);
}
@ -256,6 +259,7 @@ impl Bank {
let indexed_change = div_rounding_up(native_amount, self.deposit_index);
cm!(self.indexed_deposits += indexed_change);
cm!(position.indexed_position += indexed_change);
self.update_cumulative_interest(position, opening_indexed_position);
Ok(true)
}
@ -317,6 +321,7 @@ impl Bank {
) -> Result<(bool, I80F48)> {
require_gte!(native_amount, 0);
let native_position = position.native(self);
let opening_indexed_position = position.indexed_position;
if native_position.is_positive() {
let new_native_position = cm!(native_position - native_amount);
@ -327,12 +332,14 @@ impl Bank {
cm!(self.dust += new_native_position);
cm!(self.indexed_deposits -= position.indexed_position);
position.indexed_position = I80F48::ZERO;
self.update_cumulative_interest(position, opening_indexed_position);
return Ok((false, I80F48::ZERO));
} else {
// withdraw some deposits leaving a positive balance
let indexed_change = cm!(native_amount / self.deposit_index);
cm!(self.indexed_deposits -= indexed_change);
cm!(position.indexed_position -= indexed_change);
self.update_cumulative_interest(position, opening_indexed_position);
return Ok((true, I80F48::ZERO));
}
}
@ -355,6 +362,7 @@ impl Bank {
let indexed_change = cm!(native_amount / self.borrow_index);
cm!(self.indexed_borrows += indexed_change);
cm!(position.indexed_position -= indexed_change);
self.update_cumulative_interest(position, opening_indexed_position);
Ok((true, loan_origination_fee))
}
@ -401,6 +409,30 @@ impl Bank {
}
}
pub fn update_cumulative_interest(
&self,
position: &mut TokenPosition,
opening_indexed_position: I80F48,
) {
if opening_indexed_position.is_positive() {
let interest =
cm!((self.deposit_index - position.previous_index) * opening_indexed_position)
.to_num::<f32>();
position.cumulative_deposit_interest += interest;
} else {
let interest =
cm!((self.borrow_index - position.previous_index) * opening_indexed_position)
.to_num::<f32>();
position.cumulative_borrow_interest += interest;
}
if position.indexed_position.is_positive() {
position.previous_index = self.deposit_index
} else {
position.previous_index = self.borrow_index
}
}
pub fn compute_index(
&self,
indexed_total_deposits: I80F48,
@ -634,8 +666,11 @@ mod tests {
indexed_position: I80F48::ZERO,
token_index: 0,
in_use_count: if is_in_use { 1 } else { 0 },
cumulative_deposit_interest: 0.0,
cumulative_borrow_interest: 0.0,
previous_index: I80F48::ZERO,
padding: Default::default(),
reserved: [0; 40],
reserved: [0; 16],
};
account.indexed_position = indexed(I80F48::from_num(start), &bank);

View File

@ -17,6 +17,9 @@ use crate::state::{
};
use crate::util::checked_math as cm;
#[cfg(feature = "client")]
use crate::state::orderbook::order_type::Side as PerpOrderSide;
use super::MangoAccountRef;
const ONE_NATIVE_USDC_IN_USD: I80F48 = I80F48!(0.000001);
@ -521,8 +524,9 @@ pub struct PerpInfo {
pub base: I80F48,
// in health-reference-token native units, no asset/liab factor needed
pub quote: I80F48,
oracle_price: I80F48,
has_open_orders: bool,
pub oracle_price: I80F48,
pub has_open_orders: bool,
pub trusted_market: bool,
}
impl PerpInfo {
@ -609,13 +613,14 @@ impl PerpInfo {
quote,
oracle_price,
has_open_orders: perp_position.has_open_orders(),
trusted_market: perp_market.trusted_market(),
})
}
/// Total health contribution from perp balances
///
/// Due to isolation of perp markets, users may never borrow against perp
/// positions without settling first: perp health is capped at zero.
/// positions in untrusted without settling first: perp health is capped at zero.
///
/// Users need to settle their perp pnl with other perp market participants
/// in order to realize their gains if they want to use them as collateral.
@ -626,6 +631,17 @@ impl PerpInfo {
/// balances they could now borrow other assets).
#[inline(always)]
fn health_contribution(&self, health_type: HealthType) -> I80F48 {
let c = self.uncapped_health_contribution(health_type);
if self.trusted_market {
c
} else {
c.min(I80F48::ZERO)
}
}
#[inline(always)]
fn uncapped_health_contribution(&self, health_type: HealthType) -> I80F48 {
let weight = match (health_type, self.base.is_negative()) {
(HealthType::Init, true) => self.init_liab_weight,
(HealthType::Init, false) => self.init_asset_weight,
@ -633,9 +649,7 @@ impl PerpInfo {
(HealthType::Maint, false) => self.maint_asset_weight,
};
// FUTURE: Allow v3-style "reliable" markets where we can return
// `self.quote + weight * self.base` here
cm!(self.quote + weight * self.base).min(I80F48::ZERO)
cm!(self.quote + weight * self.base)
}
}
@ -691,6 +705,14 @@ impl HealthCache {
.ok_or_else(|| error_msg!("token index {} not found", token_index))
}
#[cfg(feature = "client")]
fn perp_info_index(&self, perp_market_index: PerpMarketIndex) -> Result<usize> {
self.perp_infos
.iter()
.position(|pi| pi.perp_market_index == perp_market_index)
.ok_or_else(|| error_msg!("perp market index {} not found", perp_market_index))
}
pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> {
let entry_index = self.token_info_index(token_index)?;
let mut entry = &mut self.token_infos[entry_index];
@ -774,7 +796,7 @@ impl HealthCache {
// can use perp_liq_base_position
p.base != 0
// can use perp_settle_pnl
|| p.quote > ONE_NATIVE_USDC_IN_USD
|| 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
});
@ -819,7 +841,18 @@ impl HealthCache {
}
}
pub fn spot_health(&self, health_type: HealthType) -> I80F48 {
/// Compute the health when it comes to settling perp pnl
///
/// Examples:
/// - An account may have maint_health < 0, but settling perp pnl could still be allowed.
/// (+100 USDC health, -50 USDT health, -50 perp health -> allow settling 50 health worth)
/// - Positive health from trusted pnl markets counts
/// - If overall health is 0 with two trusted perp pnl < 0, settling may still be possible.
/// (+100 USDC health, -150 perp1 health, -150 perp2 health -> allow settling 100 health worth)
/// - Positive trusted perp pnl can enable settling.
/// (+100 trusted perp1 health, -100 perp2 health -> allow settling of 100 health worth)
pub fn perp_settle_health(&self) -> I80F48 {
let health_type = HealthType::Maint;
let mut health = I80F48::ZERO;
for token_info in self.token_infos.iter() {
let contrib = token_info.health_contribution(health_type);
@ -829,6 +862,12 @@ impl HealthCache {
let contrib = serum3_info.health_contribution(health_type, &self.token_infos);
cm!(health += contrib);
}
for perp_info in self.perp_infos.iter() {
if perp_info.trusted_market {
let positive_contrib = perp_info.health_contribution(health_type).max(I80F48::ZERO);
cm!(health += positive_contrib);
}
}
health
}
@ -860,7 +899,8 @@ impl HealthCache {
let (assets, liabs) = self.health_assets_and_liabs(health_type);
let hundred = I80F48::from(100);
if liabs > 0 {
cm!(hundred * (assets - liabs) / liabs)
// feel free to saturate to MAX for tiny liabs
cm!(hundred * (assets - liabs)).saturating_div(liabs)
} else {
I80F48::MAX
}
@ -873,6 +913,8 @@ impl HealthCache {
/// swap BTC -> SOL and they're at ui prices of $20000 and $40, that means price
/// should be 500000 native_SOL for a native_BTC. Because 1 BTC gives you 500 SOL
/// so 1e6 native_BTC gives you 500e9 native_SOL.
///
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
#[cfg(feature = "client")]
pub fn max_swap_source_for_health_ratio(
&self,
@ -942,40 +984,6 @@ impl HealthCache {
)
};
let binary_approximation_search =
|mut left,
left_ratio: I80F48,
mut right,
mut right_ratio: I80F48,
target_ratio: I80F48| {
let max_iterations = 20;
let target_error = I80F48!(0.01);
require_msg!(
(left_ratio - target_ratio).signum() * (right_ratio - target_ratio).signum()
!= I80F48::ONE,
"internal error: left {} and right {} don't contain the target value {}",
left_ratio,
right_ratio,
target_ratio
);
for _ in 0..max_iterations {
let new = I80F48::from_num(0.5) * (left + right);
let new_ratio = health_ratio_after_swap(new);
let error = new_ratio - target_ratio;
if error > 0 && error < target_error {
return Ok(new);
}
if (new_ratio > target_ratio) ^ (right_ratio > target_ratio) {
left = new;
} else {
right = new;
right_ratio = new_ratio;
}
}
Err(error_msg!("binary search iterations exhausted"))
};
let amount =
if initial_ratio <= min_ratio && point0_ratio < min_ratio && point1_ratio < min_ratio {
// If we have to stay below the target ratio, pick the highest one
@ -1000,35 +1008,179 @@ impl HealthCache {
}
let zero_health_amount = point1_amount - point1_health / final_health_slope;
let zero_health_ratio = health_ratio_after_swap(zero_health_amount);
binary_approximation_search(
binary_search(
point1_amount,
point1_ratio,
zero_health_amount,
zero_health_ratio,
min_ratio,
health_ratio_after_swap,
)?
} else if point0_ratio >= min_ratio {
// Must be between point0_amount and point1_amount.
binary_approximation_search(
binary_search(
point0_amount,
point0_ratio,
point1_amount,
point1_ratio,
min_ratio,
health_ratio_after_swap,
)?
} else {
// Must be between 0 and point0_amount
binary_approximation_search(
binary_search(
I80F48::ZERO,
initial_ratio,
point0_amount,
point0_ratio,
min_ratio,
health_ratio_after_swap,
)?
};
Ok(amount / source.oracle_price)
}
#[cfg(feature = "client")]
/// NOTE: keep getMaxSourceForTokenSwap in ts/client in sync with changes here
pub fn max_perp_for_health_ratio(
&self,
perp_market_index: PerpMarketIndex,
price: I80F48,
base_lot_size: i64,
side: PerpOrderSide,
min_ratio: I80F48,
) -> Result<i64> {
let base_lot_size = I80F48::from(base_lot_size);
let initial_ratio = self.health_ratio(HealthType::Init);
if initial_ratio < 0 {
return Ok(0);
}
let direction = match side {
PerpOrderSide::Bid => 1,
PerpOrderSide::Ask => -1,
};
let perp_info_index = self.perp_info_index(perp_market_index)?;
let perp_info = &self.perp_infos[perp_info_index];
let oracle_price = perp_info.oracle_price;
// If the price is sufficiently good then health will just increase from trading
let final_health_slope = if direction == 1 {
perp_info.init_asset_weight * oracle_price - price
} else {
price - perp_info.init_liab_weight * oracle_price
};
if final_health_slope >= 0 {
return Ok(i64::MAX);
}
let cache_after_trade = |base_lots: I80F48| {
let mut adjusted_cache = self.clone();
let d = I80F48::from(direction);
adjusted_cache.perp_infos[perp_info_index].base +=
d * base_lots * base_lot_size * oracle_price;
adjusted_cache.perp_infos[perp_info_index].quote -=
d * base_lots * base_lot_size * price;
adjusted_cache
};
let health_ratio_after_trade =
|base_lots| cache_after_trade(base_lots).health_ratio(HealthType::Init);
// This is awkward, can we pass the base_lots and lot_size in PerpInfo?
let initial_base_lots = perp_info.base / perp_info.oracle_price / base_lot_size;
// There are two cases:
// 1. We are increasing abs(base_lots)
// 2. We are bringing the base position to 0, and then going to case 1.
let has_case2 =
initial_base_lots > 0 && direction == -1 || initial_base_lots < 0 && direction == 1;
let (case1_start, case1_start_ratio) = if has_case2 {
let case1_start = initial_base_lots.abs();
let case1_start_ratio = health_ratio_after_trade(case1_start);
(case1_start, case1_start_ratio)
} else {
(I80F48::ZERO, initial_ratio)
};
// If we start out below min_ratio and can't go above, pick the best case
let base_lots = if initial_ratio <= min_ratio && case1_start_ratio < min_ratio {
if case1_start_ratio >= initial_ratio {
case1_start
} else {
I80F48::ZERO
}
} else if case1_start_ratio >= min_ratio {
// Must reach min_ratio to the right of case1_start
let case1_start_health = cache_after_trade(case1_start).health(HealthType::Init);
if case1_start_health <= 0 {
return Ok(0);
}
let zero_health_amount =
case1_start - case1_start_health / final_health_slope / base_lot_size;
let zero_health_ratio = health_ratio_after_trade(zero_health_amount);
binary_search(
case1_start,
case1_start_ratio,
zero_health_amount,
zero_health_ratio,
min_ratio,
health_ratio_after_trade,
)?
} else {
// Between 0 and case1_start
binary_search(
I80F48::ZERO,
initial_ratio,
case1_start,
case1_start_ratio,
min_ratio,
health_ratio_after_trade,
)?
};
// truncate result
Ok(base_lots.floor().to_num())
}
}
#[cfg(feature = "client")]
fn binary_search(
mut left: I80F48,
left_value: I80F48,
mut right: I80F48,
right_value: I80F48,
target_value: I80F48,
fun: impl Fn(I80F48) -> I80F48,
) -> Result<I80F48> {
let max_iterations = 20;
let target_error = I80F48!(0.1);
require_msg!(
(left_value - target_value).signum() * (right_value - target_value).signum() != I80F48::ONE,
"internal error: left {} and right {} don't contain the target value {}",
left_value,
right_value,
target_value
);
for _ in 0..max_iterations {
let new = I80F48::from_num(0.5) * (left + right);
let new_value = fun(new);
let error = new_value - target_value;
if error > 0 && error < target_error {
return Ok(new);
}
if (new_value > target_value) ^ (right_value > target_value) {
left = new;
} else {
right = new;
}
}
Err(error_msg!("binary search iterations exhausted"))
}
fn find_token_info_index(infos: &[TokenInfo], token_index: TokenIndex) -> Result<usize> {
@ -1676,19 +1828,16 @@ mod tests {
TokenInfo {
token_index: 0,
oracle_price: I80F48::from_num(2.0),
balance: I80F48::ZERO,
..default_token_info(0.1)
},
TokenInfo {
token_index: 1,
oracle_price: I80F48::from_num(3.0),
balance: I80F48::ZERO,
..default_token_info(0.2)
},
TokenInfo {
token_index: 2,
oracle_price: I80F48::from_num(4.0),
balance: I80F48::ZERO,
..default_token_info(0.3)
},
],
@ -1838,6 +1987,155 @@ mod tests {
}
}
#[test]
fn test_max_perp() {
let default_token_info = |x| TokenInfo {
token_index: 0,
maint_asset_weight: I80F48::from_num(1.0 - x),
init_asset_weight: I80F48::from_num(1.0 - x),
maint_liab_weight: I80F48::from_num(1.0 + x),
init_liab_weight: I80F48::from_num(1.0 + x),
oracle_price: I80F48::from_num(2.0),
balance: I80F48::ZERO,
serum3_max_reserved: I80F48::ZERO,
};
let default_perp_info = |x| PerpInfo {
perp_market_index: 0,
maint_asset_weight: I80F48::from_num(1.0 - x),
init_asset_weight: I80F48::from_num(1.0 - x),
maint_liab_weight: I80F48::from_num(1.0 + x),
init_liab_weight: I80F48::from_num(1.0 + x),
base: I80F48::ZERO,
quote: I80F48::ZERO,
oracle_price: I80F48::from_num(2.0),
has_open_orders: false,
trusted_market: false,
};
let health_cache = HealthCache {
token_infos: vec![TokenInfo {
token_index: 0,
oracle_price: I80F48::from_num(1.0),
balance: I80F48::ZERO,
..default_token_info(0.0)
}],
serum3_infos: vec![],
perp_infos: vec![PerpInfo {
perp_market_index: 0,
..default_perp_info(0.3)
}],
being_liquidated: false,
};
let base_lot_size = 100;
assert_eq!(health_cache.health(HealthType::Init), I80F48::ZERO);
assert_eq!(health_cache.health_ratio(HealthType::Init), I80F48::MAX);
assert_eq!(
health_cache
.max_perp_for_health_ratio(
0,
I80F48::from(2),
base_lot_size,
PerpOrderSide::Bid,
I80F48::from_num(50.0)
)
.unwrap(),
I80F48::ZERO
);
let adjust_token = |c: &mut HealthCache, value: f64| {
let ti = &mut c.token_infos[0];
ti.balance += I80F48::from_num(value);
};
let find_max_trade =
|c: &HealthCache, side: PerpOrderSide, ratio: f64, price_factor: f64| {
let oracle_price = c.perp_infos[0].oracle_price;
let trade_price = I80F48::from_num(price_factor) * oracle_price;
let base_lots = c
.max_perp_for_health_ratio(
0,
trade_price,
base_lot_size,
side,
I80F48::from_num(ratio),
)
.unwrap();
if base_lots == i64::MAX {
return (i64::MAX, f64::MAX, f64::MAX);
}
let direction = match side {
PerpOrderSide::Bid => 1,
PerpOrderSide::Ask => -1,
};
// compute the health ratio we'd get when executing the trade
let actual_ratio = {
let base_native = I80F48::from(direction * base_lots * base_lot_size);
let mut c = c.clone();
c.perp_infos[0].base += base_native * oracle_price;
c.perp_infos[0].quote -= base_native * trade_price;
c.health_ratio(HealthType::Init).to_num::<f64>()
};
// the ratio for trading just one base lot extra
let plus_ratio = {
let base_native = I80F48::from(direction * (base_lots + 1) * base_lot_size);
let mut c = c.clone();
c.perp_infos[0].base += base_native * oracle_price;
c.perp_infos[0].quote -= base_native * trade_price;
c.health_ratio(HealthType::Init).to_num::<f64>()
};
(base_lots, actual_ratio, plus_ratio)
};
let check_max_trade = |c: &HealthCache,
side: PerpOrderSide,
ratio: f64,
price_factor: f64| {
let (base_lots, actual_ratio, plus_ratio) =
find_max_trade(c, side, ratio, price_factor);
println!(
"checking for price_factor: {price_factor}, target ratio {ratio}: actual ratio: {actual_ratio}, plus ratio: {plus_ratio}, base_lots: {base_lots}",
);
let max_binary_search_error = 0.1;
assert!(ratio <= actual_ratio);
assert!(plus_ratio - max_binary_search_error <= ratio);
};
{
let mut health_cache = health_cache.clone();
adjust_token(&mut health_cache, 3000.0);
for existing in [-5, 0, 3] {
let mut c = health_cache.clone();
c.perp_infos[0].base += I80F48::from(existing * base_lot_size * 2);
c.perp_infos[0].quote -= I80F48::from(existing * base_lot_size * 2);
for side in [PerpOrderSide::Bid, PerpOrderSide::Ask] {
println!("test 0: existing {existing}, side {side:?}");
for price_factor in [0.8, 1.0, 1.1] {
for ratio in 1..=100 {
check_max_trade(&health_cache, side, ratio as f64, price_factor);
}
}
}
}
// check some extremely bad prices
check_max_trade(&health_cache, PerpOrderSide::Bid, 50.0, 2.0);
check_max_trade(&health_cache, PerpOrderSide::Ask, 50.0, 0.1);
// and extremely good prices
assert_eq!(
find_max_trade(&health_cache, PerpOrderSide::Bid, 50.0, 0.1).0,
i64::MAX
);
assert_eq!(
find_max_trade(&health_cache, PerpOrderSide::Ask, 50.0, 1.5).0,
i64::MAX
);
}
}
#[test]
fn test_health_perp_funding() {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();

View File

@ -24,6 +24,7 @@ use super::TokenIndex;
use super::FREE_ORDER_SLOT;
use super::{HealthCache, HealthType};
use super::{PerpPosition, Serum3Orders, TokenPosition};
use crate::logs::DeactivateTokenPositionLog;
use checked_math as cm;
type BorshVecLength = u32;
@ -72,10 +73,12 @@ pub struct MangoAccount {
pub padding: [u8; 1],
// (Display only)
// Cumulative (deposits - withdraws)
// using USD prices at the time of the deposit/withdraw
// in USD units with 6 decimals
pub net_deposits: i64,
// (Display only)
// Cumulative settles on perp positions
// TODO: unimplemented
pub net_settled: i64,
@ -616,8 +619,11 @@ impl<
indexed_position: I80F48::ZERO,
token_index,
in_use_count: 0,
cumulative_deposit_interest: 0.0,
cumulative_borrow_interest: 0.0,
previous_index: I80F48::ZERO,
padding: Default::default(),
reserved: [0; 40],
reserved: [0; 16],
};
}
Ok((v, raw_index, bank_index))
@ -632,6 +638,24 @@ impl<
self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX;
}
pub fn deactivate_token_position_and_log(
&mut self,
raw_index: usize,
mango_account_pubkey: Pubkey,
) {
let mango_group = self.fixed.deref_or_borrow().group;
let token_position = self.token_position_mut_by_raw_index(raw_index);
assert!(token_position.in_use_count == 0);
emit!(DeactivateTokenPositionLog {
mango_group: mango_group,
mango_account: mango_account_pubkey,
token_index: token_position.token_index,
cumulative_deposit_interest: token_position.cumulative_deposit_interest,
cumulative_borrow_interest: token_position.cumulative_borrow_interest,
});
self.token_position_mut_by_raw_index(raw_index).token_index = TokenIndex::MAX;
}
// get mut Serum3Orders at raw_index
pub fn serum3_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut Serum3Orders {
let offset = self.header().serum3_offset(raw_index);
@ -950,7 +974,7 @@ impl<
// expand dynamic components by first moving existing positions, and then setting new ones to defaults
// perp oo
if new_header.perp_oo_count() > old_header.perp_oo_count() {
if old_header.perp_oo_count() > 0 {
unsafe {
sol_memmove(
&mut dynamic[new_header.perp_oo_offset(0)],
@ -958,14 +982,14 @@ impl<
size_of::<PerpOpenOrder>() * old_header.perp_oo_count(),
);
}
for i in old_header.perp_oo_count..new_perp_oo_count {
*get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) =
PerpOpenOrder::default();
}
}
for i in old_header.perp_oo_count..new_perp_oo_count {
*get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) =
PerpOpenOrder::default();
}
// perp positions
if new_header.perp_count() > old_header.perp_count() {
if old_header.perp_count() > 0 {
unsafe {
sol_memmove(
&mut dynamic[new_header.perp_offset(0)],
@ -973,14 +997,13 @@ impl<
size_of::<PerpPosition>() * old_header.perp_count(),
);
}
for i in old_header.perp_count..new_perp_count {
*get_helper_mut(dynamic, new_header.perp_offset(i.into())) =
PerpPosition::default();
}
}
for i in old_header.perp_count..new_perp_count {
*get_helper_mut(dynamic, new_header.perp_offset(i.into())) = PerpPosition::default();
}
// serum3 positions
if new_header.serum3_count() > old_header.serum3_count() {
if old_header.serum3_count() > 0 {
unsafe {
sol_memmove(
&mut dynamic[new_header.serum3_offset(0)],
@ -988,14 +1011,13 @@ impl<
size_of::<Serum3Orders>() * old_header.serum3_count(),
);
}
for i in old_header.serum3_count..new_serum3_count {
*get_helper_mut(dynamic, new_header.serum3_offset(i.into())) =
Serum3Orders::default();
}
}
for i in old_header.serum3_count..new_serum3_count {
*get_helper_mut(dynamic, new_header.serum3_offset(i.into())) = Serum3Orders::default();
}
// token positions
if new_header.token_count() > old_header.token_count() {
if old_header.token_count() > 0 {
unsafe {
sol_memmove(
&mut dynamic[new_header.token_offset(0)],
@ -1003,10 +1025,9 @@ impl<
size_of::<TokenPosition>() * old_header.token_count(),
);
}
for i in old_header.token_count..new_token_count {
*get_helper_mut(dynamic, new_header.token_offset(i.into())) =
TokenPosition::default();
}
}
for i in old_header.token_count..new_token_count {
*get_helper_mut(dynamic, new_header.token_offset(i.into())) = TokenPosition::default();
}
// update the already-parsed header

View File

@ -32,13 +32,24 @@ pub struct TokenPosition {
pub padding: [u8; 5],
#[derivative(Debug = "ignore")]
pub reserved: [u8; 40],
pub reserved: [u8; 16],
// bookkeeping variable for onchain interest calculation
// either deposit_index or borrow_index at last indexed_position change
pub previous_index: I80F48,
// (Display only)
// Cumulative deposit interest in token native units
pub cumulative_deposit_interest: f32,
// (Display only)
// Cumulative borrow interest in token native units
pub cumulative_borrow_interest: f32,
}
unsafe impl bytemuck::Pod for TokenPosition {}
unsafe impl bytemuck::Zeroable for TokenPosition {}
const_assert_eq!(size_of::<TokenPosition>(), 24 + 40);
const_assert_eq!(size_of::<TokenPosition>(), 64);
const_assert_eq!(size_of::<TokenPosition>() % 8, 0);
impl Default for TokenPosition {
@ -47,8 +58,11 @@ impl Default for TokenPosition {
indexed_position: I80F48::ZERO,
token_index: TokenIndex::MAX,
in_use_count: 0,
cumulative_deposit_interest: 0.0,
cumulative_borrow_interest: 0.0,
previous_index: I80F48::ZERO,
padding: Default::default(),
reserved: [0; 40],
reserved: [0; 16],
}
}
}
@ -400,8 +414,7 @@ pub use account_seeds;
#[cfg(test)]
mod tests {
use crate::state::{OracleConfig, PerpMarket};
use anchor_lang::prelude::Pubkey;
use crate::state::PerpMarket;
use fixed::types::I80F48;
use rand::Rng;
@ -416,52 +429,9 @@ mod tests {
pos
}
fn create_perp_market() -> PerpMarket {
return PerpMarket {
group: Pubkey::new_unique(),
perp_market_index: 0,
group_insurance_fund: 0,
trusted_market: 0,
name: Default::default(),
oracle: Pubkey::new_unique(),
oracle_config: OracleConfig {
conf_filter: I80F48::ZERO,
},
bids: Pubkey::new_unique(),
asks: Pubkey::new_unique(),
event_queue: Pubkey::new_unique(),
quote_lot_size: 1,
base_lot_size: 1,
maint_asset_weight: I80F48::from(1),
init_asset_weight: I80F48::from(1),
maint_liab_weight: I80F48::from(1),
init_liab_weight: I80F48::from(1),
liquidation_fee: I80F48::ZERO,
maker_fee: I80F48::ZERO,
taker_fee: I80F48::ZERO,
min_funding: I80F48::ZERO,
max_funding: I80F48::ZERO,
impact_quantity: 0,
long_funding: I80F48::ZERO,
short_funding: I80F48::ZERO,
funding_last_updated: 0,
open_interest: 0,
seq_num: 0,
fees_accrued: I80F48::ZERO,
fees_settled: I80F48::ZERO,
bump: 0,
base_decimals: 0,
reserved: [0; 112],
padding0: Default::default(),
padding1: Default::default(),
padding2: Default::default(),
registration_time: 0,
};
}
#[test]
fn test_quote_entry_long_increasing_from_zero() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(0, 0, 0);
// Go long 10 @ 10
pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-100));
@ -472,7 +442,7 @@ mod tests {
#[test]
fn test_quote_entry_short_increasing_from_zero() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(0, 0, 0);
// Go short 10 @ 10
pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(100));
@ -483,7 +453,7 @@ mod tests {
#[test]
fn test_quote_entry_long_increasing_from_long() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(10, -100, -100);
// Go long 10 @ 30
pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-300));
@ -494,7 +464,7 @@ mod tests {
#[test]
fn test_quote_entry_short_increasing_from_short() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(-10, 100, 100);
// Go short 10 @ 10
pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(300));
@ -505,7 +475,7 @@ mod tests {
#[test]
fn test_quote_entry_long_decreasing_from_short() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(-10, 100, 100);
// Go long 5 @ 50
pos.change_base_and_quote_positions(&mut market, 5, I80F48::from(-250));
@ -516,7 +486,7 @@ mod tests {
#[test]
fn test_quote_entry_short_decreasing_from_long() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(10, -100, -100);
// Go short 5 @ 50
pos.change_base_and_quote_positions(&mut market, -5, I80F48::from(250));
@ -527,7 +497,7 @@ mod tests {
#[test]
fn test_quote_entry_long_close_with_short() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(10, -100, -100);
// Go short 10 @ 50
pos.change_base_and_quote_positions(&mut market, -10, I80F48::from(250));
@ -538,7 +508,7 @@ mod tests {
#[test]
fn test_quote_entry_short_close_with_long() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(-10, 100, 100);
// Go long 10 @ 50
pos.change_base_and_quote_positions(&mut market, 10, I80F48::from(-250));
@ -549,7 +519,7 @@ mod tests {
#[test]
fn test_quote_entry_long_close_short_with_overflow() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(10, -100, -100);
// Go short 15 @ 20
pos.change_base_and_quote_positions(&mut market, -15, I80F48::from(300));
@ -560,7 +530,7 @@ mod tests {
#[test]
fn test_quote_entry_short_close_long_with_overflow() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(-10, 100, 100);
// Go short 15 @ 20
pos.change_base_and_quote_positions(&mut market, 15, I80F48::from(-300));
@ -571,7 +541,7 @@ mod tests {
#[test]
fn test_quote_entry_break_even_price() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(0, 0, 0);
// Buy 11 @ 10,000
pos.change_base_and_quote_positions(&mut market, 11, I80F48::from(-11 * 10_000));
@ -585,7 +555,7 @@ mod tests {
#[test]
fn test_quote_entry_multiple_and_reversed_changes_return_entry_to_zero() {
let mut market = create_perp_market();
let mut market = PerpMarket::default_for_tests();
let mut pos = create_perp_position(0, 0, 0);
// Generate array of random trades

View File

@ -377,6 +377,11 @@ impl<'a> Book<'a> {
apply_fees(market, mango_account, total_quote_lots_taken)?;
}
// IOC orders have a fee penalty applied regardless of match
if order_type == OrderType::ImmediateOrCancel {
apply_penalty(market, mango_account)?;
}
Ok(())
}
@ -460,12 +465,24 @@ fn apply_fees(
// risks that fees_accrued is settled to 0 before they apply. It going negative
// breaks assumptions.
// The maker fees apply to the maker's account only when the fill event is consumed.
let maker_fees = taker_quote_native * market.maker_fee;
let maker_fees = cm!(taker_quote_native * market.maker_fee);
let taker_fees = cm!(taker_quote_native * market.taker_fee);
let taker_fees = taker_quote_native * market.taker_fee;
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
perp_account.change_quote_position(-taker_fees);
market.fees_accrued += taker_fees + maker_fees;
cm!(market.fees_accrued += taker_fees + maker_fees);
Ok(())
}
/// Applies a fixed penalty fee to the account, and update the market's fees_accrued
fn apply_penalty(market: &mut PerpMarket, mango_account: &mut MangoAccountRefMut) -> Result<()> {
let perp_account = mango_account.perp_position_mut(market.perp_market_index)?;
let fee_penalty = I80F48::from_num(market.fee_penalty);
perp_account.change_quote_position(-fee_penalty);
cm!(market.fees_accrued += fee_penalty);
Ok(())
}

View File

@ -15,9 +15,7 @@ pub mod queue;
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{
MangoAccount, MangoAccountValue, PerpMarket, FREE_ORDER_SLOT, QUOTE_TOKEN_INDEX,
};
use crate::state::{MangoAccount, MangoAccountValue, PerpMarket, FREE_ORDER_SLOT};
use anchor_lang::prelude::*;
use bytemuck::Zeroable;
use fixed::types::I80F48;
@ -100,13 +98,14 @@ mod tests {
bids: bids.borrow_mut(),
asks: asks.borrow_mut(),
};
let settle_token_index = 0;
let mut new_order =
|book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 {
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
account
.ensure_perp_position(perp_market.perp_market_index, QUOTE_TOKEN_INDEX)
.ensure_perp_position(perp_market.perp_market_index, settle_token_index)
.unwrap();
let quantity = 1;
@ -194,6 +193,7 @@ mod tests {
bids: bids.borrow_mut(),
asks: asks.borrow_mut(),
};
let settle_token_index = 0;
// Add lots and fees to make sure to exercise unit conversion
market.base_lot_size = 10;
@ -205,10 +205,10 @@ mod tests {
let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap();
let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap();
maker
.ensure_perp_position(market.perp_market_index, QUOTE_TOKEN_INDEX)
.ensure_perp_position(market.perp_market_index, settle_token_index)
.unwrap();
taker
.ensure_perp_position(market.perp_market_index, QUOTE_TOKEN_INDEX)
.ensure_perp_position(market.perp_market_index, settle_token_index)
.unwrap();
let maker_pk = Pubkey::new_unique();
@ -375,4 +375,111 @@ mod tests {
match_quote - match_quote * market.taker_fee
);
}
#[test]
fn test_fee_penalty_applied_only_on_limit_order() -> Result<()> {
let (mut market, oracle_price, mut event_queue, bids, asks) = test_setup(1000.0);
let mut book = Book {
bids: bids.borrow_mut(),
asks: asks.borrow_mut(),
};
let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap();
let mut account = MangoAccountValue::from_bytes(&buffer).unwrap();
let taker_pk = Pubkey::new_unique();
let now_ts = 1000000;
market.base_lot_size = 1;
market.quote_lot_size = 1;
market.taker_fee = I80F48::from_num(0.01);
market.fee_penalty = 5.0;
account.ensure_perp_position(market.perp_market_index, 0)?;
// Passive order
book.new_order(
Side::Ask,
&mut market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&taker_pk,
1000,
2,
i64::MAX,
OrderType::Limit,
0,
43,
now_ts,
u8::MAX,
)
.unwrap();
// Partial taker
book.new_order(
Side::Bid,
&mut market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&taker_pk,
1000,
1,
i64::MAX,
OrderType::Limit,
0,
43,
now_ts,
u8::MAX,
)
.unwrap();
let pos = account.perp_position(market.perp_market_index)?;
assert_eq!(
pos.quote_position_native().round(),
I80F48::from_num(-10),
"Regular fees applied on limit order"
);
assert_eq!(
market.fees_accrued.round(),
I80F48::from_num(10),
"Fees moved to market"
);
// Full taker
book.new_order(
Side::Bid,
&mut market,
&mut event_queue,
oracle_price,
&mut account.borrow_mut(),
&taker_pk,
1000,
1,
i64::MAX,
OrderType::ImmediateOrCancel,
0,
43,
now_ts,
u8::MAX,
)
.unwrap();
let pos = account.perp_position(market.perp_market_index)?;
assert_eq!(
pos.quote_position_native().round(),
I80F48::from_num(-25), // -10 - 5
"Regular fees + fixed penalty applied on IOC order"
);
assert_eq!(
market.fees_accrued.round(),
I80F48::from_num(25), // 10 + 5
"Fees moved to market"
);
Ok(())
}
}

View File

@ -6,8 +6,8 @@ use fixed::types::I80F48;
use static_assertions::const_assert_eq;
use crate::accounts_zerocopy::KeyedAccountReader;
use crate::state::oracle;
use crate::state::orderbook::order_type::Side;
use crate::state::{oracle, TokenIndex};
use crate::util::checked_math as cm;
use super::{Book, OracleConfig, DAY_I80F48};
@ -20,8 +20,7 @@ pub struct PerpMarket {
// ABI: Clients rely on this being at offset 8
pub group: Pubkey,
// ABI: Clients rely on this being at offset 40
pub padding0: [u8; 2],
pub settle_token_index: TokenIndex,
/// Lookup indices
pub perp_market_index: PerpMarketIndex,
@ -99,13 +98,20 @@ pub struct PerpMarket {
/// Fees settled in native quote currency
pub fees_settled: I80F48,
pub reserved: [u8; 112],
pub fee_penalty: f32,
/// In native units of settlement token, given to each settle call above the
/// settle_fee_amount_threshold.
pub settle_fee_flat: f32,
/// Pnl settlement amount needed to be eligible for fees.
pub settle_fee_amount_threshold: f32,
/// Fraction of pnl to pay out as fee if +pnl account has low health.
pub settle_fee_fraction_low_health: f32,
pub reserved: [u8; 92],
}
const_assert_eq!(
size_of::<PerpMarket>(),
32 + 2 + 2 + 4 + 16 + 32 + 16 + 32 * 3 + 8 * 2 + 16 * 12 + 8 * 2 + 8 * 2 + 16 + 2 + 6 + 8 + 112
);
const_assert_eq!(size_of::<PerpMarket>(), 584);
const_assert_eq!(size_of::<PerpMarket>() % 8, 0);
impl PerpMarket {
@ -123,6 +129,10 @@ impl PerpMarket {
self.group_insurance_fund = if v { 1 } else { 0 };
}
pub fn trusted_market(&self) -> bool {
self.trusted_market == 1
}
pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 {
self.seq_num += 1;
@ -225,4 +235,52 @@ impl PerpMarket {
self.short_funding += socialized_loss;
Ok(socialized_loss)
}
/// Creates default market for tests
pub fn default_for_tests() -> PerpMarket {
PerpMarket {
group: Pubkey::new_unique(),
settle_token_index: 0,
perp_market_index: 0,
name: Default::default(),
oracle: Pubkey::new_unique(),
oracle_config: OracleConfig {
conf_filter: I80F48::ZERO,
},
bids: Pubkey::new_unique(),
asks: Pubkey::new_unique(),
event_queue: Pubkey::new_unique(),
quote_lot_size: 1,
base_lot_size: 1,
maint_asset_weight: I80F48::from(1),
init_asset_weight: I80F48::from(1),
maint_liab_weight: I80F48::from(1),
init_liab_weight: I80F48::from(1),
liquidation_fee: I80F48::ZERO,
maker_fee: I80F48::ZERO,
taker_fee: I80F48::ZERO,
min_funding: I80F48::ZERO,
max_funding: I80F48::ZERO,
impact_quantity: 0,
long_funding: I80F48::ZERO,
short_funding: I80F48::ZERO,
funding_last_updated: 0,
open_interest: 0,
seq_num: 0,
fees_accrued: I80F48::ZERO,
fees_settled: I80F48::ZERO,
bump: 0,
base_decimals: 0,
reserved: [0; 92],
padding1: Default::default(),
padding2: Default::default(),
registration_time: 0,
fee_penalty: 0.0,
trusted_market: 0,
group_insurance_fund: 0,
settle_fee_flat: 0.0,
settle_fee_amount_threshold: 0.0,
settle_fee_fraction_low_health: 0.0,
}
}
}

View File

@ -564,6 +564,7 @@ pub struct TokenDepositInstruction {
pub amount: u64,
pub account: Pubkey,
pub owner: TestKeypair,
pub token_account: Pubkey,
pub token_authority: TestKeypair,
pub bank_index: usize,
@ -607,6 +608,76 @@ impl ClientInstruction for TokenDepositInstruction {
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
owner: self.owner.pubkey(),
bank: mint_info.banks[self.bank_index],
vault: mint_info.vaults[self.bank_index],
oracle: mint_info.oracle,
token_account: self.token_account,
token_authority: self.token_authority.pubkey(),
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas.into_iter());
(accounts, instruction)
}
fn signers(&self) -> Vec<TestKeypair> {
vec![self.token_authority, self.owner]
}
}
pub struct TokenDepositIntoExistingInstruction {
pub amount: u64,
pub account: Pubkey,
pub token_account: Pubkey,
pub token_authority: TestKeypair,
pub bank_index: usize,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for TokenDepositIntoExistingInstruction {
type Accounts = mango_v4::accounts::TokenDepositIntoExisting;
type Instruction = mango_v4::instruction::TokenDepositIntoExisting;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
amount: self.amount,
};
// load account so we know its mint
let token_account: TokenAccount = account_loader.load(&self.token_account).await.unwrap();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let mint_info = Pubkey::find_program_address(
&[
b"MintInfo".as_ref(),
account.fixed.group.as_ref(),
token_account.mint.as_ref(),
],
&program_id,
)
.0;
let mint_info: MintInfo = account_loader.load(&mint_info).await.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
Some(mint_info.banks[self.bank_index]),
false,
None,
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
@ -2130,6 +2201,7 @@ pub struct PerpCreateMarketInstruction {
pub bids: Pubkey,
pub event_queue: Pubkey,
pub payer: TestKeypair,
pub settle_token_index: TokenIndex,
pub perp_market_index: PerpMarketIndex,
pub base_decimals: u8,
pub quote_lot_size: i64,
@ -2143,6 +2215,10 @@ pub struct PerpCreateMarketInstruction {
pub taker_fee: f32,
pub group_insurance_fund: bool,
pub trusted_market: bool,
pub fee_penalty: f32,
pub settle_fee_flat: f32,
pub settle_fee_amount_threshold: f32,
pub settle_fee_fraction_low_health: f32,
}
impl PerpCreateMarketInstruction {
pub async fn with_new_book_and_queue(
@ -2179,6 +2255,7 @@ impl ClientInstruction for PerpCreateMarketInstruction {
oracle_config: OracleConfig {
conf_filter: I80F48::from_num::<f32>(0.10),
},
settle_token_index: self.settle_token_index,
perp_market_index: self.perp_market_index,
quote_lot_size: self.quote_lot_size,
base_lot_size: self.base_lot_size,
@ -2195,6 +2272,10 @@ impl ClientInstruction for PerpCreateMarketInstruction {
base_decimals: self.base_decimals,
group_insurance_fund: self.group_insurance_fund,
trusted_market: self.trusted_market,
fee_penalty: self.fee_penalty,
settle_fee_flat: self.settle_fee_flat,
settle_fee_amount_threshold: self.settle_fee_amount_threshold,
settle_fee_fraction_low_health: self.settle_fee_fraction_low_health,
};
let perp_market = Pubkey::find_program_address(
@ -2552,11 +2633,12 @@ impl ClientInstruction for PerpUpdateFundingInstruction {
}
pub struct PerpSettlePnlInstruction {
pub settler: Pubkey,
pub settler_owner: TestKeypair,
pub account_a: Pubkey,
pub account_b: Pubkey,
pub perp_market: Pubkey,
pub quote_bank: Pubkey,
pub max_settle_amount: u64,
pub settle_bank: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for PerpSettlePnlInstruction {
@ -2567,31 +2649,39 @@ impl ClientInstruction for PerpSettlePnlInstruction {
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
max_settle_amount: self.max_settle_amount,
};
let instruction = Self::Instruction {};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let settle_bank: Bank = account_loader.load(&self.settle_bank).await.unwrap();
let account_a = account_loader
.load_mango_account(&self.account_a)
.await
.unwrap();
let account_b = account_loader
.load_mango_account(&self.account_b)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
let health_check_metas = derive_liquidation_remaining_account_metas(
&account_loader,
&account_a,
&account_b,
None,
false,
Some(perp_market.perp_market_index),
TokenIndex::MAX,
0,
TokenIndex::MAX,
0,
)
.await;
let accounts = Self::Accounts {
group: perp_market.group,
settler: self.settler,
settler_owner: self.settler_owner.pubkey(),
perp_market: self.perp_market,
account_a: self.account_a,
account_b: self.account_b,
oracle: perp_market.oracle,
quote_bank: self.quote_bank,
settle_bank: self.settle_bank,
settle_oracle: settle_bank.oracle,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
@ -2601,14 +2691,14 @@ impl ClientInstruction for PerpSettlePnlInstruction {
}
fn signers(&self) -> Vec<TestKeypair> {
vec![]
vec![self.settler_owner]
}
}
pub struct PerpSettleFeesInstruction {
pub account: Pubkey,
pub perp_market: Pubkey,
pub quote_bank: Pubkey,
pub settle_bank: Pubkey,
pub max_settle_amount: u64,
}
#[async_trait::async_trait(?Send)]
@ -2625,6 +2715,7 @@ impl ClientInstruction for PerpSettleFeesInstruction {
};
let perp_market: PerpMarket = account_loader.load(&self.perp_market).await.unwrap();
let settle_bank: Bank = account_loader.load(&self.settle_bank).await.unwrap();
let account = account_loader
.load_mango_account(&self.account)
.await
@ -2643,7 +2734,8 @@ impl ClientInstruction for PerpSettleFeesInstruction {
perp_market: self.perp_market,
account: self.account,
oracle: perp_market.oracle,
quote_bank: self.quote_bank,
settle_bank: self.settle_bank,
settle_oracle: settle_bank.oracle,
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas);
@ -2822,8 +2914,9 @@ impl ClientInstruction for PerpLiqBankruptcyInstruction {
liqor: self.liqor,
liqor_owner: self.liqor_owner.pubkey(),
liqee: self.liqee,
quote_bank: quote_mint_info.first_bank(),
quote_vault: quote_mint_info.first_vault(),
settle_bank: quote_mint_info.first_bank(),
settle_vault: quote_mint_info.first_vault(),
settle_oracle: quote_mint_info.oracle,
insurance_vault: group.insurance_vault,
token_program: Token::id(),
};

View File

@ -178,6 +178,7 @@ pub async fn create_funded_account(
TokenDepositInstruction {
amount: amounts,
account,
owner,
token_account: payer.token_accounts[mint.index],
token_authority: payer.key,
bank_index,

View File

@ -59,6 +59,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account: vault_account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -94,6 +95,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit1_amount,
account,
owner,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
@ -106,6 +108,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit2_amount,
account,
owner,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,
@ -354,6 +357,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: vault_amount,
account: vault_account,
owner,
token_account,
token_authority: payer.clone(),
bank_index: 1,
@ -369,6 +373,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account: vault_account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -404,6 +409,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit1_amount,
account,
owner,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
@ -416,6 +422,7 @@ async fn test_bankrupt_tokens_insurance_fund() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit2_amount,
account,
owner,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -43,7 +43,7 @@ async fn test_basic() -> Result<(), TransportError> {
AccountCreateInstruction {
account_num: 0,
token_count: 8,
serum3_count: 0,
serum3_count: 7,
perp_count: 0,
perp_oo_count: 0,
group,
@ -54,6 +54,17 @@ async fn test_basic() -> Result<(), TransportError> {
.await
.unwrap()
.account;
let account_data: MangoAccount = solana.get_account(account).await;
assert_eq!(account_data.tokens.len(), 8);
assert_eq!(
account_data.tokens.iter().filter(|t| t.is_active()).count(),
0
);
assert_eq!(account_data.serum3.len(), 7);
assert_eq!(
account_data.serum3.iter().filter(|s| s.is_active()).count(),
0
);
send_tx(
solana,
@ -71,6 +82,17 @@ async fn test_basic() -> Result<(), TransportError> {
.await
.unwrap()
.account;
let account_data: MangoAccount = solana.get_account(account).await;
assert_eq!(account_data.tokens.len(), 16);
assert_eq!(
account_data.tokens.iter().filter(|t| t.is_active()).count(),
0
);
assert_eq!(account_data.serum3.len(), 8);
assert_eq!(
account_data.serum3.iter().filter(|s| s.is_active()).count(),
0
);
//
// TEST: Deposit funds
@ -84,6 +106,7 @@ async fn test_basic() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account,
owner,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,

View File

@ -145,6 +145,7 @@ async fn test_health_compute_serum() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -264,6 +265,7 @@ async fn test_health_compute_perp() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 10,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -63,6 +63,7 @@ async fn test_health_wrap() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 1,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -89,6 +90,7 @@ async fn test_health_wrap() -> Result<(), TransportError> {
tx.add_instruction(TokenDepositInstruction {
amount: repay_amount,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -94,6 +94,7 @@ async fn test_liq_perps_force_cancel() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 1,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
@ -249,6 +250,9 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
0,
)
.await;
let settler =
create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await;
let settler_owner = owner.clone();
//
// TEST: Create a perp market
@ -304,6 +308,7 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
TokenDepositInstruction {
amount: 1,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer,
bank_index: 0,
@ -512,18 +517,19 @@ async fn test_liq_perps_base_position_and_bankruptcy() -> Result<(), TransportEr
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: liqor,
account_b: account_1,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[0].bank,
},
)
.await
.unwrap();
let liqee_spot_health_before = 1000.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_spot_health_before;
let liqee_settle_health_before = 1000.0 + 1.0 * 2.0 * 0.8;
let remaining_pnl = 20.0 * 100.0 - liq_amount_2 + liqee_settle_health_before;
assert!(remaining_pnl < 0.0);
let liqee_data = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(liqee_data.perps[0].base_position_lots(), 0);

View File

@ -233,6 +233,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: 100000,
account: vault_account,
owner,
token_account,
token_authority: payer.clone(),
bank_index: 0,
@ -269,6 +270,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit1_amount,
account,
owner,
token_account: payer_mint_accounts[2],
token_authority: payer.clone(),
bank_index: 0,
@ -281,6 +283,7 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit2_amount,
account,
owner,
token_account: payer_mint_accounts[3],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -89,6 +89,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> {
TokenDepositInstruction {
amount: deposit_amount_initial,
account,
owner,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,

View File

@ -58,6 +58,9 @@ async fn test_perp() -> Result<(), TransportError> {
0,
)
.await;
let settler =
create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await;
let settler_owner = owner.clone();
//
// TEST: Create a perp market
@ -384,11 +387,12 @@ async fn test_perp() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[0].bank,
},
)
.await
@ -398,7 +402,7 @@ async fn test_perp() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market,
quote_bank: tokens[0].bank,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)

View File

@ -18,11 +18,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..=2];
let payer_mint_accounts = &context.users[1].token_accounts[0..=2];
let initial_token_deposit0 = 10_000;
// only deposited because perps currently require the base token position to be active
let initial_token_deposit1 = 1;
let initial_token_deposit = 10_000;
//
// SETUP: Create a group and an account
@ -37,98 +34,32 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
.create(solana)
.await;
let account_0 = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
let settler =
create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await;
let settler_owner = owner.clone();
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints[0..1],
initial_token_deposit,
0,
)
.await
.unwrap()
.account;
let account_1 = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
token_count: 16,
serum3_count: 8,
perp_count: 8,
perp_oo_count: 8,
group,
owner,
payer,
},
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..1],
initial_token_deposit,
0,
)
.await
.unwrap()
.account;
//
// SETUP: Deposit user funds
//
{
send_tx(
solana,
TokenDepositInstruction {
amount: initial_token_deposit0,
account: account_0,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: initial_token_deposit1,
account: account_0,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
{
send_tx(
solana,
TokenDepositInstruction {
amount: initial_token_deposit0,
account: account_1,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: initial_token_deposit1,
account: account_1,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
}
.await;
//
// TEST: Create a perp market
@ -264,11 +195,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_1,
account_b: account_0,
perp_market,
quote_bank: tokens[1].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[1].bank,
},
)
.await;
@ -283,11 +215,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_0,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[0].bank,
},
)
.await;
@ -302,11 +235,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market: perp_market_2,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[0].bank,
},
)
.await;
@ -317,25 +251,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
"Cannot settle a position that does not exist".to_string(),
);
// max_settle_amount must be greater than zero
let result = send_tx(
solana,
PerpSettlePnlInstruction {
account_a: account_0,
account_b: account_1,
perp_market: perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: 0,
},
)
.await;
assert_mango_error(
&result,
MangoError::MaxSettleAmountMustBeGreaterThanZero.into(),
"max_settle_amount must be greater than zero".to_string(),
);
// TODO: Test funding settlement
{
@ -344,12 +259,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
initial_token_deposit0,
initial_token_deposit,
"account 0 has expected amount of tokens"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
initial_token_deposit0,
initial_token_deposit,
"account 1 has expected amount of tokens"
);
}
@ -372,11 +287,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
let result = send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_1,
account_b: account_0,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[0].bank,
},
)
.await;
@ -418,70 +334,6 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
);
}
// Partially execute the settle
let partial_settle_amount = 200;
send_tx(
solana,
PerpSettlePnlInstruction {
account_a: account_0,
account_b: account_1,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: partial_settle_amount,
},
)
.await
.unwrap();
{
let bank = solana.get_account::<Bank>(tokens[0].bank).await;
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].base_position_lots(),
1,
"base position unchanged for account 0"
);
assert_eq!(
mango_account_1.perps[0].base_position_lots(),
-1,
"base position unchanged for account 1"
);
assert_eq!(
mango_account_0.perps[0].quote_position_native().round(),
I80F48::from(-100_020) - I80F48::from(partial_settle_amount),
"quote position reduced for profitable position by max_settle_amount"
);
assert_eq!(
mango_account_1.perps[0].quote_position_native().round(),
I80F48::from(100_000 + partial_settle_amount),
"quote position increased for losing position by opposite of first account"
);
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit0 + partial_settle_amount),
"account 0 token native position increased (profit) by max_settle_amount"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit0) - I80F48::from(partial_settle_amount),
"account 1 token native position decreased (loss) by max_settle_amount"
);
assert_eq!(
mango_account_0.net_settled, partial_settle_amount as i64,
"net_settled on account 0 updated with profit from settlement"
);
assert_eq!(
mango_account_1.net_settled,
-(partial_settle_amount as i64),
"net_settled on account 1 updated with loss from settlement"
);
}
// Change the oracle to a very high price, such that the pnl exceeds the account funding
send_tx(
solana,
@ -496,8 +348,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
.await
.unwrap();
let expected_pnl_0 = I80F48::from(50000 - 20 - partial_settle_amount as i64);
let expected_pnl_1 = I80F48::from(-50000 + partial_settle_amount as i64);
let expected_pnl_0 = I80F48::from(50000 - 20);
let expected_pnl_1 = I80F48::from(-50000);
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
@ -514,16 +366,17 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
}
// Settle as much PNL as account_1's health allows
let account_1_health_non_perp = I80F48::from(9040); // 0.8 * (10000-200+1*1500)
let expected_total_settle = I80F48::from(partial_settle_amount) + account_1_health_non_perp;
let account_1_health_non_perp = I80F48::from_num(0.8 * 10000.0);
let expected_total_settle = account_1_health_non_perp;
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[0].bank,
},
)
.await
@ -558,12 +411,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit0) + expected_total_settle,
I80F48::from(initial_token_deposit) + expected_total_settle,
"account 0 token native position increased (profit)"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit0) - expected_total_settle,
I80F48::from(initial_token_deposit) - expected_total_settle,
"account 1 token native position decreased (loss)"
);
@ -591,8 +444,8 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
.await
.unwrap();
let expected_pnl_0 = I80F48::from(-9760);
let expected_pnl_1 = I80F48::from(9740);
let expected_pnl_0 = I80F48::from(-8520);
let expected_pnl_1 = I80F48::from(8500);
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
@ -613,11 +466,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_1,
account_b: account_0,
perp_market,
quote_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
settle_bank: tokens[0].bank,
},
)
.await
@ -653,12 +507,12 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
// 480 was previous settlement
assert_eq!(
mango_account_0.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit0) + expected_total_settle,
I80F48::from(initial_token_deposit) + expected_total_settle,
"account 0 token native position decreased (loss)"
);
assert_eq!(
mango_account_1.tokens[0].native(&bank).round(),
I80F48::from(initial_token_deposit0) - expected_total_settle,
I80F48::from(initial_token_deposit) - expected_total_settle,
"account 1 token native position increased (profit)"
);
@ -689,3 +543,331 @@ async fn test_perp_settle_pnl() -> Result<(), TransportError> {
Ok(())
}
#[tokio::test]
async fn test_perp_settle_pnl_fees() -> Result<(), TransportError> {
let context = TestContext::new().await;
let solana = &context.solana.clone();
let admin = TestKeypair::new();
let owner = context.users[0].key;
let payer = context.users[1].key;
let mints = &context.mints[0..=2];
let initial_token_deposit = 10_000;
//
// SETUP: Create a group and accounts
//
let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig {
admin,
payer,
mints: mints.to_vec(),
zero_token_is_quote: true,
..GroupWithTokensConfig::default()
}
.create(solana)
.await;
let settle_bank = tokens[0].bank;
// ensure vaults are not empty
create_funded_account(
&solana,
group,
owner,
250,
&context.users[1],
mints,
100_000,
0,
)
.await;
let settler =
create_funded_account(&solana, group, owner, 251, &context.users[1], &[], 0, 0).await;
let settler_owner = owner.clone();
let account_0 = create_funded_account(
&solana,
group,
owner,
0,
&context.users[1],
&mints[0..1],
initial_token_deposit,
0,
)
.await;
let account_1 = create_funded_account(
&solana,
group,
owner,
1,
&context.users[1],
&mints[0..1],
initial_token_deposit,
0,
)
.await;
//
// SETUP: Create a perp market
//
let flat_fee = 1000;
let fee_low_health = 0.05;
let mango_v4::accounts::PerpCreateMarket { perp_market, .. } = send_tx(
solana,
PerpCreateMarketInstruction {
group,
admin,
payer,
perp_market_index: 0,
quote_lot_size: 10,
base_lot_size: 100,
maint_asset_weight: 1.0,
init_asset_weight: 1.0,
maint_liab_weight: 1.0,
init_liab_weight: 1.0,
liquidation_fee: 0.0,
maker_fee: 0.0,
taker_fee: 0.0,
settle_fee_flat: flat_fee as f32,
settle_fee_amount_threshold: 2000.0,
settle_fee_fraction_low_health: fee_low_health,
..PerpCreateMarketInstruction::with_new_book_and_queue(&solana, &tokens[1]).await
},
)
.await
.unwrap();
let price_lots = {
let perp_market = solana.get_account::<PerpMarket>(perp_market).await;
perp_market.native_price_to_lot(I80F48::from(1000))
};
// Set the initial oracle price
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[1].pubkey,
payer,
price: "1000.0",
},
)
.await
.unwrap();
//
// SETUP: Create a perp base position
//
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_0,
perp_market,
owner,
side: Side::Bid,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpPlaceOrderInstruction {
account: account_1,
perp_market,
owner,
side: Side::Ask,
price_lots,
max_base_lots: 1,
max_quote_lots: i64::MAX,
client_order_id: 0,
},
)
.await
.unwrap();
send_tx(
solana,
PerpConsumeEventsInstruction {
perp_market,
mango_accounts: vec![account_0, account_1],
},
)
.await
.unwrap();
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(mango_account_0.perps[0].base_position_lots(), 1);
assert_eq!(mango_account_1.perps[0].base_position_lots(), -1);
assert_eq!(
mango_account_0.perps[0].quote_position_native().round(),
-100_000
);
assert_eq!(mango_account_1.perps[0].quote_position_native(), 100_000);
}
//
// TEST: Settle (health is high)
//
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[1].pubkey,
payer,
price: "1050.0",
},
)
.await
.unwrap();
let expected_pnl = 5000;
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank,
},
)
.await
.unwrap();
let mut total_settled_pnl = expected_pnl;
let mut total_fees_paid = flat_fee;
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].quote_position_native().round(),
I80F48::from(-100_000 - total_settled_pnl)
);
assert_eq!(
mango_account_1.perps[0].quote_position_native().round(),
I80F48::from(100_000 + total_settled_pnl),
);
assert_eq!(
account_position(solana, account_0, settle_bank).await,
initial_token_deposit as i64 + total_settled_pnl - total_fees_paid
);
assert_eq!(
account_position(solana, account_1, settle_bank).await,
initial_token_deposit as i64 - total_settled_pnl
);
assert_eq!(
account_position(solana, settler, settle_bank).await,
total_fees_paid
);
}
//
// Bring account_0 health low, specifically to
// init_health = 14000 - 1.4 * 1 * 10700 = -980
// maint_health = 14000 - 1.2 * 1 * 10700 = 1160
//
send_tx(
solana,
TokenWithdrawInstruction {
account: account_0,
owner,
token_account: context.users[1].token_accounts[2],
amount: 1,
allow_borrow: true,
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[2].pubkey,
payer,
price: "10700.0",
},
)
.await
.unwrap();
//
// TEST: Settle (health is low)
//
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: mints[1].pubkey,
payer,
price: "1100.0",
},
)
.await
.unwrap();
let expected_pnl = 5000;
send_tx(
solana,
PerpSettlePnlInstruction {
settler,
settler_owner,
account_a: account_0,
account_b: account_1,
perp_market,
settle_bank,
},
)
.await
.unwrap();
total_settled_pnl += expected_pnl;
total_fees_paid += flat_fee
+ (expected_pnl as f64 * fee_low_health as f64 * 980.0 / (1160.0 + 980.0)) as i64
+ 1;
{
let mango_account_0 = solana.get_account::<MangoAccount>(account_0).await;
let mango_account_1 = solana.get_account::<MangoAccount>(account_1).await;
assert_eq!(
mango_account_0.perps[0].quote_position_native().round(),
I80F48::from(-100_000 - total_settled_pnl)
);
assert_eq!(
mango_account_1.perps[0].quote_position_native().round(),
I80F48::from(100_000 + total_settled_pnl),
);
assert_eq!(
account_position(solana, account_0, settle_bank).await,
initial_token_deposit as i64 + total_settled_pnl - total_fees_paid
);
assert_eq!(
account_position(solana, account_1, settle_bank).await,
initial_token_deposit as i64 - total_settled_pnl
);
assert_eq!(
account_position(solana, settler, settle_bank).await,
total_fees_paid
);
}
Ok(())
}

View File

@ -80,6 +80,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -93,6 +94,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_0,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
@ -110,6 +112,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -123,6 +126,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
TokenDepositInstruction {
amount: deposit_amount,
account: account_1,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,
@ -266,7 +270,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_0,
perp_market,
quote_bank: tokens[1].bank,
settle_bank: tokens[1].bank,
max_settle_amount: u64::MAX,
},
)
@ -284,7 +288,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market: perp_market_2,
quote_bank: tokens[0].bank,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
@ -302,7 +306,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market: perp_market,
quote_bank: tokens[0].bank,
settle_bank: tokens[0].bank,
max_settle_amount: 0,
},
)
@ -350,7 +354,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_0,
perp_market,
quote_bank: tokens[0].bank,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)
@ -370,7 +374,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
// account: account_1,
// perp_market,
// oracle: tokens[0].oracle,
// quote_bank: tokens[0].bank,
// settle_bank: tokens[0].bank,
// max_settle_amount: I80F48::MAX,
// },
// )
@ -434,7 +438,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market,
quote_bank: tokens[0].bank,
settle_bank: tokens[0].bank,
max_settle_amount: partial_settle_amount,
},
)
@ -488,7 +492,7 @@ async fn test_perp_settle_fees() -> Result<(), TransportError> {
PerpSettleFeesInstruction {
account: account_1,
perp_market,
quote_bank: tokens[0].bank,
settle_bank: tokens[0].bank,
max_settle_amount: u64::MAX,
},
)

View File

@ -71,14 +71,30 @@ async fn test_position_lifetime() -> Result<()> {
{
let start_balance = solana.token_account_balance(payer_mint_accounts[0]).await;
// this activates the positions
let deposit_amount = 100;
// cannot deposit_into_existing if no token deposit exists
assert!(send_tx(
solana,
TokenDepositIntoExistingInstruction {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
bank_index: 0,
}
)
.await
.is_err());
// this activates the positions
for &payer_token in payer_mint_accounts {
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount,
account,
owner,
token_account: payer_token,
token_authority: payer.clone(),
bank_index: 0,
@ -88,6 +104,20 @@ async fn test_position_lifetime() -> Result<()> {
.unwrap();
}
// now depositing into an active account works
send_tx(
solana,
TokenDepositIntoExistingInstruction {
amount: deposit_amount,
account,
token_account: payer_mint_accounts[0],
token_authority: payer,
bank_index: 0,
},
)
.await
.unwrap();
// this closes the positions
for &payer_token in payer_mint_accounts {
send_tx(
@ -131,6 +161,7 @@ async fn test_position_lifetime() -> Result<()> {
TokenDepositInstruction {
amount: collateral_amount,
account,
owner,
token_account: payer_mint_accounts[0],
token_authority: payer.clone(),
bank_index: 0,
@ -167,6 +198,7 @@ async fn test_position_lifetime() -> Result<()> {
// deposit withdraw amount + some more to cover loan origination fees
amount: borrow_amount + 2,
account,
owner,
token_account: payer_mint_accounts[1],
token_authority: payer.clone(),
bank_index: 0,

View File

@ -1,16 +1,27 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { nativeI80F48ToUi } from '../utils';
import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48';
import { I80F48, I80F48Dto, ZERO_I80F48 } from '../numbers/I80F48';
import { As, toUiDecimals } from '../utils';
export const QUOTE_DECIMALS = 6;
export type TokenIndex = number & As<'token-index'>;
export type OracleConfig = {
confFilter: I80F48Dto;
};
export class Bank {
export interface BankForHealth {
tokenIndex: TokenIndex;
maintAssetWeight: I80F48;
initAssetWeight: I80F48;
maintLiabWeight: I80F48;
initLiabWeight: I80F48;
price: I80F48;
}
export class Bank implements BankForHealth {
public name: string;
public depositIndex: I80F48;
public borrowIndex: I80F48;
@ -25,8 +36,8 @@ export class Bank {
public rate1: I80F48;
public util0: I80F48;
public util1: I80F48;
public price: I80F48 | undefined;
public uiPrice: number | undefined;
public _price: I80F48 | undefined;
public _uiPrice: number | undefined;
public collectedFeesNative: I80F48;
public loanFeeRate: I80F48;
public loanOriginationFeeRate: I80F48;
@ -76,7 +87,7 @@ export class Bank {
mintDecimals: number;
bankNum: number;
},
) {
): Bank {
return new Bank(
publicKey,
obj.name,
@ -111,7 +122,7 @@ export class Bank {
obj.dust,
obj.flashLoanTokenAccountInitial,
obj.flashLoanApprovedAmount,
obj.tokenIndex,
obj.tokenIndex as TokenIndex,
obj.mintDecimals,
obj.bankNum,
);
@ -151,7 +162,7 @@ export class Bank {
dust: I80F48Dto,
flashLoanTokenAccountInitial: BN,
flashLoanApprovedAmount: BN,
public tokenIndex: number,
public tokenIndex: TokenIndex,
public mintDecimals: number,
public bankNum: number,
) {
@ -178,8 +189,8 @@ export class Bank {
this.initLiabWeight = I80F48.from(initLiabWeight);
this.liquidationFee = I80F48.from(liquidationFee);
this.dust = I80F48.from(dust);
this.price = undefined;
this.uiPrice = undefined;
this._price = undefined;
this._uiPrice = undefined;
}
toString(): string {
@ -198,64 +209,82 @@ export class Bank {
'\n oracle - ' +
this.oracle.toBase58() +
'\n price - ' +
this.price?.toNumber() +
this._price?.toString() +
'\n uiPrice - ' +
this.uiPrice +
this._uiPrice +
'\n deposit index - ' +
this.depositIndex.toNumber() +
this.depositIndex.toString() +
'\n borrow index - ' +
this.borrowIndex.toNumber() +
this.borrowIndex.toString() +
'\n indexedDeposits - ' +
this.indexedDeposits.toNumber() +
this.indexedDeposits.toString() +
'\n indexedBorrows - ' +
this.indexedBorrows.toNumber() +
this.indexedBorrows.toString() +
'\n cachedIndexedTotalDeposits - ' +
this.cachedIndexedTotalDeposits.toNumber() +
this.cachedIndexedTotalDeposits.toString() +
'\n cachedIndexedTotalBorrows - ' +
this.cachedIndexedTotalBorrows.toNumber() +
this.cachedIndexedTotalBorrows.toString() +
'\n indexLastUpdated - ' +
new Date(this.indexLastUpdated.toNumber() * 1000) +
'\n bankRateLastUpdated - ' +
new Date(this.bankRateLastUpdated.toNumber() * 1000) +
'\n avgUtilization - ' +
this.avgUtilization.toNumber() +
this.avgUtilization.toString() +
'\n adjustmentFactor - ' +
this.adjustmentFactor.toNumber() +
this.adjustmentFactor.toString() +
'\n maxRate - ' +
this.maxRate.toNumber() +
this.maxRate.toString() +
'\n util0 - ' +
this.util0.toNumber() +
this.util0.toString() +
'\n rate0 - ' +
this.rate0.toNumber() +
this.rate0.toString() +
'\n util1 - ' +
this.util1.toNumber() +
this.util1.toString() +
'\n rate1 - ' +
this.rate1.toNumber() +
this.rate1.toString() +
'\n loanFeeRate - ' +
this.loanFeeRate.toNumber() +
this.loanFeeRate.toString() +
'\n loanOriginationFeeRate - ' +
this.loanOriginationFeeRate.toNumber() +
this.loanOriginationFeeRate.toString() +
'\n maintAssetWeight - ' +
this.maintAssetWeight.toNumber() +
this.maintAssetWeight.toString() +
'\n initAssetWeight - ' +
this.initAssetWeight.toNumber() +
this.initAssetWeight.toString() +
'\n maintLiabWeight - ' +
this.maintLiabWeight.toNumber() +
this.maintLiabWeight.toString() +
'\n initLiabWeight - ' +
this.initLiabWeight.toNumber() +
this.initLiabWeight.toString() +
'\n liquidationFee - ' +
this.liquidationFee.toNumber() +
this.liquidationFee.toString() +
'\n uiDeposits() - ' +
this.uiDeposits() +
'\n uiBorrows() - ' +
this.uiBorrows() +
'\n getDepositRate() - ' +
this.getDepositRate().toNumber() +
this.getDepositRate().toString() +
'\n getBorrowRate() - ' +
this.getBorrowRate().toNumber()
this.getBorrowRate().toString()
);
}
get price(): I80F48 {
if (!this._price) {
throw new Error(
`Undefined price for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`,
);
}
return this._price;
}
get uiPrice(): number {
if (!this._uiPrice) {
throw new Error(
`Undefined uiPrice for bank ${this.publicKey} with tokenIndex ${this.tokenIndex}!`,
);
}
return this._uiPrice;
}
nativeDeposits(): I80F48 {
return this.indexedDeposits.mul(this.depositIndex);
}
@ -265,17 +294,17 @@ export class Bank {
}
uiDeposits(): number {
return nativeI80F48ToUi(
return toUiDecimals(
this.indexedDeposits.mul(this.depositIndex),
this.mintDecimals,
).toNumber();
);
}
uiBorrows(): number {
return nativeI80F48ToUi(
return toUiDecimals(
this.indexedBorrows.mul(this.borrowIndex),
this.mintDecimals,
).toNumber();
);
}
/**
@ -361,11 +390,11 @@ export class MintInfo {
registrationTime: BN;
groupInsuranceFund: number;
},
) {
): MintInfo {
return new MintInfo(
publicKey,
obj.group,
obj.tokenIndex,
obj.tokenIndex as TokenIndex,
obj.mint,
obj.banks,
obj.vaults,
@ -378,7 +407,7 @@ export class MintInfo {
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public tokenIndex: number,
public tokenIndex: TokenIndex,
public mint: PublicKey,
public banks: PublicKey[],
public vaults: PublicKey[],

View File

@ -6,22 +6,26 @@ import {
Market,
Orderbook,
} from '@project-serum/serum';
import { parsePriceData, PriceData } from '@pythnetwork/client';
import { AccountInfo, PublicKey } from '@solana/web3.js';
import { parsePriceData } from '@pythnetwork/client';
import {
AccountInfo,
AddressLookupTableAccount,
PublicKey,
} from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { Id } from '../ids';
import { toNativeDecimals, toUiDecimals } from '../utils';
import { Bank, MintInfo } from './bank';
import { I80F48, ONE_I80F48 } from './I80F48';
import { I80F48, ONE_I80F48 } from '../numbers/I80F48';
import { toNative, toNativeI80F48, toUiDecimals } from '../utils';
import { Bank, MintInfo, TokenIndex } from './bank';
import {
isPythOracle,
isSwitchboardOracle,
parseSwitchboardOracle,
} from './oracle';
import { BookSide, PerpMarket } from './perp';
import { Serum3Market } from './serum3';
import { BookSide, PerpMarket, PerpMarketIndex } from './perp';
import { MarketIndex, Serum3Market } from './serum3';
export class Group {
static from(
@ -35,6 +39,7 @@ export class Group {
insuranceVault: PublicKey;
testing: number;
version: number;
addressLookupTables: PublicKey[];
},
): Group {
return new Group(
@ -47,15 +52,19 @@ export class Group {
obj.insuranceVault,
obj.testing,
obj.version,
obj.addressLookupTables,
[], // addressLookupTablesList
new Map(), // banksMapByName
new Map(), // banksMapByMint
new Map(), // banksMapByTokenIndex
new Map(), // serum3MarketsMap
new Map(), // serum3MarketsMapByExternal
new Map(), // serum3MarketsMapByMarketIndex
new Map(), // serum3MarketExternalsMap
new Map(), // perpMarketsMap
new Map(), // perpMarketsMapByOracle
new Map(), // perpMarketsMapByMarketIndex
new Map(), // perpMarketsMapByName
new Map(), // mintInfosMapByTokenIndex
new Map(), // mintInfosMapByMint
new Map(), // oraclesMap
new Map(), // vaultAmountsMap
);
}
@ -70,20 +79,23 @@ export class Group {
public insuranceVault: PublicKey,
public testing: number,
public version: number,
public addressLookupTables: PublicKey[],
public addressLookupTablesList: AddressLookupTableAccount[],
public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: Map<string, Bank[]>,
public banksMapByTokenIndex: Map<number, Bank[]>,
public banksMapByTokenIndex: Map<TokenIndex, Bank[]>,
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
public serum3MarketExternalsMap: Map<string, Market>,
// TODO rethink key
public perpMarketsMap: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<number, MintInfo>,
public serum3MarketsMapByMarketIndex: Map<MarketIndex, Serum3Market>,
public serum3ExternalMarketsMap: Map<string, Market>,
public perpMarketsMapByOracle: Map<string, PerpMarket>,
public perpMarketsMapByMarketIndex: Map<PerpMarketIndex, PerpMarket>,
public perpMarketsMapByName: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<TokenIndex, MintInfo>,
public mintInfosMapByMint: Map<string, MintInfo>,
private oraclesMap: Map<string, PriceData>, // UNUSED
public vaultAmountsMap: Map<string, number>,
public vaultAmountsMap: Map<string, BN>,
) {}
public async reloadAll(client: MangoClient) {
public async reloadAll(client: MangoClient): Promise<void> {
let ids: Id | undefined = undefined;
if (client.idsSource === 'api') {
@ -96,15 +108,16 @@ export class Group {
// console.time('group.reload');
await Promise.all([
this.reloadAlts(client),
this.reloadBanks(client, ids).then(() =>
Promise.all([
this.reloadBankOraclePrices(client),
this.reloadVaults(client, ids),
this.reloadVaults(client),
]),
),
this.reloadMintInfos(client, ids),
this.reloadSerum3Markets(client, ids).then(() =>
this.reloadSerum3ExternalMarkets(client, ids),
this.reloadSerum3ExternalMarkets(client),
),
this.reloadPerpMarkets(client, ids).then(() =>
this.reloadPerpMarketOraclePrices(client),
@ -113,7 +126,23 @@ export class Group {
// console.timeEnd('group.reload');
}
public async reloadBanks(client: MangoClient, ids?: Id) {
public async reloadAlts(client: MangoClient): Promise<void> {
const alts = await Promise.all(
this.addressLookupTables
.filter((alt) => !alt.equals(PublicKey.default))
.map((alt) =>
client.program.provider.connection.getAddressLookupTable(alt),
),
);
this.addressLookupTablesList = alts.map((res, i) => {
if (!res || !res.value) {
throw new Error(`Undefined ALT ${this.addressLookupTables[i]}!`);
}
return res.value;
});
}
public async reloadBanks(client: MangoClient, ids?: Id): Promise<void> {
let banks: Bank[];
if (ids && ids.getBanks().length) {
@ -143,7 +172,7 @@ export class Group {
}
}
public async reloadMintInfos(client: MangoClient, ids?: Id) {
public async reloadMintInfos(client: MangoClient, ids?: Id): Promise<void> {
let mintInfos: MintInfo[];
if (ids && ids.getMintInfos().length) {
mintInfos = (
@ -168,7 +197,10 @@ export class Group {
);
}
public async reloadSerum3Markets(client: MangoClient, ids?: Id) {
public async reloadSerum3Markets(
client: MangoClient,
ids?: Id,
): Promise<void> {
let serum3Markets: Serum3Market[];
if (ids && ids.getSerum3Markets().length) {
serum3Markets = (
@ -188,9 +220,15 @@ export class Group {
serum3Market,
]),
);
this.serum3MarketsMapByMarketIndex = new Map(
serum3Markets.map((serum3Market) => [
serum3Market.marketIndex,
serum3Market,
]),
);
}
public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) {
public async reloadSerum3ExternalMarkets(client: MangoClient): Promise<void> {
const externalMarkets = await Promise.all(
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
Market.load(
@ -202,7 +240,7 @@ export class Group {
),
);
this.serum3MarketExternalsMap = new Map(
this.serum3ExternalMarketsMap = new Map(
Array.from(this.serum3MarketsMapByExternal.values()).map(
(serum3Market, index) => [
serum3Market.serumMarketExternal.toBase58(),
@ -212,7 +250,7 @@ export class Group {
);
}
public async reloadPerpMarkets(client: MangoClient, ids?: Id) {
public async reloadPerpMarkets(client: MangoClient, ids?: Id): Promise<void> {
let perpMarkets: PerpMarket[];
if (ids && ids.getPerpMarkets().length) {
perpMarkets = (
@ -226,9 +264,18 @@ export class Group {
perpMarkets = await client.perpGetMarkets(this);
}
this.perpMarketsMap = new Map(
this.perpMarketsMapByName = new Map(
perpMarkets.map((perpMarket) => [perpMarket.name, perpMarket]),
);
this.perpMarketsMapByOracle = new Map(
perpMarkets.map((perpMarket) => [
perpMarket.oracle.toBase58(),
perpMarket,
]),
);
this.perpMarketsMapByMarketIndex = new Map(
perpMarkets.map((perpMarket) => [perpMarket.perpMarketIndex, perpMarket]),
);
}
public async reloadBankOraclePrices(client: MangoClient): Promise<void> {
@ -244,8 +291,8 @@ export class Group {
for (const [index, ai] of ais.entries()) {
for (const bank of banks[index]) {
if (bank.name === 'USDC') {
bank.price = ONE_I80F48();
bank.uiPrice = 1;
bank._price = ONE_I80F48();
bank._uiPrice = 1;
} else {
if (!ai)
throw new Error(
@ -256,10 +303,9 @@ export class Group {
bank.oracle,
ai,
this.getMintDecimals(bank.mint),
this.getMintDecimals(this.insuranceMint),
);
bank.price = price;
bank.uiPrice = uiPrice;
bank._price = price;
bank._uiPrice = uiPrice;
}
}
}
@ -268,7 +314,9 @@ export class Group {
public async reloadPerpMarketOraclePrices(
client: MangoClient,
): Promise<void> {
const perpMarkets: PerpMarket[] = Array.from(this.perpMarketsMap.values());
const perpMarkets: PerpMarket[] = Array.from(
this.perpMarketsMapByName.values(),
);
const oracles = perpMarkets.map((b) => b.oracle);
const ais =
await client.program.provider.connection.getMultipleAccountsInfo(oracles);
@ -277,16 +325,17 @@ export class Group {
ais.forEach(async (ai, i) => {
const perpMarket = perpMarkets[i];
if (!ai)
throw new Error('Undefined ai object in reloadPerpMarketOraclePrices!');
throw new Error(
`Undefined ai object in reloadPerpMarketOraclePrices for ${perpMarket.oracle}!`,
);
const { price, uiPrice } = await this.decodePriceFromOracleAi(
coder,
perpMarket.oracle,
ai,
perpMarket.baseDecimals,
this.getMintDecimals(this.insuranceMint),
);
perpMarket.price = price;
perpMarket.uiPrice = uiPrice;
perpMarket._price = price;
perpMarket._uiPrice = uiPrice;
});
}
@ -295,8 +344,7 @@ export class Group {
oracle: PublicKey,
ai: AccountInfo<Buffer>,
baseDecimals: number,
quoteDecimals: number,
) {
): Promise<{ price: I80F48; uiPrice: number }> {
let price, uiPrice;
if (
!BorshAccountsCoder.accountDiscriminator('stubOracle').compare(
@ -305,22 +353,22 @@ export class Group {
) {
const stubOracle = coder.decode('stubOracle', ai.data);
price = new I80F48(stubOracle.price.val);
uiPrice = this?.toUiPrice(price, baseDecimals, quoteDecimals);
uiPrice = this?.toUiPrice(price, baseDecimals);
} else if (isPythOracle(ai)) {
uiPrice = parsePriceData(ai.data).previousPrice;
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
price = this?.toNativePrice(uiPrice, baseDecimals);
} else if (isSwitchboardOracle(ai)) {
uiPrice = await parseSwitchboardOracle(ai);
price = this?.toNativePrice(uiPrice, baseDecimals, quoteDecimals);
price = this?.toNativePrice(uiPrice, baseDecimals);
} else {
throw new Error(
`Unknown oracle provider for oracle ${oracle}, with owner ${ai.owner}`,
`Unknown oracle provider (parsing not implemented) for oracle ${oracle}, with owner ${ai.owner}!`,
);
}
return { price, uiPrice };
}
public async reloadVaults(client: MangoClient, ids?: Id): Promise<void> {
public async reloadVaults(client: MangoClient): Promise<void> {
const vaultPks = Array.from(this.banksMapByMint.values())
.flat()
.map((bank) => bank.vault);
@ -331,10 +379,13 @@ export class Group {
this.vaultAmountsMap = new Map(
vaultAccounts.map((vaultAi, i) => {
if (!vaultAi) throw new Error('Missing vault account info');
const vaultAmount = coder()
.accounts.decode('token', vaultAi.data)
.amount.toNumber();
if (!vaultAi) {
throw new Error(`Undefined vaultAi for ${vaultPks[i]}`!);
}
const vaultAmount = coder().accounts.decode(
'token',
vaultAi.data,
).amount;
return [vaultPks[i].toBase58(), vaultAmount];
}),
);
@ -342,135 +393,185 @@ export class Group {
public getMintDecimals(mintPk: PublicKey): number {
const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks)
throw new Error(`Unable to find mint decimals for ${mintPk.toString()}`);
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
return banks[0].mintDecimals;
}
public getInsuranceMintDecimals(): number {
return this.getMintDecimals(this.insuranceMint);
}
public getFirstBankByMint(mintPk: PublicKey): Bank {
const banks = this.banksMapByMint.get(mintPk.toString());
if (!banks) throw new Error(`Unable to find bank for ${mintPk.toString()}`);
if (!banks) throw new Error(`No bank found for mint ${mintPk}!`);
return banks[0];
}
public getFirstBankByTokenIndex(tokenIndex: number): Bank {
public getFirstBankByTokenIndex(tokenIndex: TokenIndex): Bank {
const banks = this.banksMapByTokenIndex.get(tokenIndex);
if (!banks)
throw new Error(`Unable to find banks for tokenIndex ${tokenIndex}`);
if (!banks) throw new Error(`No bank found for tokenIndex ${tokenIndex}!`);
return banks[0];
}
/**
*
* @param mintPk
* @returns sum of native balances of vaults for all banks for a token (fetched from vaultAmountsMap cache)
*/
public getTokenVaultBalanceByMint(mintPk: PublicKey): I80F48 {
const banks = this.banksMapByMint.get(mintPk.toBase58());
if (!banks)
throw new Error(
`Mint does not exist in getTokenVaultBalanceByMint ${mintPk.toString()}`,
);
let totalAmount = 0;
for (const bank of banks) {
const amount = this.vaultAmountsMap.get(bank.vault.toBase58());
if (amount) {
totalAmount += amount;
}
}
return I80F48.fromNumber(totalAmount);
}
public getSerum3MarketByPk(pk: PublicKey): Serum3Market | undefined {
return Array.from(this.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.serumMarketExternal.equals(pk),
);
}
public getSerum3MarketByIndex(marketIndex: number): Serum3Market | undefined {
return Array.from(this.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.marketIndex === marketIndex,
);
}
public getSerum3MarketByName(name: string): Serum3Market | undefined {
return Array.from(this.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.name === name,
);
}
public async loadSerum3BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return await serum3Market.loadBids(client, this);
}
public async loadSerum3AsksForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
if (!serum3Market) {
throw new Error(
`Unable to find mint serum3Market for ${externalMarketPk.toString()}`,
);
}
return await serum3Market.loadAsks(client, this);
}
public getFeeRate(maker = true) {
// TODO: fetch msrm/srm vault balance
const feeTier = getFeeTier(0, 0);
const rates = getFeeRates(feeTier);
return maker ? rates.maker : rates.taker;
}
public async loadPerpBidsForMarket(
client: MangoClient,
marketName: string,
): Promise<BookSide> {
const perpMarket = this.perpMarketsMap.get(marketName);
if (!perpMarket) {
throw new Error(`Perp Market ${marketName} not found!`);
}
return await perpMarket.loadBids(client);
}
public async loadPerpAsksForMarket(
client: MangoClient,
marketName: string,
): Promise<BookSide> {
const perpMarket = this.perpMarketsMap.get(marketName);
if (!perpMarket) {
throw new Error(`Perp Market ${marketName} not found!`);
}
return await perpMarket.loadAsks(client);
}
/**
*
* @param mintPk
* @returns sum of ui balances of vaults for all banks for a token
*/
public getTokenVaultBalanceByMintUi(mintPk: PublicKey): number {
const vaultBalance = this.getTokenVaultBalanceByMint(mintPk);
const mintDecimals = this.getMintDecimals(mintPk);
const banks = this.banksMapByMint.get(mintPk.toBase58());
if (!banks) {
throw new Error(`No bank found for mint ${mintPk}!`);
}
const totalAmount = new BN(0);
for (const bank of banks) {
const amount = this.vaultAmountsMap.get(bank.vault.toBase58());
if (!amount) {
throw new Error(
`Vault balance not found for bank ${bank.name} ${bank.bankNum}!`,
);
}
totalAmount.iadd(amount);
}
return toUiDecimals(vaultBalance, mintDecimals);
return toUiDecimals(totalAmount, this.getMintDecimals(mintPk));
}
public consoleLogBanks() {
public getSerum3MarketByMarketIndex(marketIndex: MarketIndex): Serum3Market {
const serum3Market = this.serum3MarketsMapByMarketIndex.get(marketIndex);
if (!serum3Market) {
throw new Error(`No serum3Market found for marketIndex ${marketIndex}!`);
}
return serum3Market;
}
public getSerum3MarketByName(name: string): Serum3Market {
const serum3Market = Array.from(
this.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.name === name);
if (!serum3Market) {
throw new Error(`No serum3Market found by name ${name}!`);
}
return serum3Market;
}
public getSerum3MarketByPk(pk: PublicKey): Serum3Market | undefined {
const serum3Market = Array.from(
this.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.serumMarketExternal.equals(pk));
if (!serum3Market) {
throw new Error(`No serum3Market found by public key ${pk}!`);
}
return serum3Market;
}
public getSerum3MarketByExternalMarket(
externalMarketPk: PublicKey,
): Serum3Market {
const serum3Market = Array.from(
this.serum3MarketsMapByExternal.values(),
).find((serum3Market) =>
serum3Market.serumMarketExternal.equals(externalMarketPk),
);
if (!serum3Market) {
throw new Error(
`No serum3Market found for external serum3 market ${externalMarketPk.toString()}!`,
);
}
return serum3Market;
}
public getSerum3ExternalMarket(externalMarketPk: PublicKey): Market {
const market = this.serum3ExternalMarketsMap.get(
externalMarketPk.toBase58(),
);
if (!market) {
throw new Error(
`No external market found for pk ${externalMarketPk.toString()}!`,
);
}
return market;
}
public async loadSerum3BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk);
return await serum3Market.loadBids(client, this);
}
public async loadSerum3AsksForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
const serum3Market = this.getSerum3MarketByExternalMarket(externalMarketPk);
return await serum3Market.loadAsks(client, this);
}
public getSerum3FeeRates(maker = true): number {
// TODO: fetch msrm/srm vault balance
const feeTier = getFeeTier(0, 0);
const rates = getFeeRates(feeTier);
return maker ? rates.maker : rates.taker;
}
public findPerpMarket(marketIndex: PerpMarketIndex): PerpMarket {
const perpMarket = Array.from(this.perpMarketsMapByName.values()).find(
(perpMarket) => perpMarket.perpMarketIndex === marketIndex,
);
if (!perpMarket) {
throw new Error(
`No perpMarket found for perpMarketIndex ${marketIndex}!`,
);
}
return perpMarket;
}
public getPerpMarketByOracle(oracle: PublicKey): PerpMarket {
const perpMarket = this.perpMarketsMapByOracle.get(oracle.toBase58());
if (!perpMarket) {
throw new Error(`No PerpMarket found for oracle ${oracle}!`);
}
return perpMarket;
}
public getPerpMarketByMarketIndex(marketIndex: PerpMarketIndex): PerpMarket {
const perpMarket = this.perpMarketsMapByMarketIndex.get(marketIndex);
if (!perpMarket) {
throw new Error(`No PerpMarket found with marketIndex ${marketIndex}!`);
}
return perpMarket;
}
public getPerpMarketByName(perpMarketName: string): PerpMarket {
const perpMarket = Array.from(
this.perpMarketsMapByMarketIndex.values(),
).find((perpMarket) => perpMarket.name === perpMarketName);
if (!perpMarket) {
throw new Error(`No PerpMarket found by name ${perpMarketName}!`);
}
return perpMarket;
}
public async loadPerpBidsForMarket(
client: MangoClient,
perpMarketIndex: PerpMarketIndex,
): Promise<BookSide> {
const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex);
return await perpMarket.loadBids(client);
}
public async loadPerpAsksForMarket(
client: MangoClient,
group: Group,
perpMarketIndex: PerpMarketIndex,
): Promise<BookSide> {
const perpMarket = this.getPerpMarketByMarketIndex(perpMarketIndex);
return await perpMarket.loadAsks(client);
}
public consoleLogBanks(): void {
for (const mintBanks of this.banksMapByMint.values()) {
for (const bank of mintBanks) {
console.log(bank.toString());
@ -478,29 +579,22 @@ export class Group {
}
}
public toUiPrice(
price: I80F48,
baseDecimals: number,
quoteDecimals: number,
): number {
return price
.mul(I80F48.fromNumber(Math.pow(10, baseDecimals - quoteDecimals)))
.toNumber();
public toUiPrice(price: I80F48, baseDecimals: number): number {
return toUiDecimals(price, baseDecimals - this.getInsuranceMintDecimals());
}
public toNativePrice(
uiPrice: number,
baseDecimals: number,
quoteDecimals: number,
): I80F48 {
return I80F48.fromNumber(uiPrice).mul(
I80F48.fromNumber(Math.pow(10, quoteDecimals - baseDecimals)),
public toNativePrice(uiPrice: number, baseDecimals: number): I80F48 {
return toNativeI80F48(
uiPrice,
// note: our oracles are quoted in USD and our insurance mint is USD
// please update when these assumptions change
this.getInsuranceMintDecimals() - baseDecimals,
);
}
public toNativeDecimals(uiAmount: number, mintPk: PublicKey): BN {
const decimals = this.getMintDecimals(mintPk);
return toNativeDecimals(uiAmount, decimals);
return toNative(uiAmount, decimals);
}
toString(): string {

View File

@ -0,0 +1,435 @@
import { BN } from '@project-serum/anchor';
import { OpenOrders } from '@project-serum/serum';
import { expect } from 'chai';
import { I80F48, ZERO_I80F48 } from '../numbers/I80F48';
import { toUiDecimalsForQuote } from '../utils';
import { BankForHealth, TokenIndex } from './bank';
import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache';
import { HealthType, PerpPosition } from './mangoAccount';
import { PerpMarket } from './perp';
import { MarketIndex } from './serum3';
function mockBankAndOracle(
tokenIndex: TokenIndex,
maintWeight: number,
initWeight: number,
price: number,
): BankForHealth {
return {
tokenIndex,
maintAssetWeight: I80F48.fromNumber(1 - maintWeight),
initAssetWeight: I80F48.fromNumber(1 - initWeight),
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price: I80F48.fromNumber(price),
};
}
function mockPerpMarket(
perpMarketIndex: number,
maintWeight: number,
initWeight: number,
price: I80F48,
): PerpMarket {
return {
perpMarketIndex,
maintAssetWeight: I80F48.fromNumber(1 - maintWeight),
initAssetWeight: I80F48.fromNumber(1 - initWeight),
maintLiabWeight: I80F48.fromNumber(1 + maintWeight),
initLiabWeight: I80F48.fromNumber(1 + initWeight),
price,
quoteLotSize: new BN(100),
baseLotSize: new BN(10),
longFunding: ZERO_I80F48(),
shortFunding: ZERO_I80F48(),
} as unknown as PerpMarket;
}
describe('Health Cache', () => {
it('test_health0', () => {
const sourceBank: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0.1,
0.2,
1,
);
const targetBank: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
);
const ti1 = TokenInfo.fromBank(sourceBank, I80F48.fromNumber(100));
const ti2 = TokenInfo.fromBank(targetBank, I80F48.fromNumber(-10));
const si1 = Serum3Info.fromOoModifyingTokenInfos(
1,
ti2,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(21),
baseTokenTotal: new BN(18),
quoteTokenFree: new BN(1),
baseTokenFree: new BN(3),
referrerRebatesAccrued: new BN(2),
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, targetBank.price);
const pp = new PerpPosition(
pM.perpMarketIndex,
new BN(3),
I80F48.fromNumber(-310),
new BN(7),
new BN(11),
new BN(1),
new BN(2),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
const hc = new HealthCache([ti1, ti2], [si1], [pi1]);
// for bank1/oracle1, including open orders (scenario: bids execute)
const health1 = (100.0 + 1.0 + 2.0 + (20.0 + 15.0 * 5.0)) * 0.8;
// for bank2/oracle2
const health2 = (-10.0 + 3.0) * 5.0 * 1.5;
// for perp (scenario: bids execute)
const health3 =
(3.0 + 7.0 + 1.0) * 10.0 * 5.0 * 0.8 +
(-310.0 + 2.0 * 100.0 - 7.0 * 10.0 * 5.0);
const health = hc.health(HealthType.init).toNumber();
console.log(
`health ${health
.toFixed(3)
.padStart(
10,
)}, case "test that includes all the side values (like referrer_rebates_accrued)"`,
);
expect(health - (health1 + health2 + health3)).lessThan(0.0000001);
});
it('test_health1', () => {
function testFixture(fixture: {
name: string;
token1: number;
token2: number;
token3: number;
oo12: [number, number];
oo13: [number, number];
perp1: [number, number, number, number];
expectedHealth: number;
}): void {
const bank1: BankForHealth = mockBankAndOracle(
1 as TokenIndex,
0.1,
0.2,
1,
);
const bank2: BankForHealth = mockBankAndOracle(
4 as TokenIndex,
0.3,
0.5,
5,
);
const bank3: BankForHealth = mockBankAndOracle(
5 as TokenIndex,
0.3,
0.5,
10,
);
const ti1 = TokenInfo.fromBank(bank1, I80F48.fromNumber(fixture.token1));
const ti2 = TokenInfo.fromBank(bank2, I80F48.fromNumber(fixture.token2));
const ti3 = TokenInfo.fromBank(bank3, I80F48.fromNumber(fixture.token3));
const si1 = Serum3Info.fromOoModifyingTokenInfos(
1,
ti2,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(fixture.oo12[0]),
baseTokenTotal: new BN(fixture.oo12[1]),
quoteTokenFree: new BN(0),
baseTokenFree: new BN(0),
referrerRebatesAccrued: new BN(0),
} as any as OpenOrders,
);
const si2 = Serum3Info.fromOoModifyingTokenInfos(
2,
ti3,
0,
ti1,
2 as MarketIndex,
{
quoteTokenTotal: new BN(fixture.oo13[0]),
baseTokenTotal: new BN(fixture.oo13[1]),
quoteTokenFree: new BN(0),
baseTokenFree: new BN(0),
referrerRebatesAccrued: new BN(0),
} as any as OpenOrders,
);
const pM = mockPerpMarket(9, 0.1, 0.2, bank2.price);
const pp = new PerpPosition(
pM.perpMarketIndex,
new BN(fixture.perp1[0]),
I80F48.fromNumber(fixture.perp1[1]),
new BN(fixture.perp1[2]),
new BN(fixture.perp1[3]),
new BN(0),
new BN(0),
I80F48.fromNumber(0),
I80F48.fromNumber(0),
);
const pi1 = PerpInfo.fromPerpPosition(pM, pp);
const hc = new HealthCache([ti1, ti2, ti3], [si1, si2], [pi1]);
const health = hc.health(HealthType.init).toNumber();
console.log(
`health ${health.toFixed(3).padStart(10)}, case "${fixture.name}"`,
);
expect(health - fixture.expectedHealth).lessThan(0.0000001);
}
const basePrice = 5;
const baseLotsToQuote = 10.0 * basePrice;
testFixture({
name: '0',
token1: 100,
token2: -10,
token3: 0,
oo12: [20, 15],
oo13: [0, 0],
perp1: [3, -131, 7, 11],
expectedHealth:
// for token1, including open orders (scenario: bids execute)
(100.0 + (20.0 + 15.0 * basePrice)) * 0.8 -
// for token2
10.0 * basePrice * 1.5 +
// for perp (scenario: bids execute)
(3.0 + 7.0) * baseLotsToQuote * 0.8 +
(-131.0 - 7.0 * baseLotsToQuote),
});
testFixture({
name: '1',
token1: -100,
token2: 10,
token3: 0,
oo12: [20, 15],
oo13: [0, 0],
perp1: [-10, -131, 7, 11],
expectedHealth:
// for token1
-100.0 * 1.2 +
// for token2, including open orders (scenario: asks execute)
(10.0 * basePrice + (20.0 + 15.0 * basePrice)) * 0.5 +
// for perp (scenario: asks execute)
(-10.0 - 11.0) * baseLotsToQuote * 1.2 +
(-131.0 + 11.0 * baseLotsToQuote),
});
testFixture({
name: '2',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [-10, 100, 0, 0],
expectedHealth: 0,
});
testFixture({
name: '3',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [1, -100, 0, 0],
expectedHealth: -100.0 + 0.8 * 1.0 * baseLotsToQuote,
});
testFixture({
name: '4',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [10, 100, 0, 0],
expectedHealth: 0,
});
testFixture({
name: '5',
token1: 0,
token2: 0,
token3: 0,
oo12: [0, 0],
oo13: [0, 0],
perp1: [30, -100, 0, 0],
expectedHealth: 0,
});
testFixture({
name: '6, reserved oo funds',
token1: -100,
token2: -10,
token3: -10,
oo12: [1, 1],
oo13: [1, 1],
perp1: [30, -100, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
10.0 * 5.0 * 1.5 -
10.0 * 10.0 * 1.5 +
// oo_1_2 (-> token1)
(1.0 + 5.0) * 1.2 +
// oo_1_3 (-> token1)
(1.0 + 10.0) * 1.2,
});
testFixture({
name: '7, reserved oo funds cross the zero balance level',
token1: -14,
token2: -10,
token3: -10,
oo12: [1, 1],
oo13: [1, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
-14.0 * 1.2 -
10.0 * 5.0 * 1.5 -
10.0 * 10.0 * 1.5 +
// oo_1_2 (-> token1)
3.0 * 1.2 +
3.0 * 0.8 +
// oo_1_3 (-> token1)
8.0 * 1.2 +
3.0 * 0.8,
});
testFixture({
name: '8, reserved oo funds in a non-quote currency',
token1: -100,
token2: -100,
token3: -1,
oo12: [0, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
100.0 * 5.0 * 1.5 -
10.0 * 1.5 +
// oo_1_3 (-> token3)
10.0 * 1.5 +
10.0 * 0.5,
});
testFixture({
name: '9, like 8 but oo_1_2 flips the oo_1_3 target',
token1: -100,
token2: -100,
token3: -1,
oo12: [100, 0],
oo13: [10, 1],
perp1: [0, 0, 0, 0],
expectedHealth:
// tokens
-100.0 * 1.2 -
100.0 * 5.0 * 1.5 -
10.0 * 1.5 +
// oo_1_2 (-> token1)
80.0 * 1.2 +
20.0 * 0.8 +
// oo_1_3 (-> token1)
20.0 * 0.8,
});
});
it('max swap tokens for min ratio', () => {
// USDC like
const sourceBank: BankForHealth = {
tokenIndex: 0 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(1),
initAssetWeight: I80F48.fromNumber(1),
maintLiabWeight: I80F48.fromNumber(1),
initLiabWeight: I80F48.fromNumber(1),
price: I80F48.fromNumber(1),
};
// BTC like
const targetBank: BankForHealth = {
tokenIndex: 1 as TokenIndex,
maintAssetWeight: I80F48.fromNumber(0.9),
initAssetWeight: I80F48.fromNumber(0.8),
maintLiabWeight: I80F48.fromNumber(1.1),
initLiabWeight: I80F48.fromNumber(1.2),
price: I80F48.fromNumber(20000),
};
const hc = new HealthCache(
[
new TokenInfo(
0 as TokenIndex,
sourceBank.maintAssetWeight,
sourceBank.initAssetWeight,
sourceBank.maintLiabWeight,
sourceBank.initLiabWeight,
sourceBank.price!,
I80F48.fromNumber(-18 * Math.pow(10, 6)),
ZERO_I80F48(),
),
new TokenInfo(
1 as TokenIndex,
targetBank.maintAssetWeight,
targetBank.initAssetWeight,
targetBank.maintLiabWeight,
targetBank.initLiabWeight,
targetBank.price!,
I80F48.fromNumber(51 * Math.pow(10, 6)),
ZERO_I80F48(),
),
],
[],
[],
);
expect(
toUiDecimalsForQuote(
hc.getMaxSourceForTokenSwap(
targetBank,
sourceBank,
I80F48.fromNumber(1),
I80F48.fromNumber(0.95),
),
).toFixed(3),
).equals('0.008');
expect(
toUiDecimalsForQuote(
hc.getMaxSourceForTokenSwap(
sourceBank,
targetBank,
I80F48.fromNumber(1),
I80F48.fromNumber(0.95),
),
).toFixed(3),
).equals('90.176');
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import {
SwitchboardDecimal,
} from '@switchboard-xyz/switchboard-v2';
import BN from 'bn.js';
import { I80F48, I80F48Dto } from './I80F48';
import { I80F48, I80F48Dto } from '../numbers/I80F48';
const SBV1_DEVNET_PID = new PublicKey(
'7azgmy1pFXHikv36q1zZASvFq5vFa39TT9NweVugKKTU',
@ -20,7 +20,7 @@ let sbv2MainnetProgram;
export class StubOracle {
public price: I80F48;
public lastUpdated: number;
public lastUpdated: BN;
static from(
publicKey: PublicKey,
@ -48,7 +48,7 @@ export class StubOracle {
lastUpdated: BN,
) {
this.price = I80F48.from(price);
this.lastUpdated = lastUpdated.toNumber();
this.lastUpdated = lastUpdated;
}
}
@ -102,7 +102,7 @@ export async function parseSwitchboardOracle(
return parseSwitcboardOracleV1(accountInfo);
}
throw new Error(`Unable to parse switchboard oracle ${accountInfo.owner}`);
throw new Error(`Should not be reached!`);
}
export function isSwitchboardOracle(accountInfo: AccountInfo<Buffer>): boolean {

View File

@ -3,9 +3,11 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import Big from 'big.js';
import { MangoClient } from '../client';
import { U64_MAX_BN } from '../utils';
import { I80F48, I80F48Dto } from '../numbers/I80F48';
import { As, toNative, U64_MAX_BN } from '../utils';
import { OracleConfig, QUOTE_DECIMALS } from './bank';
import { I80F48, I80F48Dto } from './I80F48';
export type PerpMarketIndex = number & As<'perp-market-index'>;
export class PerpMarket {
public name: string;
@ -18,21 +20,23 @@ export class PerpMarket {
public takerFee: I80F48;
public minFunding: I80F48;
public maxFunding: I80F48;
public openInterest: number;
public seqNum: number;
public longFunding: I80F48;
public shortFunding: I80F48;
public openInterest: BN;
public seqNum: BN;
public feesAccrued: I80F48;
priceLotsToUiConverter: number;
baseLotsToUiConverter: number;
quoteLotsToUiConverter: number;
public price: number;
public uiPrice: number;
public _price: I80F48;
public _uiPrice: number;
static from(
publicKey: PublicKey,
obj: {
group: PublicKey;
quoteTokenIndex: number;
perpMarketIndex: number;
trustedMarket: number;
name: number[];
oracle: PublicKey;
oracleConfig: OracleConfig;
@ -55,7 +59,7 @@ export class PerpMarket {
shortFunding: I80F48Dto;
fundingLastUpdated: BN;
openInterest: BN;
seqNum: any; // TODO: ts complains that this is unknown for whatever reason
seqNum: BN;
feesAccrued: I80F48Dto;
bump: number;
baseDecimals: number;
@ -65,8 +69,8 @@ export class PerpMarket {
return new PerpMarket(
publicKey,
obj.group,
obj.quoteTokenIndex,
obj.perpMarketIndex,
obj.perpMarketIndex as PerpMarketIndex,
obj.trustedMarket == 1,
obj.name,
obj.oracle,
obj.oracleConfig,
@ -100,8 +104,8 @@ export class PerpMarket {
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public quoteTokenIndex: number,
public perpMarketIndex: number,
public perpMarketIndex: PerpMarketIndex, // TODO rename to marketIndex?
public trustedMarket: boolean,
name: number[],
public oracle: PublicKey,
oracleConfig: OracleConfig,
@ -140,8 +144,10 @@ export class PerpMarket {
this.takerFee = I80F48.from(takerFee);
this.minFunding = I80F48.from(minFunding);
this.maxFunding = I80F48.from(maxFunding);
this.openInterest = openInterest.toNumber();
this.seqNum = seqNum.toNumber();
this.longFunding = I80F48.from(longFunding);
this.shortFunding = I80F48.from(shortFunding);
this.openInterest = openInterest;
this.seqNum = seqNum;
this.feesAccrued = I80F48.from(feesAccrued);
this.priceLotsToUiConverter = new Big(10)
@ -159,6 +165,23 @@ export class PerpMarket {
.toNumber();
}
get price(): I80F48 {
if (!this._price) {
throw new Error(
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
);
}
return this._price;
}
get uiPrice(): number {
if (!this._uiPrice) {
throw new Error(
`Undefined price for perpMarket ${this.publicKey} with marketIndex ${this.perpMarketIndex}!`,
);
}
return this._uiPrice;
}
public async loadAsks(client: MangoClient): Promise<BookSide> {
const asks = await client.program.account.bookSide.fetch(this.asks);
return BookSide.from(client, this, BookSideType.asks, asks);
@ -176,26 +199,48 @@ export class PerpMarket {
return new PerpEventQueue(client, eventQueue.header, eventQueue.buf);
}
public async loadFills(client: MangoClient, lastSeqNum: BN) {
public async loadFills(
client: MangoClient,
lastSeqNum: BN,
): Promise<(OutEvent | FillEvent | LiquidateEvent)[]> {
const eventQueue = await this.loadEventQueue(client);
return eventQueue
.eventsSince(lastSeqNum)
.filter((event) => event.eventType == PerpEventQueue.FILL_EVENT_TYPE);
}
public async logOb(client: MangoClient): Promise<string> {
let res = ``;
res += ` ${this.name} OrderBook`;
let orders = await this?.loadAsks(client);
for (const order of orders!.items()) {
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
.toString()
.padStart(10)}`;
}
res += `\n asks ↑ --------- ↓ bids`;
orders = await this?.loadBids(client);
for (const order of orders!.items()) {
res += `\n ${order.uiPrice.toFixed(5).padStart(10)}, ${order.uiSize
.toString()
.padStart(10)}`;
}
return res;
}
/**
*
* @param bids
* @param asks
* @returns returns funding rate per hour
*/
public getCurrentFundingRate(bids: BookSide, asks: BookSide) {
public getCurrentFundingRate(bids: BookSide, asks: BookSide): number {
const MIN_FUNDING = this.minFunding.toNumber();
const MAX_FUNDING = this.maxFunding.toNumber();
const bid = bids.getImpactPriceUi(new BN(this.impactQuantity));
const ask = asks.getImpactPriceUi(new BN(this.impactQuantity));
const indexPrice = this.uiPrice;
const indexPrice = this._uiPrice;
let funding;
if (bid !== undefined && ask !== undefined) {
@ -215,21 +260,17 @@ export class PerpMarket {
}
public uiPriceToLots(price: number): BN {
return new BN(price * Math.pow(10, QUOTE_DECIMALS))
return toNative(price, QUOTE_DECIMALS)
.mul(this.baseLotSize)
.div(this.quoteLotSize.mul(new BN(Math.pow(10, this.baseDecimals))));
}
public uiBaseToLots(quantity: number): BN {
return new BN(quantity * Math.pow(10, this.baseDecimals)).div(
this.baseLotSize,
);
return toNative(quantity, this.baseDecimals).div(this.baseLotSize);
}
public uiQuoteToLots(uiQuote: number): BN {
return new BN(uiQuote * Math.pow(10, QUOTE_DECIMALS)).div(
this.quoteLotSize,
);
return toNative(uiQuote, QUOTE_DECIMALS).div(this.quoteLotSize);
}
public priceLotsToUi(price: BN): number {
@ -250,19 +291,19 @@ export class PerpMarket {
'\n perpMarketIndex -' +
this.perpMarketIndex +
'\n maintAssetWeight -' +
this.maintAssetWeight.toNumber() +
this.maintAssetWeight.toString() +
'\n initAssetWeight -' +
this.initAssetWeight.toNumber() +
this.initAssetWeight.toString() +
'\n maintLiabWeight -' +
this.maintLiabWeight.toNumber() +
this.maintLiabWeight.toString() +
'\n initLiabWeight -' +
this.initLiabWeight.toNumber() +
this.initLiabWeight.toString() +
'\n liquidationFee -' +
this.liquidationFee.toNumber() +
this.liquidationFee.toString() +
'\n makerFee -' +
this.makerFee.toNumber() +
this.makerFee.toString() +
'\n takerFee -' +
this.takerFee.toNumber()
this.takerFee.toString()
);
}
}
@ -284,7 +325,7 @@ export class BookSide {
leafCount: number;
nodes: unknown;
},
) {
): BookSide {
return new BookSide(
client,
perpMarket,
@ -311,7 +352,6 @@ export class BookSide {
public includeExpired = false,
maxBookDelay?: number,
) {
// TODO why? Ask Daffy
// Determine the maxTimestamp found on the book to use for tif
// If maxBookDelay is not provided, use 3600 as a very large number
maxBookDelay = maxBookDelay === undefined ? 3600 : maxBookDelay;
@ -329,7 +369,7 @@ export class BookSide {
this.now = maxTimestamp;
}
static getPriceFromKey(key: BN) {
static getPriceFromKey(key: BN): BN {
return key.ushrn(64);
}
@ -359,12 +399,16 @@ export class BookSide {
}
}
public best(): PerpOrder | undefined {
return this.items().next().value;
}
getImpactPriceUi(baseLots: BN): number | undefined {
const s = new BN(0);
for (const order of this.items()) {
s.iadd(order.sizeLots);
if (s.gte(baseLots)) {
return order.price;
return order.uiPrice;
}
}
return undefined;
@ -391,7 +435,7 @@ export class BookSide {
public getL2Ui(depth: number): [number, number][] {
const levels: [number, number][] = [];
for (const { price, size } of this.items()) {
for (const { uiPrice: price, uiSize: size } of this.items()) {
if (levels.length > 0 && levels[levels.length - 1][0] === price) {
levels[levels.length - 1][1] += size;
} else if (levels.length === depth) {
@ -456,29 +500,34 @@ export class LeafNode {
) {}
}
export class InnerNode {
static from(obj: { children: [number] }) {
static from(obj: { children: [number] }): InnerNode {
return new InnerNode(obj.children);
}
constructor(public children: [number]) {}
}
export class Side {
export class PerpOrderSide {
static bid = { bid: {} };
static ask = { ask: {} };
}
export class PerpOrderType {
static limit = { limit: {} };
static immediateOrCancel = { immediateorcancel: {} };
static postOnly = { postonly: {} };
static immediateOrCancel = { immediateOrCancel: {} };
static postOnly = { postOnly: {} };
static market = { market: {} };
static postOnlySlide = { postonlyslide: {} };
static postOnlySlide = { postOnlySlide: {} };
}
export class PerpOrder {
static from(perpMarket: PerpMarket, leafNode: LeafNode, type: BookSideType) {
const side = type == BookSideType.bids ? Side.bid : Side.ask;
static from(
perpMarket: PerpMarket,
leafNode: LeafNode,
type: BookSideType,
): PerpOrder {
const side =
type == BookSideType.bids ? PerpOrderSide.bid : PerpOrderSide.ask;
const price = BookSide.getPriceFromKey(leafNode.key);
const expiryTimestamp = leafNode.timeInForce
? leafNode.timestamp.add(new BN(leafNode.timeInForce))
@ -506,11 +555,11 @@ export class PerpOrder {
public owner: PublicKey,
public openOrdersSlot: number,
public feeTier: 0,
public price: number,
public uiPrice: number,
public priceLots: BN,
public size: number,
public uiSize: number,
public sizeLots: BN,
public side: Side,
public side: PerpOrderSide,
public timestamp: BN,
public expiryTimestamp: BN,
) {}
@ -532,7 +581,7 @@ export class PerpEventQueue {
this.head = header.head;
this.count = header.count;
this.seqNum = header.seqNum;
this.rawEvents = buf.slice(this.head, this.count).map((event) => {
this.rawEvents = buf.map((event) => {
if (event.eventType === PerpEventQueue.FILL_EVENT_TYPE) {
return (client.program as any)._coder.types.typeLayouts
.get('FillEvent')
@ -554,7 +603,7 @@ export class PerpEventQueue {
),
);
}
throw new Error(`Unknown event with eventType ${event.eventType}`);
throw new Error(`Unknown event with eventType ${event.eventType}!`);
});
}

View File

@ -4,8 +4,12 @@ import { Cluster, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { As } from '../utils';
import { TokenIndex } from './bank';
import { Group } from './group';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48';
export type MarketIndex = number & As<'market-index'>;
export class Serum3Market {
public name: string;
@ -26,12 +30,12 @@ export class Serum3Market {
return new Serum3Market(
publicKey,
obj.group,
obj.baseTokenIndex,
obj.quoteTokenIndex,
obj.baseTokenIndex as TokenIndex,
obj.quoteTokenIndex as TokenIndex,
obj.name,
obj.serumProgram,
obj.serumMarketExternal,
obj.marketIndex,
obj.marketIndex as MarketIndex,
obj.registrationTime,
);
}
@ -39,12 +43,12 @@ export class Serum3Market {
constructor(
public publicKey: PublicKey,
public group: PublicKey,
public baseTokenIndex: number,
public quoteTokenIndex: number,
public baseTokenIndex: TokenIndex,
public quoteTokenIndex: TokenIndex,
name: number[],
public serumProgram: PublicKey,
public serumMarketExternal: PublicKey,
public marketIndex: number,
public marketIndex: MarketIndex,
public registrationTime: BN,
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
@ -58,19 +62,7 @@ export class Serum3Market {
*/
maxBidLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
if (!baseBank) {
throw new Error(
`bank for base token with index ${this.baseTokenIndex} not found`,
);
}
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if (
quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48())
) {
@ -90,18 +82,7 @@ export class Serum3Market {
*/
maxAskLeverage(group: Group): number {
const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex);
if (!baseBank) {
throw new Error(
`bank for base token with index ${this.baseTokenIndex} not found`,
);
}
const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex);
if (!quoteBank) {
throw new Error(
`bank for quote token with index ${this.quoteTokenIndex} not found`,
);
}
if (
baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48())
@ -114,35 +95,19 @@ export class Serum3Market {
.toNumber();
}
public getSerum3ExternalMarket(group: Group) {
return group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
);
}
public async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
const serum3MarketExternal = group.getSerum3ExternalMarket(
this.serumMarketExternal,
);
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadBids(
client.program.provider.connection,
);
}
public async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
const serum3MarketExternal = group.getSerum3ExternalMarket(
this.serumMarketExternal,
);
if (!serum3MarketExternal) {
throw new Error(
`Unable to find serum3MarketExternal for ${this.serumMarketExternal.toBase58()}`,
);
}
return await serum3MarketExternal.loadAsks(
client.program.provider.connection,
);

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,25 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import { Cluster, Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { Group } from '../accounts/group';
import { I80F48 } from '../accounts/I80F48';
import { HealthCache } from '../accounts/healthCache';
import { HealthType, MangoAccount } from '../accounts/mangoAccount';
import { PerpMarket } from '../accounts/perp';
import { Serum3Market } from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
import { toUiDecimalsForQuote } from '../utils';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const PAYER_KEYPAIR =
process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const GROUP_NUM = Number(process.env.GROUP_NUM || 2);
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
async function debugUser(
client: MangoClient,
group: Group,
@ -16,61 +27,56 @@ async function debugUser(
) {
console.log(mangoAccount.toString(group));
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(
'buildFixedAccountRetrieverHealthAccounts ' +
client
.buildFixedAccountRetrieverHealthAccounts(
group,
mangoAccount,
[
group.banksMapByName.get('BTC')![0],
group.banksMapByName.get('USDC')![0],
],
[],
)
.map((pk) => pk.toBase58())
.join(', '),
);
console.log(
'mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
);
console.log(
'mangoAccount.getHealth(HealthType.init) ' +
toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init)!.toNumber()),
toUiDecimalsForQuote(
mangoAccount.getHealth(group, HealthType.init)!.toNumber(),
),
);
console.log(
'HealthCache.fromMangoAccount(group,mangoAccount).health(HealthType.init) ' +
toUiDecimalsForQuote(
HealthCache.fromMangoAccount(group, mangoAccount)
.health(HealthType.init)
.toNumber(),
),
);
console.log(
'mangoAccount.getHealthRatio(HealthType.init) ' +
mangoAccount.getHealthRatio(HealthType.init)!.toNumber(),
mangoAccount.getHealthRatio(group, HealthType.init)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.init) ' +
mangoAccount.getHealthRatioUi(HealthType.init),
mangoAccount.getHealthRatioUi(group, HealthType.init),
);
console.log(
'mangoAccount.getHealthRatio(HealthType.maint) ' +
mangoAccount.getHealthRatio(HealthType.maint)!.toNumber(),
mangoAccount.getHealthRatio(group, HealthType.maint)!.toNumber(),
);
console.log(
'mangoAccount.getHealthRatioUi(HealthType.maint) ' +
mangoAccount.getHealthRatioUi(HealthType.maint),
mangoAccount.getHealthRatioUi(group, HealthType.maint),
);
console.log(
'mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
toUiDecimalsForQuote(mangoAccount.getCollateralValue(group)!.toNumber()),
);
console.log(
'mangoAccount.getAssetsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
),
);
console.log(
'mangoAccount.getLiabsValue() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
),
);
@ -107,19 +113,12 @@ async function debugUser(
function getMaxSourceForTokenSwapWrapper(src, tgt) {
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount
.getMaxSourceForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
)!
.div(
I80F48.fromNumber(
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
),
)
.toNumber(),
mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
),
);
}
for (const srcToken of Array.from(group.banksMapByName.keys())) {
@ -130,6 +129,30 @@ async function debugUser(
}
}
function getMaxForPerpWrapper(perpMarket: PerpMarket) {
console.log(
`getMaxQuoteForPerpBidUi ${perpMarket.perpMarketIndex} ` +
mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
perpMarket.price.toNumber(),
),
);
console.log(
`getMaxBaseForPerpAskUi ${perpMarket.perpMarketIndex} ` +
mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
perpMarket.price.toNumber(),
),
);
}
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
getMaxForPerpWrapper(perpMarket);
}
function getMaxForSerum3Wrapper(serum3Market: Serum3Market) {
// if (serum3Market.name !== 'SOL/USDC') return;
console.log(
@ -156,12 +179,10 @@ async function debugUser(
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.MB_CLUSTER_URL!, options);
const connection = new Connection(CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')),
),
Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))),
);
console.log(`Admin ${admin.publicKey.toBase58()}`);
@ -169,16 +190,15 @@ async function main() {
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = MangoClient.connect(
adminProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
);
const group = await client.getGroupForCreator(admin.publicKey, 2);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
for (const keypair of [
process.env.MB_PAYER_KEYPAIR!,
process.env.MB_USER2_KEYPAIR!,
]) {
for (const keypair of [USER_KEYPAIR!]) {
console.log();
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(keypair, 'utf-8'))),
@ -201,9 +221,9 @@ async function main() {
for (const mangoAccount of mangoAccounts) {
console.log(`MangoAccount ${mangoAccount.publicKey}`);
// if (mangoAccount.name === 'PnL Test') {
await debugUser(client, group, mangoAccount);
// }
if (mangoAccount.name === 'PnL Test') {
await debugUser(client, group, mangoAccount);
}
}
}

View File

@ -0,0 +1,76 @@
///
/// debugging
///
import { AccountMeta, PublicKey } from '@solana/web3.js';
import { Bank } from './accounts/bank';
import { Group } from './accounts/group';
import { MangoAccount, Serum3Orders } from './accounts/mangoAccount';
import { PerpMarket } from './accounts/perp';
export function debugAccountMetas(ams: AccountMeta[]): void {
for (const am of ams) {
console.log(
`${am.pubkey.toBase58()}, isSigner: ${am.isSigner
.toString()
.padStart(5, ' ')}, isWritable - ${am.isWritable
.toString()
.padStart(5, ' ')}`,
);
}
}
export function debugHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
publicKeys: PublicKey[],
): void {
const banks = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].publicKey.toBase58(),
`${banks[0].name} bank`,
]),
);
const oracles = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].oracle.toBase58(),
`${banks[0].name} oracle`,
]),
);
const serum3 = new Map(
mangoAccount.serum3Active().map((serum3: Serum3Orders) => {
const serum3Market = Array.from(
group.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.marketIndex === serum3.marketIndex);
if (!serum3Market) {
throw new Error(
`Serum3Orders for non existent market with market index ${serum3.marketIndex}`,
);
}
return [serum3.openOrders.toBase58(), `${serum3Market.name} spot oo`];
}),
);
const perps = new Map(
Array.from(group.perpMarketsMapByName.values()).map(
(perpMarket: PerpMarket) => [
perpMarket.publicKey.toBase58(),
`${perpMarket.name} perp market`,
],
),
);
publicKeys.map((pk) => {
if (banks.get(pk.toBase58())) {
console.log(banks.get(pk.toBase58()));
}
if (oracles.get(pk.toBase58())) {
console.log(oracles.get(pk.toBase58()));
}
if (serum3.get(pk.toBase58())) {
console.log(serum3.get(pk.toBase58()));
}
if (perps.get(pk.toBase58())) {
console.log(perps.get(pk.toBase58()));
}
});
}

View File

@ -51,7 +51,7 @@ export class Id {
static fromIdsByName(name: string): Id {
const groupConfig = ids.groups.find((id) => id['name'] === name);
if (!groupConfig) throw new Error(`Unable to find group config ${name}`);
if (!groupConfig) throw new Error(`No group config ${name} found in Ids!`);
return new Id(
groupConfig.cluster as Cluster,
groupConfig.name,
@ -71,7 +71,7 @@ export class Id {
(id) => id['publicKey'] === groupPk.toString(),
);
if (!groupConfig)
throw new Error(`Unable to find group config ${groupPk.toString()}`);
throw new Error(`No group config ${groupPk.toString()} found in Ids!`);
return new Id(
groupConfig.cluster as Cluster,
groupConfig.name,

View File

@ -4,7 +4,7 @@ import { MangoClient } from './client';
import { MANGO_V4_ID } from './constants';
export * from './accounts/bank';
export * from './accounts/I80F48';
export * from './numbers/I80F48';
export * from './accounts/mangoAccount';
export * from './accounts/perp';
export {

View File

@ -1076,6 +1076,62 @@ export type MangoV4 = {
},
{
"name": "tokenDeposit",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "bank",
"isMut": true,
"isSigner": false
},
{
"name": "vault",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "tokenAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "amount",
"type": "u64"
}
]
},
{
"name": "tokenDepositIntoExisting",
"accounts": [
{
"name": "group",
@ -2315,6 +2371,26 @@ export type MangoV4 = {
{
"name": "trustedMarket",
"type": "bool"
},
{
"name": "feePenalty",
"type": "f32"
},
{
"name": "settleFeeFlat",
"type": "f32"
},
{
"name": "settleFeeAmountThreshold",
"type": "f32"
},
{
"name": "settleFeeFractionLowHealth",
"type": "f32"
},
{
"name": "settleTokenIndex",
"type": "u16"
}
]
},
@ -2429,6 +2505,30 @@ export type MangoV4 = {
"type": {
"option": "bool"
}
},
{
"name": "feePenaltyOpt",
"type": {
"option": "f32"
}
},
{
"name": "settleFeeFlatOpt",
"type": {
"option": "f32"
}
},
{
"name": "settleFeeAmountThresholdOpt",
"type": {
"option": "f32"
}
},
{
"name": "settleFeeFractionLowHealthOpt",
"type": {
"option": "f32"
}
}
]
},
@ -2824,6 +2924,16 @@ export type MangoV4 = {
"isMut": false,
"isSigner": false
},
{
"name": "settler",
"isMut": true,
"isSigner": false
},
{
"name": "settlerOwner",
"isMut": false,
"isSigner": true
},
{
"name": "perpMarket",
"isMut": false,
@ -2845,17 +2955,17 @@ export type MangoV4 = {
"isSigner": false
},
{
"name": "quoteBank",
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "maxSettleAmount",
"type": "u64"
}
]
"args": []
},
{
"name": "perpSettleFees",
@ -2881,9 +2991,14 @@ export type MangoV4 = {
"isSigner": false
},
{
"name": "quoteBank",
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
}
],
"args": [
@ -3004,15 +3119,20 @@ export type MangoV4 = {
"isSigner": false
},
{
"name": "quoteBank",
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "quoteVault",
"name": "settleVault",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
@ -3795,13 +3915,8 @@ export type MangoV4 = {
"type": "publicKey"
},
{
"name": "padding0",
"type": {
"array": [
"u8",
2
]
}
"name": "settleTokenIndex",
"type": "u16"
},
{
"name": "perpMarketIndex",
@ -4014,12 +4129,38 @@ export type MangoV4 = {
"defined": "I80F48"
}
},
{
"name": "feePenalty",
"type": "f32"
},
{
"name": "settleFeeFlat",
"docs": [
"In native units of settlement token, given to each settle call above the",
"settle_fee_amount_threshold."
],
"type": "f32"
},
{
"name": "settleFeeAmountThreshold",
"docs": [
"Pnl settlement amount needed to be eligible for fees."
],
"type": "f32"
},
{
"name": "settleFeeFractionLowHealth",
"docs": [
"Fraction of pnl to pay out as fee if +pnl account has low health."
],
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
112
92
]
}
}
@ -4391,6 +4532,10 @@ export type MangoV4 = {
{
"name": "hasOpenOrders",
"type": "bool"
},
{
"name": "trustedMarket",
"type": "bool"
}
]
}
@ -4473,9 +4618,23 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
40
16
]
}
},
{
"name": "previousIndex",
"type": {
"defined": "I80F48"
}
},
{
"name": "cumulativeDepositInterest",
"type": "f32"
},
{
"name": "cumulativeBorrowInterest",
"type": "f32"
}
]
}
@ -5425,11 +5584,6 @@ export type MangoV4 = {
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i64",
"index": false
},
{
"name": "longFunding",
"type": "i128",
@ -5474,11 +5628,6 @@ export type MangoV4 = {
"name": "borrowIndex",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
@ -5844,7 +5993,7 @@ export type MangoV4 = {
]
},
{
"name": "OpenOrdersBalanceLog",
"name": "Serum3OpenOrdersBalanceLog",
"fields": [
{
"name": "mangoGroup",
@ -5857,7 +6006,12 @@ export type MangoV4 = {
"index": false
},
{
"name": "marketIndex",
"name": "baseTokenIndex",
"type": "u16",
"index": false
},
{
"name": "quoteTokenIndex",
"type": "u16",
"index": false
},
@ -5885,11 +6039,6 @@ export type MangoV4 = {
"name": "referrerRebatesAccrued",
"type": "u64",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
@ -5974,6 +6123,36 @@ export type MangoV4 = {
"index": false
}
]
},
{
"name": "DeactivateTokenPositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "cumulativeDepositInterest",
"type": "f32",
"index": false
},
{
"name": "cumulativeBorrowInterest",
"type": "f32",
"index": false
}
]
}
],
"errors": [
@ -7173,6 +7352,62 @@ export const IDL: MangoV4 = {
},
{
"name": "tokenDeposit",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "bank",
"isMut": true,
"isSigner": false
},
{
"name": "vault",
"isMut": true,
"isSigner": false
},
{
"name": "oracle",
"isMut": false,
"isSigner": false
},
{
"name": "tokenAccount",
"isMut": true,
"isSigner": false
},
{
"name": "tokenAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "amount",
"type": "u64"
}
]
},
{
"name": "tokenDepositIntoExisting",
"accounts": [
{
"name": "group",
@ -8412,6 +8647,26 @@ export const IDL: MangoV4 = {
{
"name": "trustedMarket",
"type": "bool"
},
{
"name": "feePenalty",
"type": "f32"
},
{
"name": "settleFeeFlat",
"type": "f32"
},
{
"name": "settleFeeAmountThreshold",
"type": "f32"
},
{
"name": "settleFeeFractionLowHealth",
"type": "f32"
},
{
"name": "settleTokenIndex",
"type": "u16"
}
]
},
@ -8526,6 +8781,30 @@ export const IDL: MangoV4 = {
"type": {
"option": "bool"
}
},
{
"name": "feePenaltyOpt",
"type": {
"option": "f32"
}
},
{
"name": "settleFeeFlatOpt",
"type": {
"option": "f32"
}
},
{
"name": "settleFeeAmountThresholdOpt",
"type": {
"option": "f32"
}
},
{
"name": "settleFeeFractionLowHealthOpt",
"type": {
"option": "f32"
}
}
]
},
@ -8921,6 +9200,16 @@ export const IDL: MangoV4 = {
"isMut": false,
"isSigner": false
},
{
"name": "settler",
"isMut": true,
"isSigner": false
},
{
"name": "settlerOwner",
"isMut": false,
"isSigner": true
},
{
"name": "perpMarket",
"isMut": false,
@ -8942,17 +9231,17 @@ export const IDL: MangoV4 = {
"isSigner": false
},
{
"name": "quoteBank",
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "maxSettleAmount",
"type": "u64"
}
]
"args": []
},
{
"name": "perpSettleFees",
@ -8978,9 +9267,14 @@ export const IDL: MangoV4 = {
"isSigner": false
},
{
"name": "quoteBank",
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
}
],
"args": [
@ -9101,15 +9395,20 @@ export const IDL: MangoV4 = {
"isSigner": false
},
{
"name": "quoteBank",
"name": "settleBank",
"isMut": true,
"isSigner": false
},
{
"name": "quoteVault",
"name": "settleVault",
"isMut": true,
"isSigner": false
},
{
"name": "settleOracle",
"isMut": false,
"isSigner": false
},
{
"name": "insuranceVault",
"isMut": true,
@ -9892,13 +10191,8 @@ export const IDL: MangoV4 = {
"type": "publicKey"
},
{
"name": "padding0",
"type": {
"array": [
"u8",
2
]
}
"name": "settleTokenIndex",
"type": "u16"
},
{
"name": "perpMarketIndex",
@ -10111,12 +10405,38 @@ export const IDL: MangoV4 = {
"defined": "I80F48"
}
},
{
"name": "feePenalty",
"type": "f32"
},
{
"name": "settleFeeFlat",
"docs": [
"In native units of settlement token, given to each settle call above the",
"settle_fee_amount_threshold."
],
"type": "f32"
},
{
"name": "settleFeeAmountThreshold",
"docs": [
"Pnl settlement amount needed to be eligible for fees."
],
"type": "f32"
},
{
"name": "settleFeeFractionLowHealth",
"docs": [
"Fraction of pnl to pay out as fee if +pnl account has low health."
],
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
112
92
]
}
}
@ -10488,6 +10808,10 @@ export const IDL: MangoV4 = {
{
"name": "hasOpenOrders",
"type": "bool"
},
{
"name": "trustedMarket",
"type": "bool"
}
]
}
@ -10570,9 +10894,23 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
40
16
]
}
},
{
"name": "previousIndex",
"type": {
"defined": "I80F48"
}
},
{
"name": "cumulativeDepositInterest",
"type": "f32"
},
{
"name": "cumulativeBorrowInterest",
"type": "f32"
}
]
}
@ -11522,11 +11860,6 @@ export const IDL: MangoV4 = {
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i64",
"index": false
},
{
"name": "longFunding",
"type": "i128",
@ -11571,11 +11904,6 @@ export const IDL: MangoV4 = {
"name": "borrowIndex",
"type": "i128",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
@ -11941,7 +12269,7 @@ export const IDL: MangoV4 = {
]
},
{
"name": "OpenOrdersBalanceLog",
"name": "Serum3OpenOrdersBalanceLog",
"fields": [
{
"name": "mangoGroup",
@ -11954,7 +12282,12 @@ export const IDL: MangoV4 = {
"index": false
},
{
"name": "marketIndex",
"name": "baseTokenIndex",
"type": "u16",
"index": false
},
{
"name": "quoteTokenIndex",
"type": "u16",
"index": false
},
@ -11982,11 +12315,6 @@ export const IDL: MangoV4 = {
"name": "referrerRebatesAccrued",
"type": "u64",
"index": false
},
{
"name": "price",
"type": "i128",
"index": false
}
]
},
@ -12071,6 +12399,36 @@ export const IDL: MangoV4 = {
"index": false
}
]
},
{
"name": "DeactivateTokenPositionLog",
"fields": [
{
"name": "mangoGroup",
"type": "publicKey",
"index": false
},
{
"name": "mangoAccount",
"type": "publicKey",
"index": false
},
{
"name": "tokenIndex",
"type": "u16",
"index": false
},
{
"name": "cumulativeDepositInterest",
"type": "f32",
"index": false
},
{
"name": "cumulativeBorrowInterest",
"type": "f32",
"index": false
}
]
}
],
"errors": [

View File

@ -45,7 +45,7 @@ export class I80F48 {
}
static fromNumber(x: number): I80F48 {
const int_part = Math.trunc(x);
const v = new BN(int_part).iushln(48);
const v = new BN(int_part.toFixed(0)).iushln(48);
v.iadd(new BN((x - int_part) * I80F48.MULTIPLIER_NUMBER));
return new I80F48(v);
}

View File

@ -0,0 +1,37 @@
import BN from 'bn.js';
import { expect } from 'chai';
import { U64_MAX_BN } from '../utils';
import { I80F48 } from './I80F48';
describe('Math', () => {
it('js number to BN and I80F48', () => {
// BN can be only be created from js numbers which are <=2^53
expect(function () {
new BN(0x1fffffffffffff);
}).to.not.throw('Assertion failed');
expect(function () {
new BN(0x20000000000000);
}).to.throw('Assertion failed');
// max BN cant be converted to a number
expect(function () {
U64_MAX_BN.toNumber();
}).to.throw('Number can only safely store up to 53 bits');
// max I80F48 can be converted to a number
// though, the number is represented in scientific notation
// anything above ^20 gets represented with scientific notation
expect(
I80F48.fromString('604462909807314587353087.999999999999996')
.toNumber()
.toString(),
).equals('6.044629098073146e+23');
// I80F48 constructor takes a BN, but it doesnt do what one might think it does
expect(new I80F48(new BN(10)).toNumber()).not.equals(10);
expect(I80F48.fromI64(new BN(10)).toNumber()).equals(10);
// BN treats input as whole integer
expect(new BN(1.5).toNumber()).equals(1);
});
});

View File

@ -1,7 +1,6 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { AccountSize } from '../../accounts/mangoAccount';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
@ -45,10 +44,7 @@ async function main() {
const group = await user1Client.getGroupForAdmin(admin.publicKey, GROUP_NUM);
console.log(`Found group ${group.publicKey.toBase58()}`);
const user1MangoAccount = await user1Client.getOrCreateMangoAccount(
group,
user1.publicKey,
);
const user1MangoAccount = await user1Client.getOrCreateMangoAccount(group);
console.log(`...mangoAccount1 ${user1MangoAccount.publicKey}`);
@ -75,10 +71,7 @@ async function main() {
);
console.log(`user2 ${user2Wallet.publicKey.toBase58()}`);
const user2MangoAccount = await user2Client.getOrCreateMangoAccount(
group,
user2.publicKey,
);
const user2MangoAccount = await user2Client.getOrCreateMangoAccount(group);
console.log(`...mangoAccount2 ${user2MangoAccount.publicKey}`);
/// Increase usdc price temporarily to allow lots of borrows

View File

@ -48,10 +48,7 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());

View File

@ -42,10 +42,7 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// logging serum3 open orders for user

View File

@ -43,10 +43,7 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
// log users tokens

View File

@ -48,10 +48,7 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());

View File

@ -53,10 +53,7 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(`start balance \n${mangoAccount.toString(group)}`);

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';
//
// An example for admins based on high level api i.e. the client
@ -357,9 +363,7 @@ async function main() {
0,
'BTC-PERP',
0.1,
1,
6,
1,
10,
100,
0.975,
@ -369,18 +373,22 @@ async function main() {
0.012,
0.0002,
0.0,
0,
0.05,
0.05,
100,
true,
true,
1000,
1000000,
0.05,
0,
);
console.log('done');
} catch (error) {
console.log(error);
}
const perpMarkets = await client.perpGetMarkets(
group,
group.getFirstBankByMint(btcDevnetMint).tokenIndex,
);
const perpMarkets = await client.perpGetMarkets(group);
console.log(`...created perp market ${perpMarkets[0].publicKey}`);
//
@ -480,6 +488,109 @@ async function main() {
} catch (error) {
throw error;
}
console.log(`Editing BTC-PERP...`);
try {
let sig = await client.perpEditMarket(
group,
group.getPerpMarketByName('BTC-PERP').perpMarketIndex,
btcDevnetOracle,
0.1,
6,
0.975,
0.95,
1.025,
1.05,
0.012,
0.0002,
0.0,
0,
0.05,
0.05,
100,
true,
true,
1000,
1000000,
0.05,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
} catch (error) {
throw error;
}
}
if (
// true
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}?cluster=devnet`,
);
console.log(`ALT: set at index 0 for group...`);
sig = await client.altSet(
group,
new PublicKey('EmN5RjHUFsoag7tZ2AyBL2N8JrhV7nLMKgNbpCfzC81D'),
0,
);
console.log(`...https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// Extend using a mango v4 program ix
// Throws > Instruction references an unknown account 11111111111111111111111111111111 atm
//
console.log(
`ALT: extending using mango v4 program with bank publick keys and oracles`,
);
// let sig = await client.altExtend(
// group,
// new PublicKey('EmN5RjHUFsoag7tZ2AyBL2N8JrhV7nLMKgNbpCfzC81D'),
// 0,
// Array.from(group.banksMapByMint.values())
// .flat()
// .map((bank) => [bank.publicKey, bank.oracle])
// .flat(),
// );
// console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
console.log(`ALT: extending manually with bank publick keys and oracles`);
const extendIx = AddressLookupTableProgram.extendLookupTable({
lookupTable: createIx[1],
payer: admin.publicKey,
authority: admin.publicKey,
addresses: Array.from(group.banksMapByMint.values())
.flat()
.map((bank) => [bank.publicKey, bank.oracle])
.flat(),
});
const extendTx = await buildVersionedTx(
client.program.provider as AnchorProvider,
[extendIx],
);
sig = await client.program.provider.connection.sendTransaction(extendTx);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
}
}
try {
} catch (error) {
console.log(error);
}
process.exit();

View File

@ -71,10 +71,7 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));

View File

@ -1,9 +1,9 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { expect } from 'chai';
import fs from 'fs';
import { I80F48 } from '../accounts/I80F48';
import { HealthType } from '../accounts/mangoAccount';
import { BookSide, PerpOrderType, Side } from '../accounts/perp';
import { BookSide, PerpOrderSide, PerpOrderType } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
@ -69,115 +69,137 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
let mangoAccount = (await client.getOrCreateMangoAccount(
group,
user.publicKey,
))!;
let mangoAccount = (await client.getOrCreateMangoAccount(group))!;
await mangoAccount.reload(client);
if (!mangoAccount) {
throw new Error(`MangoAccount not found for user ${user.publicKey}`);
}
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));
await mangoAccount.reload(client, group);
// set delegate, and change name
if (false) {
if (true) {
console.log(`...changing mango account name, and setting a delegate`);
const newName = 'my_changed_name';
const randomKey = new PublicKey(
'4ZkS7ZZkxfsC3GtvvsHP3DFcUeByU9zzZELS4r8HCELo',
);
await client.editMangoAccount(
group,
mangoAccount,
'my_changed_name',
randomKey,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
await client.editMangoAccount(group, mangoAccount, newName, randomKey);
await mangoAccount.reload(client);
expect(mangoAccount.name).deep.equals(newName);
expect(mangoAccount.delegate).deep.equals(randomKey);
const oldName = 'my_mango_account';
console.log(`...resetting mango account name, and re-setting a delegate`);
await client.editMangoAccount(
group,
mangoAccount,
'my_mango_account',
oldName,
PublicKey.default,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString());
await mangoAccount.reload(client);
expect(mangoAccount.name).deep.equals(oldName);
expect(mangoAccount.delegate).deep.equals(PublicKey.default);
}
// expand account
if (false) {
if (
mangoAccount.tokens.length < 16 ||
mangoAccount.serum3.length < 8 ||
mangoAccount.perps.length < 8 ||
mangoAccount.perpOpenOrders.length < 64
) {
console.log(
`...expanding mango account to have serum3 and perp position slots`,
`...expanding mango account to max 16 token positions, 8 serum3, 8 perp position and 64 perp oo slots, previous (tokens ${mangoAccount.tokens.length}, serum3 ${mangoAccount.serum3.length}, perps ${mangoAccount.perps.length}, perps oo ${mangoAccount.perpOpenOrders.length})`,
);
await client.expandMangoAccount(group, mangoAccount, 8, 8, 8, 8);
await mangoAccount.reload(client, group);
let sig = await client.expandMangoAccount(
group,
mangoAccount,
16,
8,
8,
64,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await mangoAccount.reload(client);
expect(mangoAccount.tokens.length).equals(16);
expect(mangoAccount.serum3.length).equals(8);
expect(mangoAccount.perps.length).equals(8);
expect(mangoAccount.perpOpenOrders.length).equals(64);
}
// deposit and withdraw
if (false) {
try {
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
50,
);
await mangoAccount.reload(client, group);
if (true) {
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client, group);
// deposit USDC
let oldBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
50,
);
await mangoAccount.reload(client);
let newBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
expect(toUiDecimalsForQuote(newBalance.sub(oldBalance)).toString()).equals(
'50',
);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
1,
);
await mangoAccount.reload(client, group);
// deposit SOL
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client);
console.log(`...withdrawing 1 USDC`);
await client.tokenWithdraw(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
1,
true,
);
await mangoAccount.reload(client, group);
// deposit MNGO
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
1,
);
await mangoAccount.reload(client);
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('BTC')!),
0.0005,
);
await mangoAccount.reload(client, group);
// withdraw USDC
console.log(`...withdrawing 1 USDC`);
oldBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
await client.tokenWithdraw(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
1,
true,
);
await mangoAccount.reload(client);
newBalance = mangoAccount.getTokenBalance(
group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)),
);
expect(toUiDecimalsForQuote(oldBalance.sub(newBalance)).toString()).equals(
'1',
);
console.log(mangoAccount.toString(group));
} catch (error) {
console.log(error);
}
console.log(`...depositing 0.0005 BTC`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('BTC')!),
0.0005,
);
await mangoAccount.reload(client);
}
if (false) {
if (true) {
// serum3
const serum3Market = group.serum3MarketsMapByExternal.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
);
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!,
);
const asks = await group.loadSerum3AsksForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
@ -187,7 +209,19 @@ async function main() {
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const highestBid = Array.from(asks!)![0];
const highestBid = Array.from(bids!)![0];
console.log(`...cancelling all existing serum3 orders`);
if (
Array.from(mangoAccount.serum3OosMapByMarketIndex.values()).length > 0
) {
await client.serum3CancelAllOrders(
group,
mangoAccount,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
10,
);
}
let price = 20;
let qty = 0.0001;
@ -206,7 +240,14 @@ async function main() {
Date.now(),
10,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
expect(orders[0].price).equals(20);
expect(orders[0].size).equals(qty);
price = lowestAsk.price + lowestAsk.price / 2;
qty = 0.0001;
@ -225,7 +266,7 @@ async function main() {
Date.now(),
10,
);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
price = highestBid.price - highestBid.price / 2;
qty = 0.0001;
@ -246,7 +287,7 @@ async function main() {
);
console.log(`...current own orders on OB`);
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
@ -283,42 +324,28 @@ async function main() {
);
}
if (false) {
// serum3 market
const serum3Market = group.serum3MarketsMapByExternal.get(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(),
);
console.log(await serum3Market?.logOb(client, group));
}
if (false) {
await mangoAccount.reload(client, group);
if (true) {
await mangoAccount.reload(client);
console.log(
'...mangoAccount.getEquity() ' +
toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()),
toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()),
);
console.log(
'...mangoAccount.getCollateralValue() ' +
toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()),
);
console.log(
'...mangoAccount.accountData["healthCache"].health(HealthType.init) ' +
toUiDecimalsForQuote(
mangoAccount
.accountData!['healthCache'].health(HealthType.init)
.toNumber(),
mangoAccount.getCollateralValue(group)!.toNumber(),
),
);
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsValue(HealthType.init)!.toNumber(),
mangoAccount.getAssetsValue(group, HealthType.init)!.toNumber(),
),
);
console.log(
'...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsValue(HealthType.init)!.toNumber(),
mangoAccount.getLiabsValue(group, HealthType.init)!.toNumber(),
),
);
console.log(
@ -334,35 +361,16 @@ async function main() {
);
}
if (false) {
const asks = await group.loadSerum3AsksForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const lowestAsk = Array.from(asks!)[0];
const bids = await group.loadSerum3BidsForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const highestBid = Array.from(asks!)![0];
if (true) {
function getMaxSourceForTokenSwapWrapper(src, tgt) {
// console.log();
console.log(
`getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` +
mangoAccount
.getMaxSourceForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
)!
.div(
I80F48.fromNumber(
Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals),
),
)
.toNumber(),
mangoAccount.getMaxSourceUiForTokenSwap(
group,
group.banksMapByName.get(src)![0].mint,
group.banksMapByName.get(tgt)![0].mint,
1,
)!,
);
}
for (const srcToken of Array.from(group.banksMapByName.keys())) {
@ -407,39 +415,101 @@ async function main() {
// perps
if (true) {
let sig;
const perpMarket = group.getPerpMarketByName('BTC-PERP');
const orders = await mangoAccount.loadPerpOpenOrdersForMarket(
client,
group,
'BTC-PERP',
perpMarket.perpMarketIndex,
);
for (const order of orders) {
console.log(`Current order - ${order.price} ${order.size} ${order.side}`);
console.log(
`Current order - ${order.uiPrice} ${order.uiSize} ${order.side}`,
);
}
console.log(`...cancelling all perp orders`);
let sig = await client.perpCancelAllOrders(
sig = await client.perpCancelAllOrders(
group,
mangoAccount,
'BTC-PERP',
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// scenario 1
// not going to be hit orders, far from each other
// bid max perp
try {
const clientId = Math.floor(Math.random() * 99999);
await mangoAccount.reload(client);
await group.reloadAll(client);
const price =
group.banksMapByName.get('BTC')![0].uiPrice! -
Math.floor(Math.random() * 100);
const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
);
const baseQty = quoteQty / price;
console.log(
` simHealthRatioWithPerpBidUiChanges - ${mangoAccount.simHealthRatioWithPerpBidUiChanges(
group,
perpMarket.perpMarketIndex,
baseQty,
)}`,
);
console.log(
`...placing max qty perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
perpMarket.perpMarketIndex,
PerpOrderSide.bid,
price,
baseQty,
quoteQty,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
1,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
}
console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// bid max perp + some
try {
const clientId = Math.floor(Math.random() * 99999);
const price =
group.banksMapByName.get('BTC')![0].uiPrice! -
Math.floor(Math.random() * 100);
console.log(`...placing perp bid ${clientId} at ${price}`);
const quoteQty =
mangoAccount.getMaxQuoteForPerpBidUi(
group,
perpMarket.perpMarketIndex,
) * 1.02;
const baseQty = quoteQty / price;
console.log(
`...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.bid,
perpMarket.perpMarketIndex,
PerpOrderSide.bid,
price,
0.01,
price * 0.01,
baseQty,
quoteQty,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
@ -448,21 +518,38 @@ async function main() {
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
console.log('Errored out as expected');
}
// bid max ask
try {
const clientId = Math.floor(Math.random() * 99999);
const price =
group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100);
console.log(`...placing perp ask ${clientId} at ${price}`);
const baseQty = mangoAccount.getMaxBaseForPerpAskUi(
group,
perpMarket.perpMarketIndex,
);
console.log(
` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges(
group,
perpMarket.perpMarketIndex,
baseQty,
)}`,
);
const quoteQty = baseQty * price;
console.log(
`...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.ask,
perpMarket.perpMarketIndex,
PerpOrderSide.ask,
price,
0.01,
price * 0.01,
baseQty,
quoteQty,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
@ -472,9 +559,46 @@ async function main() {
} catch (error) {
console.log(error);
}
// should be able to cancel them
// bid max ask + some
try {
const clientId = Math.floor(Math.random() * 99999);
const price =
group.banksMapByName.get('BTC')![0].uiPrice! +
Math.floor(Math.random() * 100);
const baseQty =
mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) *
1.02;
const quoteQty = baseQty * price;
console.log(
`...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`,
);
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
perpMarket.perpMarketIndex,
PerpOrderSide.ask,
price,
baseQty,
quoteQty,
clientId,
PerpOrderType.limit,
0, //Date.now() + 200,
1,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
} catch (error) {
console.log(error);
console.log('Errored out as expected');
}
console.log(`...cancelling all perp orders`);
sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
sig = await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
// scenario 2
@ -486,8 +610,8 @@ async function main() {
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.bid,
perpMarket.perpMarketIndex,
PerpOrderSide.bid,
price,
0.01,
price * 0.01,
@ -507,8 +631,8 @@ async function main() {
const sig = await client.perpPlaceOrder(
group,
mangoAccount,
'BTC-PERP',
Side.ask,
perpMarket.perpMarketIndex,
PerpOrderSide.ask,
price,
0.01,
price * 0.011,
@ -523,32 +647,32 @@ async function main() {
}
// // should be able to cancel them : know bug
// console.log(`...cancelling all perp orders`);
// sig = await client.perpCancelAllOrders(group, mangoAccount, 'BTC-PERP', 10);
// sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10);
// console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`);
const perpMarket = group.perpMarketsMap.get('BTC-PERP');
const bids: BookSide = await perpMarket?.loadBids(client)!;
console.log(`bids - ${Array.from(bids.items())}`);
const asks: BookSide = await perpMarket?.loadAsks(client)!;
console.log(`asks - ${Array.from(asks.items())}`);
await perpMarket?.loadEventQueue(client)!;
const fr = await perpMarket?.getCurrentFundingRate(
const fr = perpMarket?.getCurrentFundingRate(
await perpMarket.loadBids(client),
await perpMarket.loadAsks(client),
);
console.log(`current funding rate per hour is ${fr}`);
const eq = await perpMarket?.loadEventQueue(client)!;
console.log(`raw events - ${eq.rawEvents}`);
console.log(
`raw events - ${JSON.stringify(eq.eventsSince(new BN(0)), null, 2)}`,
);
// sleep so that keeper can catch up
await new Promise((r) => setTimeout(r, 2000));
// make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position
// make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position or we see a small quotePositionNative
await group.reloadAll(client);
await mangoAccount.reload(client, group);
await mangoAccount.reload(client);
console.log(`${mangoAccount.toString(group)}`);
}

View File

@ -45,15 +45,6 @@ async function main() {
let sig;
// close stub oracles
const stubOracles = await client.getStubOracle(group);
for (const stubOracle of stubOracles) {
sig = await client.stubOracleClose(group, stubOracle.publicKey);
console.log(
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// close all banks
for (const banks of group.banksMapByMint.values()) {
sig = await client.tokenDeregister(group, banks[0].mint);
@ -81,6 +72,15 @@ async function main() {
);
}
// close stub oracles
const stubOracles = await client.getStubOracle(group);
for (const stubOracle of stubOracles) {
sig = await client.stubOracleClose(group, stubOracle.publicKey);
console.log(
`Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// finally, close the group
sig = await client.groupClose(group);
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);

View File

@ -337,10 +337,7 @@ async function createUser(userKeypair: string) {
const user = result[2];
console.log(`Creating MangoAccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
if (!mangoAccount) {
throw new Error(`MangoAccount not found for user ${user.publicKey}`);
}

View File

@ -34,10 +34,7 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
const mangoAccount = await client.getOrCreateMangoAccount(group);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));

View File

@ -16,12 +16,14 @@ const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['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
]);
// External markets are matched with those in https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json
@ -179,14 +181,52 @@ async function main() {
}
console.log('Registering SOL/USDC serum market...');
await client.serum3RegisterMarket(
group,
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)),
1,
'SOL/USDC',
);
try {
await client.serum3RegisterMarket(
group,
new PublicKey(MAINNET_SERUM3_MARKETS.get('SOL/USDC')!),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('SOL')!)),
group.getFirstBankByMint(new PublicKey(MAINNET_MINTS.get('USDC')!)),
1,
'SOL/USDC',
);
} catch (error) {
console.log(error);
}
console.log('Registering MNGO-PERP market...');
const mngoMainnetOracle = oracles.get('MNGO');
try {
await client.perpCreateMarket(
group,
mngoMainnetOracle,
0,
'MNGO-PERP',
0.1,
9,
10,
100000, // base lots
0.9,
0.8,
1.1,
1.2,
0.05,
-0.001,
0.002,
0,
-0.1,
0.1,
10,
false,
false,
0,
0,
0,
0,
);
} catch (error) {
console.log(error);
}
process.exit();
}

View File

@ -39,11 +39,14 @@ async function main() {
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = (await client.getOrCreateMangoAccount(
const mangoAccount = (await client.createAndFetchMangoAccount(
group,
admin.publicKey,
ACCOUNT_NUM,
'LIQTEST, FUNDING',
8,
4,
4,
4,
))!;
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
@ -54,16 +57,16 @@ async function main() {
// deposit
try {
console.log(`...depositing 10 USDC`);
await client.tokenDeposit(group, mangoAccount, usdcMint, 10);
console.log(`...depositing 5 USDC`);
await client.tokenDeposit(group, mangoAccount, usdcMint, 5);
await mangoAccount.reload(client, group);
console.log(`...depositing 0.0004 BTC`);
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0004);
console.log(`...depositing 0.0002 BTC`);
await client.tokenDeposit(group, mangoAccount, btcMint, 0.0002);
await mangoAccount.reload(client, group);
console.log(`...depositing 0.25 SOL`);
await client.tokenDeposit(group, mangoAccount, solMint, 0.25);
console.log(`...depositing 0.15 SOL`);
await client.tokenDeposit(group, mangoAccount, solMint, 0.15);
await mangoAccount.reload(client, group);
} catch (error) {
console.log(error);

View File

@ -6,6 +6,8 @@ import {
Serum3SelfTradeBehavior,
Serum3Side,
} from '../accounts/serum3';
import { Side, PerpOrderType } from '../accounts/perp';
import { MangoAccount } from '../accounts/mangoAccount';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
@ -20,12 +22,14 @@ const PRICES = {
BTC: 20000.0,
SOL: 0.04,
USDC: 1,
MNGO: 0.04,
};
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['SOL', 'So11111111111111111111111111111111111111112'],
['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'],
]);
const TOKEN_SCENARIOS: [string, string, number, string, number][] = [
@ -66,19 +70,30 @@ async function main() {
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;
maxAccountNum = maxAccountNum + 1;
await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4);
return (await client.getMangoAccountForOwner(
group,
admin.publicKey,
accountNum,
))!;
}
for (const scenario of TOKEN_SCENARIOS) {
const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
// create account
console.log(`Creating mangoaccount...`);
let mangoAccount = (await client.getOrCreateMangoAccount(
group,
admin.publicKey,
maxAccountNum + 1,
name,
))!;
maxAccountNum = maxAccountNum + 1;
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
@ -119,13 +134,7 @@ async function main() {
const name = 'LIQTEST, serum orders';
console.log(`Creating mangoaccount...`);
let mangoAccount = (await client.getOrCreateMangoAccount(
group,
admin.publicKey,
maxAccountNum + 1,
name,
))!;
maxAccountNum = maxAccountNum + 1;
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
@ -188,6 +197,211 @@ async function main() {
}
}
// Perp orders bring health <0, liquidator force closes
{
const name = 'LIQTEST, perp orders';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...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;
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
100000,
); // valued as $0.004 maint collateral
await mangoAccount.reload(client, group);
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 4);
try {
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.bid,
1, // ui price that won't get hit
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
4200,
PerpOrderType.limit,
0,
5,
);
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
}
}
// Perp base pos brings health<0, liquidator takes most of it
{
const name = 'LIQTEST, perp base pos';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...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;
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
100000,
); // valued as $0.004 maint collateral
await mangoAccount.reload(client, group);
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 5);
try {
await client.perpPlaceOrder(
group,
fundingAccount,
'MNGO-PERP',
Side.ask,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
4200,
PerpOrderType.limit,
0,
5,
);
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.bid,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
4200,
PerpOrderType.market,
0,
5,
);
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
}
}
// borrows and positive perp pnl (but no position)
{
const name = 'LIQTEST, perp positive pnl';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const baseMint = new PublicKey(MAINNET_MINTS.get('MNGO')!);
const baseOracle = (await client.getStubOracle(group, baseMint))[0]
.publicKey;
const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const collateralOracle = group.banksMapByName.get('SOL')![0].oracle;
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
100000,
); // valued as $0.004 maint collateral
await mangoAccount.reload(client, group);
try {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL'] * 10);
// Spot-borrow more than the collateral is worth
await client.tokenWithdrawNative(
group,
mangoAccount,
liabMint,
-5000,
true,
);
await mangoAccount.reload(client, group);
// Execute two trades that leave the account with +$0.022 positive pnl
await client.stubOracleSet(group, baseOracle, PRICES['MNGO'] / 2);
await client.perpPlaceOrder(
group,
fundingAccount,
'MNGO-PERP',
Side.ask,
20,
0.0011, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
4200,
PerpOrderType.limit,
0,
5,
);
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.bid,
20,
0.0011, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
4200,
PerpOrderType.market,
0,
5,
);
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
await client.perpPlaceOrder(
group,
fundingAccount,
'MNGO-PERP',
Side.bid,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
4201,
PerpOrderType.limit,
0,
5,
);
await client.perpPlaceOrder(
group,
mangoAccount,
'MNGO-PERP',
Side.ask,
40,
0.0011, // ui base quantity, 11 base lots, $0.044
0.044, // ui quote quantity
4201,
PerpOrderType.market,
0,
5,
);
await client.perpConsumeAllEvents(group, 'MNGO-PERP');
} finally {
await client.stubOracleSet(group, collateralOracle, PRICES['SOL']);
await client.stubOracleSet(group, baseOracle, PRICES['MNGO']);
}
}
process.exit();
}

View File

@ -1,7 +1,8 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { BN, AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { Side, PerpOrderType } from '../accounts/perp';
import { MANGO_V4_ID } from '../constants';
//
@ -59,63 +60,18 @@ async function main() {
await client.serum3SettleFunds(group, account, serumExternal);
await client.serum3CloseOpenOrders(group, account, serumExternal);
}
}
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) {
console.log(`settling borrows on account: ${account}`);
// first, settle all borrows
for (let token of account.tokensActive()) {
const bank = group.getFirstBankByTokenIndex(token.tokenIndex);
const amount = token.balance(bank).toNumber();
if (amount < 0) {
try {
await client.tokenDepositNative(
group,
account,
bank.mint,
Math.ceil(-amount),
);
await account.reload(client, group);
} catch (error) {
console.log(
`failed to deposit ${bank.name} into ${account.publicKey}: ${error}`,
);
process.exit();
}
}
for (let perpPosition of account.perpActive()) {
const perpMarket = group.findPerpMarket(perpPosition.marketIndex)!;
console.log(
`closing perp orders on: ${account} for market ${perpMarket.name}`,
);
await client.perpCancelAllOrders(group, account, perpMarket.name, 10);
}
}
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) {
console.log(`withdrawing deposits of account: ${account}`);
// withdraw all funds
for (let token of account.tokensActive()) {
const bank = group.getFirstBankByTokenIndex(token.tokenIndex);
const amount = token.balance(bank).toNumber();
if (amount > 0) {
try {
const allowBorrow = false;
await client.tokenWithdrawNative(
group,
account,
bank.mint,
amount,
allowBorrow,
);
await account.reload(client, group);
} catch (error) {
console.log(
`failed to withdraw ${bank.name} from ${account.publicKey}: ${error}`,
);
process.exit();
}
}
}
// close account
try {
console.log(`closing account: ${account}`);

View File

@ -0,0 +1,48 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
// For easy switching between mainnet and devnet, default is mainnet
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
// Throwaway keypair
const user = new Keypair();
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
);
// Load mango account
let mangoAccount = await client.getMangoAccountForPublicKey(
new PublicKey(MANGO_ACCOUNT_PK),
);
await mangoAccount.reload(client);
// Load group for mango account
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
// Log OB
const perpMarket = group.getPerpMarketByName('BTC-PERP');
while (true) {
await new Promise((r) => setTimeout(r, 2000));
console.clear();
console.log(await perpMarket.logOb(client));
}
}
main();

View File

@ -0,0 +1,514 @@
import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import {
Cluster,
Connection,
Keypair,
PublicKey,
TransactionInstruction,
} from '@solana/web3.js';
import fs from 'fs';
import path from 'path';
import { Group } from '../../accounts/group';
import { MangoAccount } from '../../accounts/mangoAccount';
import {
BookSide,
PerpMarket,
PerpMarketIndex,
PerpOrderSide,
PerpOrderType,
} from '../../accounts/perp';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
import { toUiDecimalsForQuote } from '../../utils';
import { sendTransaction } from '../../utils/rpc';
import {
makeCheckAndSetSequenceNumberIx,
makeInitSequenceEnforcerAccountIx,
seqEnforcerProgramIds,
} from './sequence-enforcer-util';
// TODO switch to more efficient async logging if available in nodejs
// Env vars
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
// Load configuration
const paramsFileName = process.env.PARAMS || 'default.json';
const params = JSON.parse(
fs.readFileSync(
path.resolve(__dirname, `./params/${paramsFileName}`),
'utf-8',
),
);
const control = { isRunning: true, interval: params.interval };
// State which is passed around
type State = {
mangoAccount: MangoAccount;
lastMangoAccountUpdate: number;
marketContexts: Map<PerpMarketIndex, MarketContext>;
};
type MarketContext = {
params: any;
market: PerpMarket;
bids: BookSide;
asks: BookSide;
lastBookUpdate: number;
aggBid: number | undefined;
aggAsk: number | undefined;
ftxMid: number | undefined;
sequenceAccount: PublicKey;
sequenceAccountBump: number;
sentBidPrice: number;
sentAskPrice: number;
lastOrderUpdate: number;
};
// Refresh mango account and perp market order books
async function refreshState(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
marketContexts: Map<PerpMarketIndex, MarketContext>,
): Promise<State> {
const ts = Date.now() / 1000;
// TODO do all updates in one RPC call
await Promise.all([group.reloadAll(client), mangoAccount.reload(client)]);
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
const mc = marketContexts.get(perpMarket.perpMarketIndex)!;
mc.market = perpMarket;
mc.bids = await perpMarket.loadBids(client);
mc.asks = await perpMarket.loadAsks(client);
mc.lastBookUpdate = ts;
}
return {
mangoAccount,
lastMangoAccountUpdate: ts,
marketContexts,
};
}
// Initialiaze sequence enforcer accounts
async function initSequenceEnforcerAccounts(
client: MangoClient,
marketContexts: MarketContext[],
) {
const seqAccIxs = marketContexts.map((mc) =>
makeInitSequenceEnforcerAccountIx(
mc.sequenceAccount,
(client.program.provider as AnchorProvider).wallet.publicKey,
mc.sequenceAccountBump,
mc.market.name,
CLUSTER,
),
);
while (true) {
try {
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
seqAccIxs,
[],
);
console.log(
`Sequence enforcer accounts created, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
} catch (e) {
console.log('Failed to initialize sequence enforcer accounts!');
console.log(e);
continue;
}
break;
}
}
// Cancel all orders on exit
async function onExit(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
marketContexts: MarketContext[],
) {
const ixs: TransactionInstruction[] = [];
for (const mc of marketContexts) {
const cancelAllIx = await client.perpCancelAllOrdersIx(
group,
mangoAccount,
mc.market.perpMarketIndex,
10,
);
ixs.push(cancelAllIx);
}
await sendTransaction(client.program.provider as AnchorProvider, ixs, []);
}
// Main driver for the market maker
async function fullMarketMaker() {
// Load client
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))),
);
// TODO: make work for delegate
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
);
// Load mango account
let mangoAccount = await client.getMangoAccountForPublicKey(
new PublicKey(MANGO_ACCOUNT_PK),
);
console.log(
`MangoAccount ${mangoAccount.publicKey} for user ${user.publicKey}`,
);
await mangoAccount.reload(client);
// Load group
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
// Cancel all existing orders
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
await client.perpCancelAllOrders(
group,
mangoAccount,
perpMarket.perpMarketIndex,
10,
);
}
// Build and maintain an aggregate context object per market
const marketContexts: Map<PerpMarketIndex, MarketContext> = new Map();
for (const perpMarket of Array.from(
group.perpMarketsMapByMarketIndex.values(),
)) {
const [sequenceAccount, sequenceAccountBump] =
await PublicKey.findProgramAddress(
[
Buffer.from(perpMarket.name, 'utf-8'),
(
client.program.provider as AnchorProvider
).wallet.publicKey.toBytes(),
],
seqEnforcerProgramIds[CLUSTER],
);
marketContexts.set(perpMarket.perpMarketIndex, {
params: params.assets[perpMarket.name.replace('-PERP', '')].perp,
market: perpMarket,
bids: await perpMarket.loadBids(client),
asks: await perpMarket.loadAsks(client),
lastBookUpdate: 0,
sequenceAccount,
sequenceAccountBump,
sentBidPrice: 0,
sentAskPrice: 0,
lastOrderUpdate: 0,
// TODO
aggBid: undefined,
aggAsk: undefined,
ftxMid: undefined,
});
}
// Init sequence enforcer accounts
await initSequenceEnforcerAccounts(
client,
Array.from(marketContexts.values()),
);
// Load state first time
let state = await refreshState(client, group, mangoAccount, marketContexts);
// Add handler for e.g. CTRL+C
process.on('SIGINT', function () {
console.log('Caught keyboard interrupt. Canceling orders');
control.isRunning = false;
onExit(client, group, mangoAccount, Array.from(marketContexts.values()));
});
// Loop indefinitely
while (control.isRunning) {
try {
// TODO update this in a non blocking manner
state = await refreshState(client, group, mangoAccount, marketContexts);
mangoAccount = state.mangoAccount;
// Calculate pf level values
let pfQuoteValue: number | undefined = 0;
for (const mc of Array.from(marketContexts.values())) {
const pos = mangoAccount.getPerpPositionUi(
group,
mc.market.perpMarketIndex,
);
// TODO use ftx to get mid then also combine with books from other exchanges
const midWorkaround = mc.market.uiPrice;
if (midWorkaround) {
pfQuoteValue += pos * midWorkaround;
} else {
pfQuoteValue = undefined;
console.log(
`Breaking pfQuoteValue computation, since mid is undefined for ${mc.market.name}!`,
);
break;
}
}
// Don't proceed if we don't have pfQuoteValue yet
if (pfQuoteValue === undefined) {
console.log(
`Continuing control loop, since pfQuoteValue is undefined!`,
);
continue;
}
// Update all orders on all markets
for (const mc of Array.from(marketContexts.values())) {
const ixs = await makeMarketUpdateInstructions(
client,
group,
mangoAccount,
mc,
pfQuoteValue,
);
if (ixs.length === 0) {
continue;
}
// TODO: batch ixs
const sig = await sendTransaction(
client.program.provider as AnchorProvider,
ixs,
group.addressLookupTablesList,
);
console.log(
`Orders for market updated, sig https://explorer.solana.com/tx/${sig}?cluster=${
CLUSTER == 'devnet' ? 'devnet' : ''
}`,
);
}
} catch (e) {
console.log(e);
} finally {
console.log(
`${new Date().toUTCString()} sleeping for ${control.interval / 1000}s`,
);
await new Promise((r) => setTimeout(r, control.interval));
}
}
}
async function makeMarketUpdateInstructions(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
mc: MarketContext,
pfQuoteValue: number,
): Promise<TransactionInstruction[]> {
const perpMarketIndex = mc.market.perpMarketIndex;
const perpMarket = mc.market;
const aggBid = perpMarket.uiPrice; // TODO mc.aggBid;
const aggAsk = perpMarket.uiPrice; // TODO mc.aggAsk;
if (aggBid === undefined || aggAsk === undefined) {
console.log(`No Aggregate Book for ${mc.market.name}!`);
return [];
}
const leanCoeff = mc.params.leanCoeff;
const fairValue = (aggBid + aggAsk) / 2;
const aggSpread = (aggAsk - aggBid) / fairValue;
const requoteThresh = mc.params.requoteThresh;
const equity = toUiDecimalsForQuote(mangoAccount.getEquity(group));
const sizePerc = mc.params.sizePerc;
const quoteSize = equity * sizePerc;
const size = quoteSize / fairValue;
// TODO look at event queue as well for unprocessed fills
const basePos = mangoAccount.getPerpPositionUi(group, perpMarketIndex);
const lean = (-leanCoeff * basePos) / size;
const pfQuoteLeanCoeff = params.pfQuoteLeanCoeff || 0.001; // How much to move if pf pos is equal to equity
const pfQuoteLean = (pfQuoteValue / equity) * -pfQuoteLeanCoeff;
const charge = (mc.params.charge || 0.0015) + aggSpread / 2;
const bias = mc.params.bias;
const bidPrice = fairValue * (1 - charge + lean + bias + pfQuoteLean);
const askPrice = fairValue * (1 + charge + lean + bias + pfQuoteLean);
// TODO volatility adjustment
const modelBidPrice = perpMarket.uiPriceToLots(bidPrice);
const nativeBidSize = perpMarket.uiBaseToLots(size);
const modelAskPrice = perpMarket.uiPriceToLots(askPrice);
const nativeAskSize = perpMarket.uiBaseToLots(size);
const bids = mc.bids;
const asks = mc.asks;
const bestBid = bids.best();
const bestAsk = asks.best();
const bookAdjBid =
bestAsk !== undefined
? BN.min(bestAsk.priceLots.sub(new BN(1)), modelBidPrice)
: modelBidPrice;
const bookAdjAsk =
bestBid !== undefined
? BN.max(bestBid.priceLots.add(new BN(1)), modelAskPrice)
: modelAskPrice;
// TODO use order book to requote if size has changed
// TODO
// const takeSpammers = mc.params.takeSpammers;
// const spammerCharge = mc.params.spammerCharge;
let moveOrders = false;
if (mc.lastBookUpdate >= mc.lastOrderUpdate + 2) {
// console.log(` - moveOrders - 303`);
// If mango book was updated recently, then MangoAccount was also updated
const openOrders = await mangoAccount.loadPerpOpenOrdersForMarket(
client,
group,
perpMarketIndex,
);
moveOrders = openOrders.length < 2 || openOrders.length > 2;
for (const o of openOrders) {
const refPrice = o.side === 'buy' ? bookAdjBid : bookAdjAsk;
moveOrders =
moveOrders ||
Math.abs(o.priceLots.toNumber() / refPrice.toNumber() - 1) >
requoteThresh;
}
} else {
// console.log(
// ` - moveOrders - 319, mc.lastBookUpdate ${mc.lastBookUpdate}, mc.lastOrderUpdate ${mc.lastOrderUpdate}`,
// );
// If order was updated before MangoAccount, then assume that sent order already executed
moveOrders =
moveOrders ||
Math.abs(mc.sentBidPrice / bookAdjBid.toNumber() - 1) > requoteThresh ||
Math.abs(mc.sentAskPrice / bookAdjAsk.toNumber() - 1) > requoteThresh;
}
// Start building the transaction
const instructions: TransactionInstruction[] = [
makeCheckAndSetSequenceNumberIx(
mc.sequenceAccount,
(client.program.provider as AnchorProvider).wallet.publicKey,
Date.now(),
CLUSTER,
),
];
// TODO Clear 1 lot size orders at the top of book that bad people use to manipulate the price
if (moveOrders) {
// Cancel all, requote
const cancelAllIx = await client.perpCancelAllOrdersIx(
group,
mangoAccount,
perpMarketIndex,
10,
);
const expiryTimestamp =
params.tif !== undefined ? Date.now() / 1000 + params.tif : 0;
const placeBidIx = await client.perpPlaceOrderIx(
group,
mangoAccount,
perpMarketIndex,
PerpOrderSide.bid,
// TODO fix this, native to ui to native
perpMarket.priceLotsToUi(bookAdjBid),
perpMarket.baseLotsToUi(nativeBidSize),
undefined,
Date.now(),
PerpOrderType.postOnlySlide,
expiryTimestamp,
20,
);
const placeAskIx = await client.perpPlaceOrderIx(
group,
mangoAccount,
perpMarketIndex,
PerpOrderSide.ask,
perpMarket.priceLotsToUi(bookAdjAsk),
perpMarket.baseLotsToUi(nativeAskSize),
undefined,
Date.now(),
PerpOrderType.postOnlySlide,
expiryTimestamp,
20,
);
instructions.push(cancelAllIx);
const posAsTradeSizes = basePos / size;
if (posAsTradeSizes < 15) {
instructions.push(placeBidIx);
}
if (posAsTradeSizes > -15) {
instructions.push(placeAskIx);
}
console.log(
`Requoting for market ${mc.market.name} sentBid: ${
mc.sentBidPrice
} newBid: ${bookAdjBid} sentAsk: ${
mc.sentAskPrice
} newAsk: ${bookAdjAsk} pfLean: ${(pfQuoteLean * 10000).toFixed(
1,
)} aggBid: ${aggBid} addAsk: ${aggAsk}`,
);
mc.sentBidPrice = bookAdjBid.toNumber();
mc.sentAskPrice = bookAdjAsk.toNumber();
mc.lastOrderUpdate = Date.now() / 1000;
} else {
console.log(
`Not requoting for market ${mc.market.name}. No need to move orders`,
);
}
// If instruction is only the sequence enforcement, then just send empty
if (instructions.length === 1) {
return [];
} else {
return instructions;
}
}
function startMarketMaker() {
if (control.isRunning) {
fullMarketMaker().finally(startMarketMaker);
}
}
startMarketMaker();

View File

@ -0,0 +1,16 @@
{
"interval": 1000,
"batch": 1,
"assets": {
"BTC": {
"perp": {
"sizePerc": 0.05,
"leanCoeff": 0.00025,
"bias": 0.0,
"requoteThresh": 0.0002,
"takeSpammers": true,
"spammerCharge": 2
}
}
}
}

View File

@ -0,0 +1,68 @@
import { BN } from '@project-serum/anchor';
import {
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import { createHash } from 'crypto';
export const seqEnforcerProgramIds = {
devnet: new PublicKey('FBngRHN4s5cmHagqy3Zd6xcK3zPJBeX5DixtHFbBhyCn'),
testnet: new PublicKey('FThcgpaJM8WiEbK5rw3i31Ptb8Hm4rQ27TrhfzeR1uUy'),
'mainnet-beta': new PublicKey('GDDMwNyyx8uB6zrqwBFHjLLG3TBYk2F8Az4yrQC5RzMp'),
};
export function makeInitSequenceEnforcerAccountIx(
account: PublicKey,
ownerPk: PublicKey,
bump: number,
sym: string,
cluster: string,
): TransactionInstruction {
const keys = [
{ isSigner: false, isWritable: true, pubkey: account },
{ isSigner: true, isWritable: true, pubkey: ownerPk },
{ isSigner: false, isWritable: false, pubkey: SystemProgram.programId },
];
const variant = createHash('sha256')
.update('global:initialize')
.digest()
.slice(0, 8);
const bumpData = new BN(bump).toBuffer('le', 1);
const strLen = new BN(sym.length).toBuffer('le', 4);
const symEncoded = Buffer.from(sym);
const data = Buffer.concat([variant, bumpData, strLen, symEncoded]);
return new TransactionInstruction({
keys,
data,
programId: seqEnforcerProgramIds[cluster],
});
}
export function makeCheckAndSetSequenceNumberIx(
sequenceAccount: PublicKey,
ownerPk: PublicKey,
seqNum: number,
cluster,
): TransactionInstruction {
const keys = [
{ isSigner: false, isWritable: true, pubkey: sequenceAccount },
{ isSigner: true, isWritable: false, pubkey: ownerPk },
];
const variant = createHash('sha256')
.update('global:check_and_set_sequence_number')
.digest()
.slice(0, 8);
const seqNumBuffer = new BN(seqNum).toBuffer('le', 8);
const data = Buffer.concat([variant, seqNumBuffer]);
return new TransactionInstruction({
keys,
data,
programId: seqEnforcerProgramIds[cluster],
});
}

View File

@ -0,0 +1,111 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Cluster, Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Group } from '../../accounts/group';
import { MangoAccount } from '../../accounts/mangoAccount';
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../../accounts/perp';
import { MangoClient } from '../../client';
import { MANGO_V4_ID } from '../../constants';
import { toUiDecimalsForQuote } from '../../utils';
// For easy switching between mainnet and devnet, default is mainnet
const CLUSTER: Cluster =
(process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta';
const CLUSTER_URL =
process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL;
const USER_KEYPAIR =
process.env.USER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR;
const MANGO_ACCOUNT_PK = process.env.MANGO_ACCOUNT_PK || '';
async function takeOrder(
client: MangoClient,
group: Group,
mangoAccount: MangoAccount,
perpMarket: PerpMarket,
side: PerpOrderSide,
) {
await mangoAccount.reload(client);
const size = Math.random() * 0.001;
const price =
side === PerpOrderSide.bid
? perpMarket.uiPrice * 1.01
: perpMarket.uiPrice * 0.99;
console.log(
`${perpMarket.name} taking with a ${
side === PerpOrderSide.bid ? 'bid' : 'ask'
} at price ${price.toFixed(4)} and size ${size.toFixed(6)}`,
);
const oldPosition = mangoAccount.getPerpPosition(perpMarket.perpMarketIndex);
if (oldPosition) {
console.log(
`- before base: ${perpMarket.baseLotsToUi(
oldPosition.basePositionLots,
)}, quote: ${toUiDecimalsForQuote(oldPosition.quotePositionNative)}`,
);
}
await client.perpPlaceOrder(
group,
mangoAccount,
perpMarket.perpMarketIndex,
side,
price,
size,
undefined,
Date.now(),
PerpOrderType.market,
0,
10,
);
// Sleep to see change, alternatively we could reload account with processed commitmment
await new Promise((r) => setTimeout(r, 5000));
await mangoAccount.reload(client);
const newPosition = mangoAccount.getPerpPosition(perpMarket.perpMarketIndex);
if (newPosition) {
console.log(
`- after base: ${perpMarket.baseLotsToUi(
newPosition.basePositionLots,
)}, quote: ${toUiDecimalsForQuote(newPosition.quotePositionNative)}`,
);
}
}
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(CLUSTER_URL!, options);
const user = Keypair.fromSecretKey(
Buffer.from(JSON.parse(fs.readFileSync(USER_KEYPAIR!, 'utf-8'))),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
CLUSTER,
MANGO_V4_ID[CLUSTER],
{},
'get-program-accounts',
);
// Load mango account
let mangoAccount = await client.getMangoAccountForPublicKey(
new PublicKey(MANGO_ACCOUNT_PK),
);
await mangoAccount.reload(client);
// Load group
const group = await client.getGroup(mangoAccount.group);
await group.reloadAll(client);
// Take on OB
const perpMarket = group.getPerpMarketByName('BTC-PERP');
while (true) {
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.bid);
await takeOrder(client, group, mangoAccount, perpMarket, PerpOrderSide.ask);
}
}
main();

View File

@ -1,107 +1,63 @@
import { AnchorProvider } from '@project-serum/anchor';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import {
AccountMeta,
AddressLookupTableAccount,
MessageV0,
PublicKey,
Signer,
SystemProgram,
TransactionInstruction,
VersionedTransaction,
} from '@solana/web3.js';
import BN from 'bn.js';
import { Bank, QUOTE_DECIMALS } from './accounts/bank';
import { Group } from './accounts/group';
import { I80F48 } from './accounts/I80F48';
import { MangoAccount, Serum3Orders } from './accounts/mangoAccount';
import { PerpMarket } from './accounts/perp';
import { QUOTE_DECIMALS } from './accounts/bank';
import { I80F48 } from './numbers/I80F48';
///
/// numeric helpers
///
export const U64_MAX_BN = new BN('18446744073709551615');
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
export function debugAccountMetas(ams: AccountMeta[]) {
for (const am of ams) {
console.log(
`${am.pubkey.toBase58()}, isSigner: ${am.isSigner
.toString()
.padStart(5, ' ')}, isWritable - ${am.isWritable
.toString()
.padStart(5, ' ')}`,
);
export function toNativeI80F48(uiAmount: number, decimals: number): I80F48 {
return I80F48.fromNumber(uiAmount * Math.pow(10, decimals));
}
export function toNative(uiAmount: number, decimals: number): BN {
return new BN((uiAmount * Math.pow(10, decimals)).toFixed(0));
}
export function toUiDecimals(
nativeAmount: BN | I80F48 | number,
decimals: number,
): number {
if (nativeAmount instanceof BN) {
return nativeAmount.div(new BN(Math.pow(10, decimals))).toNumber();
} else if (nativeAmount instanceof I80F48) {
return nativeAmount
.div(I80F48.fromNumber(Math.pow(10, decimals)))
.toNumber();
}
return nativeAmount / Math.pow(10, decimals);
}
export function debugHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
publicKeys: PublicKey[],
) {
const banks = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].publicKey.toBase58(),
`${banks[0].name} bank`,
]),
);
const oracles = new Map(
Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [
banks[0].oracle.toBase58(),
`${banks[0].name} oracle`,
]),
);
const serum3 = new Map(
mangoAccount.serum3Active().map((serum3: Serum3Orders) => {
const serum3Market = Array.from(
group.serum3MarketsMapByExternal.values(),
).find((serum3Market) => serum3Market.marketIndex === serum3.marketIndex);
if (!serum3Market) {
throw new Error(
`Serum3Orders for non existent market with market index ${serum3.marketIndex}`,
);
}
return [serum3.openOrders.toBase58(), `${serum3Market.name} spot oo`];
}),
);
const perps = new Map(
Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [
perpMarket.publicKey.toBase58(),
`${perpMarket.name} perp market`,
]),
);
publicKeys.map((pk) => {
if (banks.get(pk.toBase58())) {
console.log(banks.get(pk.toBase58()));
}
if (oracles.get(pk.toBase58())) {
console.log(oracles.get(pk.toBase58()));
}
if (serum3.get(pk.toBase58())) {
console.log(serum3.get(pk.toBase58()));
}
if (perps.get(pk.toBase58())) {
console.log(perps.get(pk.toBase58()));
}
});
export function toUiDecimalsForQuote(
nativeAmount: BN | I80F48 | number,
): number {
return toUiDecimals(nativeAmount, QUOTE_DECIMALS);
}
export async function findOrCreate<T>(
entityName: string,
findMethod: (...x: any) => any,
findArgs: any[],
createMethod: (...x: any) => any,
createArgs: any[],
): Promise<T> {
let many: T[] = await findMethod(...findArgs);
let one: T;
if (many.length > 0) {
one = many[0];
return one;
}
await createMethod(...createArgs);
many = await findMethod(...findArgs);
one = many[0];
return one;
export function toUiI80F48(nativeAmount: I80F48, decimals: number): I80F48 {
return nativeAmount.div(I80F48.fromNumber(Math.pow(10, decimals)));
}
///
/// web3js extensions
///
/**
* Get the address of the associated token account for a given mint and owner
*
@ -121,7 +77,7 @@ export async function getAssociatedTokenAddress(
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
): Promise<PublicKey> {
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer()))
throw new Error('TokenOwnerOffCurve');
throw new Error('TokenOwnerOffCurve!');
const [address] = await PublicKey.findProgramAddress(
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],
@ -155,36 +111,33 @@ export async function createAssociatedTokenAccountIdempotentInstruction(
});
}
export function toNative(uiAmount: number, decimals: number): I80F48 {
return I80F48.fromNumber(uiAmount).mul(
I80F48.fromNumber(Math.pow(10, decimals)),
);
export async function buildVersionedTx(
provider: AnchorProvider,
ix: TransactionInstruction[],
additionalSigners: Signer[] = [],
alts: AddressLookupTableAccount[] = [],
): Promise<VersionedTransaction> {
const message = MessageV0.compile({
payerKey: (provider as AnchorProvider).wallet.publicKey,
instructions: ix,
recentBlockhash: (await provider.connection.getLatestBlockhash()).blockhash,
addressLookupTableAccounts: alts,
});
const vTx = new VersionedTransaction(message);
// TODO: remove use of any when possible in future
vTx.sign([
((provider as AnchorProvider).wallet as any).payer as Signer,
...additionalSigners,
]);
return vTx;
}
export function toNativeDecimals(amount: number, decimals: number): BN {
return new BN(Math.trunc(amount * Math.pow(10, decimals)));
}
///
/// ts extension
///
export function toUiDecimals(
amount: I80F48 | number,
decimals: number,
): number {
amount = amount instanceof I80F48 ? amount.toNumber() : amount;
return amount / Math.pow(10, decimals);
}
export function toUiDecimalsForQuote(amount: I80F48 | number): number {
amount = amount instanceof I80F48 ? amount.toNumber() : amount;
return amount / Math.pow(10, QUOTE_DECIMALS);
}
export function toU64(amount: number, decimals: number): BN {
const bn = toNativeDecimals(amount, decimals).toString();
console.log('bn', bn);
return new BN(bn);
}
export function nativeI80F48ToUi(amount: I80F48, decimals: number): I80F48 {
return amount.div(I80F48.fromNumber(Math.pow(10, decimals)));
// https://stackoverflow.com/questions/70261755/user-defined-type-guard-function-and-type-narrowing-to-more-specific-type/70262876#70262876
export declare abstract class As<Tag extends keyof never> {
private static readonly $as$: unique symbol;
private [As.$as$]: Record<Tag, true>;
}

View File

@ -1,42 +0,0 @@
import {
simulateTransaction,
SuccessfulTxSimulationResponse,
} from '@project-serum/anchor/dist/cjs/utils/rpc';
import {
Signer,
PublicKey,
Transaction,
Commitment,
SimulatedTransactionResponse,
} from '@solana/web3.js';
class SimulateError extends Error {
constructor(
readonly simulationResponse: SimulatedTransactionResponse,
message?: string,
) {
super(message);
}
}
export async function simulate(
tx: Transaction,
signers?: Signer[],
commitment?: Commitment,
includeAccounts?: boolean | PublicKey[],
): Promise<SuccessfulTxSimulationResponse> {
tx.feePayer = this.wallet.publicKey;
tx.recentBlockhash = (
await this.connection.getLatestBlockhash(
commitment ?? this.connection.commitment,
)
).blockhash;
const result = await simulateTransaction(this.connection, tx);
if (result.value.err) {
throw new SimulateError(result.value);
}
return result.value;
}

View File

@ -1,27 +1,34 @@
import { AnchorProvider } from '@project-serum/anchor';
import { Transaction } from '@solana/web3.js';
import {
AddressLookupTableAccount,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
export async function sendTransaction(
provider: AnchorProvider,
transaction: Transaction,
ixs: TransactionInstruction[],
alts: AddressLookupTableAccount[],
opts: any = {},
) {
): Promise<string> {
const connection = provider.connection;
const payer = provider.wallet;
const latestBlockhash = await connection.getLatestBlockhash(
opts.preflightCommitment,
);
transaction.recentBlockhash = latestBlockhash.blockhash;
transaction.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
transaction.feePayer = payer.publicKey;
const payer = (provider as AnchorProvider).wallet;
// const tx = await buildVersionedTx(provider, ixs, opts.additionalSigners, alts);
const tx = new Transaction();
tx.recentBlockhash = latestBlockhash.blockhash;
tx.lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
tx.feePayer = payer.publicKey;
tx.add(...ixs);
if (opts.additionalSigners?.length > 0) {
transaction.partialSign(...opts.additionalSigners);
tx.partialSign(...opts.additionalSigners);
}
await payer.signTransaction(tx);
await payer.signTransaction(transaction);
const rawTransaction = transaction.serialize();
const signature = await connection.sendRawTransaction(rawTransaction, {
const signature = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: true,
});
@ -35,21 +42,23 @@ export async function sendTransaction(
let status: any;
if (
transaction.recentBlockhash != null &&
transaction.lastValidBlockHeight != null
latestBlockhash.blockhash != null &&
latestBlockhash.lastValidBlockHeight != null
) {
// TODO: tyler, can we remove these?
console.log('confirming via blockhash');
status = (
await connection.confirmTransaction(
{
signature: signature,
blockhash: transaction.recentBlockhash,
lastValidBlockHeight: transaction.lastValidBlockHeight,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
},
'processed',
)
).value;
} else {
// TODO: tyler, can we remove these?
console.log('confirming via timeout');
status = (await connection.confirmTransaction(signature, 'processed'))
.value;

163
yarn.lock
View File

@ -23,13 +23,20 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
"@babel/runtime@^7.10.5", "@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2":
"@babel/runtime@^7.10.5":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.17.2":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259"
integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA==
dependencies:
regenerator-runtime "^0.13.4"
"@cykura/sdk-core@npm:@jup-ag/cykura-sdk-core@0.1.8", "@jup-ag/cykura-sdk-core@0.1.8":
version "0.1.8"
resolved "https://registry.npmjs.org/@jup-ag/cykura-sdk-core/-/cykura-sdk-core-0.1.8.tgz"
@ -192,6 +199,21 @@
resolved "https://registry.npmjs.org/@mercurial-finance/optimist/-/optimist-0.1.4.tgz"
integrity sha512-m8QuyPx9j7fGd2grw0mD5WcYtBb8l7+OQI5aHdeIlxPg3QoPrbSdCHyFOuipYbvB0EY5YDbOmyeFwiTcBkBBSw==
"@noble/ed25519@^1.7.0":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.1.tgz#6899660f6fbb97798a6fbd227227c4589a454724"
integrity sha512-Rk4SkJFaXZiznFyC/t77Q0NKS4FL7TLJJsVG2V2oiEq3kJVeTdxysEe/yRWSpnWMe808XRDJ+VFh5pt/FN5plw==
"@noble/hashes@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183"
integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==
"@noble/secp256k1@^1.6.3":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.0.tgz#d15357f7c227e751d90aa06b05a0e5cf993ba8c1"
integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@ -514,7 +536,7 @@
"@solana/buffer-layout@^4.0.0":
version "4.0.0"
resolved "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz"
resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz#75b1b11adc487234821c81dfae3119b73a5fd734"
integrity sha512-lR0EMP2HC3+Mxwd4YcnZb0smnaDw7Bl2IQWZiTevRH5ZZBZn6VRWn3/92E3qdU4SSImJkA6IDHawOHAnx/qUvQ==
dependencies:
buffer "~6.0.3"
@ -583,7 +605,28 @@
superstruct "^0.14.2"
tweetnacl "^1.0.0"
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0", "@solana/web3.js@^1.36.0":
"@solana/web3.js@1.63.1", "@solana/web3.js@^1.36.0", "@solana/web3.js@^1.63.1":
version "1.63.1"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.63.1.tgz#88a19a17f5f4aada73ad70a94044c1067cab2b4d"
integrity sha512-wgEdGVK5FTS2zENxbcGSvKpGZ0jDS6BUdGu8Gn6ns0CzgJkK83u4ip3THSnBPEQ5i/jrqukg998BwV1H67+qiQ==
dependencies:
"@babel/runtime" "^7.12.5"
"@noble/ed25519" "^1.7.0"
"@noble/hashes" "^1.1.2"
"@noble/secp256k1" "^1.6.3"
"@solana/buffer-layout" "^4.0.0"
bigint-buffer "^1.1.5"
bn.js "^5.0.0"
borsh "^0.7.0"
bs58 "^4.0.1"
buffer "6.0.1"
fast-stable-stringify "^1.0.0"
jayson "^3.4.4"
node-fetch "2"
rpc-websockets "^7.5.0"
superstruct "^0.14.2"
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0":
version "1.51.0"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.51.0.tgz#51b28b5332f1f03ea25bea6c229869e33aebc507"
integrity sha512-kf2xHKYETKiIY4DCt8Os7VDPoY5oyOMJ3UWRcLeOVEFXwiv2ClNmSg0EG3BqV3I4TwOojGtmVqk0ubCkUnpmfg==
@ -702,20 +745,11 @@
"@types/connect@^3.4.33":
version "3.4.35"
resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==
dependencies:
"@types/node" "*"
"@types/express-serve-static-core@^4.17.9":
version "4.17.30"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.30.tgz#0f2f99617fa8f9696170c46152ccf7500b34ac04"
integrity sha512-gstzbTWro2/nFed1WXtf+TtrpwxH7Ggs4RLYTLbeVgIkUQOI3WG/JKjgeOU1zXDvezllupjrf8OPIdvTbIaVOQ==
dependencies:
"@types/node" "*"
"@types/qs" "*"
"@types/range-parser" "*"
"@types/json-schema@^7.0.9":
version "7.0.11"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz"
@ -726,11 +760,6 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.159":
version "4.14.182"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz"
integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==
"@types/long@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a"
@ -742,9 +771,9 @@
integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==
"@types/node@*":
version "18.7.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.1.tgz#352bee64f93117d867d05f7406642a52685cbca6"
integrity sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ==
version "18.7.22"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.7.22.tgz#76f7401362ad63d9d7eefa7dcdfa5fcd9baddff3"
integrity sha512-TsmoXYd4zrkkKjJB0URF/mTIKPl+kVcbqClB2F/ykU7vil1BfWZVndOnpEIozPv4fURD28gyPFeIkW2G+KXOvw==
"@types/node@>=13.7.0":
version "18.7.6"
@ -768,16 +797,6 @@
dependencies:
"@types/retry" "*"
"@types/qs@*":
version "6.9.7"
resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/range-parser@*":
version "1.2.4"
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/retry@*", "@types/retry@^0.12.2":
version "0.12.2"
resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz"
@ -785,7 +804,7 @@
"@types/ws@^7.4.4":
version "7.4.7"
resolved "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.4.7.tgz#f7c390a36f7a0679aa69de2d501319f4f8d9b702"
integrity sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==
dependencies:
"@types/node" "*"
@ -888,7 +907,7 @@
JSONStream@^1.3.5:
version "1.3.5"
resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==
dependencies:
jsonparse "^1.2.0"
@ -1041,7 +1060,7 @@ base-x@^4.0.0:
base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
big.js@^5.2.2:
@ -1061,7 +1080,7 @@ big.js@^6.2.0:
bigint-buffer@^1.1.5:
version "1.1.5"
resolved "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz"
resolved "https://registry.yarnpkg.com/bigint-buffer/-/bigint-buffer-1.1.5.tgz#d038f31c8e4534c1f8d0015209bf34b4fa6dd442"
integrity sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==
dependencies:
bindings "^1.3.0"
@ -1078,7 +1097,7 @@ binary-extensions@^2.0.0:
bindings@^1.3.0:
version "1.5.0"
resolved "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==
dependencies:
file-uri-to-path "1.0.0"
@ -1120,7 +1139,7 @@ borsh@^0.4.0:
borsh@^0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz"
resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a"
integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==
dependencies:
bn.js "^5.2.0"
@ -1161,7 +1180,7 @@ browser-stdout@1.3.1:
bs58@^4.0.0, bs58@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==
dependencies:
base-x "^3.0.2"
@ -1185,7 +1204,7 @@ buffer-layout@^1.2.0, buffer-layout@^1.2.2:
buffer@6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.1.tgz"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.1.tgz#3cbea8c1463e5a0779e30b66d4c88c6ffa182ac2"
integrity sha512-rVAXBwEcEoYtxnHSO5iWyhzV/O1WMtkUYWlfdLS7FjU4PnSJJHEfHXi/uHPI5EwltmOA794gN3bm3/pzuctWjQ==
dependencies:
base64-js "^1.3.1"
@ -1193,7 +1212,7 @@ buffer@6.0.1:
buffer@6.0.3, buffer@^6.0.1, buffer@~6.0.3:
version "6.0.3"
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
@ -1209,7 +1228,7 @@ buffer@^5.4.3:
bufferutil@^4.0.1:
version "4.0.6"
resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz"
resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433"
integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==
dependencies:
node-gyp-build "^4.3.0"
@ -1332,7 +1351,7 @@ color-name@~1.1.4:
commander@^2.20.3:
version "2.20.3"
resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
concat-map@0.0.1:
@ -1434,7 +1453,7 @@ define-properties@^1.1.3, define-properties@^1.1.4:
delay@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz"
resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d"
integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
diff@5.0.0:
@ -1559,12 +1578,12 @@ es6-object-assign@^1.1.0:
es6-promise@^4.0.3:
version "4.2.8"
resolved "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
es6-promisify@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz"
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
integrity sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==
dependencies:
es6-promise "^4.0.3"
@ -1730,7 +1749,7 @@ event-stream@=3.3.4:
eventemitter3@^4.0.7:
version "4.0.7"
resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
execa@5.1.1:
@ -1750,7 +1769,7 @@ execa@5.1.1:
eyes@^0.1.8:
version "0.1.8"
resolved "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz"
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
@ -1781,7 +1800,7 @@ fast-levenshtein@^2.0.6:
fast-stable-stringify@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz"
resolved "https://registry.yarnpkg.com/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz#5c5543462b22aeeefd36d05b34e51c78cb86d313"
integrity sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==
fastq@^1.6.0:
@ -1808,7 +1827,7 @@ file-entry-cache@^6.0.1:
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
fill-range@^7.0.1:
@ -2066,7 +2085,7 @@ human-signals@^2.1.0:
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
ignore@^4.0.6:
@ -2274,17 +2293,15 @@ isexe@^2.0.0:
isomorphic-ws@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz"
resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc"
integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==
jayson@^3.4.4:
version "3.6.6"
resolved "https://registry.npmjs.org/jayson/-/jayson-3.6.6.tgz"
integrity sha512-f71uvrAWTtrwoww6MKcl9phQTC+56AopLyEenWvKVAIMz+q0oVGj6tenLZ7Z6UiPBkJtKLj4kt0tACllFQruGQ==
version "3.7.0"
resolved "https://registry.yarnpkg.com/jayson/-/jayson-3.7.0.tgz#b735b12d06d348639ae8230d7a1e2916cb078f25"
integrity sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==
dependencies:
"@types/connect" "^3.4.33"
"@types/express-serve-static-core" "^4.17.9"
"@types/lodash" "^4.14.159"
"@types/node" "^12.12.54"
"@types/ws" "^7.4.4"
JSONStream "^1.3.5"
@ -2361,7 +2378,7 @@ json-stable-stringify-without-jsonify@^1.0.1:
json-stringify-safe@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
json5@^1.0.1:
@ -2378,7 +2395,7 @@ jsonc-parser@^3.0.0:
jsonparse@^1.2.0:
version "1.3.1"
resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz"
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
lazy-ass@1.6.0:
@ -2418,7 +2435,7 @@ lodash.truncate@^4.4.2:
lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
log-symbols@4.1.0:
@ -2616,7 +2633,7 @@ node-domexception@^1.0.0:
node-fetch@2, node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
@ -2846,7 +2863,7 @@ readdirp@~3.6.0:
regenerator-runtime@^0.13.4:
version "0.13.9"
resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regexp.prototype.flags@^1.4.3:
@ -2902,7 +2919,7 @@ rimraf@^3.0.2:
rpc-websockets@^7.4.2, rpc-websockets@^7.5.0:
version "7.5.0"
resolved "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.5.0.tgz"
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.0.tgz#bbeb87572e66703ff151e50af1658f98098e2748"
integrity sha512-9tIRi1uZGy7YmDjErf1Ax3wtqdSSLIlnmL5OtOzgd5eqPKbsPpwDP5whUDO2LQay3Xp0CcHlcNSGzacNRluBaQ==
dependencies:
"@babel/runtime" "^7.17.2"
@ -3108,7 +3125,7 @@ strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.
superstruct@^0.14.2:
version "0.14.2"
resolved "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz"
resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b"
integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==
superstruct@^0.15.2, superstruct@^0.15.4:
@ -3150,7 +3167,7 @@ table@^6.0.9:
text-encoding-utf-8@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz"
resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13"
integrity sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==
text-table@^0.2.0:
@ -3160,7 +3177,7 @@ text-table@^0.2.0:
through@2, "through@>=2.2.7 <3", through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
tiny-invariant@^1.1.0, tiny-invariant@^1.2.0, tiny-invariant@~1.2.0:
@ -3187,7 +3204,7 @@ toml@^3.0.0:
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
traverse-chain@~0.1.0:
@ -3314,7 +3331,7 @@ uri-js@^4.2.2:
utf-8-validate@^5.0.2:
version "5.0.9"
resolved "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.9.tgz"
resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.9.tgz#ba16a822fbeedff1a58918f2a6a6b36387493ea3"
integrity sha512-Yek7dAy0v3Kl0orwMlvi7TPtiCNrdfHNd7Gcc/pLq4BLXqfAmd0J7OWMizUQnTTJsyjKn02mU7anqwfmUP4J8Q==
dependencies:
node-gyp-build "^4.3.0"
@ -3333,7 +3350,7 @@ util@^0.12.0:
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3:
@ -3369,7 +3386,7 @@ web-streams-polyfill@^3.0.3:
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
webidl-conversions@^5.0.0:
@ -3388,7 +3405,7 @@ whatwg-url-without-unicode@8.0.0-3:
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
@ -3454,9 +3471,9 @@ ws@^7.4.5:
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@^8.5.0:
version "8.8.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
version "8.9.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"
integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==
y18n@^5.0.5:
version "5.0.8"