Merge remote-tracking branch 'origin/dev' into main
This commit is contained in:
commit
73039e1b39
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<_>>())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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<_>>())
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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>>();
|
||||
|
||||
//
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<()> {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[],
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
@ -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 {
|
||||
|
|
|
@ -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}!`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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)}`);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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();
|
|
@ -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();
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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],
|
||||
});
|
||||
}
|
|
@ -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();
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
163
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue