From dff3f7cd8c29a6503f4ea89dff1b7149060527e8 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 4 Jul 2022 12:29:35 +0200 Subject: [PATCH] client functions via program simulation Signed-off-by: microwavedcola1 --- programs/mango-v4/src/events.rs | 30 +++ .../src/instructions/compute_account_data.rs | 32 +++ .../src/instructions/compute_health.rs | 22 -- programs/mango-v4/src/instructions/mod.rs | 4 +- programs/mango-v4/src/lib.rs | 9 +- programs/mango-v4/src/state/equity.rs | 64 ++++++ programs/mango-v4/src/state/health.rs | 47 ++-- programs/mango-v4/src/state/mod.rs | 2 + .../tests/program_test/mango_client.rs | 12 +- programs/mango-v4/tests/test_basic.rs | 2 +- ts/client/src/accounts/bank.ts | 41 +++- ts/client/src/accounts/group.ts | 49 ++++- ts/client/src/accounts/mangoAccount.ts | 185 +++++++++++++++- ts/client/src/client.ts | 48 +++-- ts/client/src/mango_v4.ts | 200 +++++++++++++++--- ts/client/src/scripts/example1-admin.ts | 2 +- ts/client/src/scripts/example1-flash-loan.ts | 4 +- .../scripts/example1-user-close-account.ts | 4 +- ts/client/src/scripts/example1-user.ts | 84 ++++++-- .../src/scripts/mb-example1-close-account.ts | 4 +- ts/client/src/scripts/mb-flash-loan-3.ts | 2 +- .../example1-create-liquidation-candidate.ts | 6 +- ts/client/src/utils.ts | 7 +- 23 files changed, 713 insertions(+), 147 deletions(-) create mode 100644 programs/mango-v4/src/events.rs create mode 100644 programs/mango-v4/src/instructions/compute_account_data.rs delete mode 100644 programs/mango-v4/src/instructions/compute_health.rs create mode 100644 programs/mango-v4/src/state/equity.rs diff --git a/programs/mango-v4/src/events.rs b/programs/mango-v4/src/events.rs new file mode 100644 index 000000000..21a43c41b --- /dev/null +++ b/programs/mango-v4/src/events.rs @@ -0,0 +1,30 @@ +use anchor_lang::prelude::*; +use fixed::types::I80F48; + +use crate::state::{PerpMarketIndex, TokenIndex}; + +#[event] +#[derive(Debug)] +pub struct MangoAccountData { + pub init_health: I80F48, + pub maint_health: I80F48, + pub equity: Equity, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Debug)] +pub struct Equity { + pub tokens: Vec, + pub perps: Vec, +} + +#[derive(AnchorDeserialize, AnchorSerialize, Debug)] +pub struct TokenEquity { + pub token_index: TokenIndex, + pub value: I80F48, // in native quote +} + +#[derive(AnchorDeserialize, AnchorSerialize, Debug)] +pub struct PerpEquity { + pub perp_market_index: PerpMarketIndex, + value: I80F48, // in native quote +} diff --git a/programs/mango-v4/src/instructions/compute_account_data.rs b/programs/mango-v4/src/instructions/compute_account_data.rs new file mode 100644 index 000000000..a532d23ad --- /dev/null +++ b/programs/mango-v4/src/instructions/compute_account_data.rs @@ -0,0 +1,32 @@ +use crate::{events::MangoAccountData, state::*}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ComputeAccountData<'info> { + pub group: AccountLoader<'info, Group>, + + #[account( + has_one = group, + )] + pub account: AccountLoader<'info, MangoAccount>, +} + +pub fn compute_account_data(ctx: Context) -> Result<()> { + let group_pk = ctx.accounts.group.key(); + let account = ctx.accounts.account.load()?; + + let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group_pk)?; + + let init_health = compute_health(&account, HealthType::Init, &account_retriever)?; + let maint_health = compute_health(&account, HealthType::Maint, &account_retriever)?; + + let equity = compute_equity(&account, &account_retriever)?; + + emit!(MangoAccountData { + init_health, + maint_health, + equity + }); + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/compute_health.rs b/programs/mango-v4/src/instructions/compute_health.rs deleted file mode 100644 index 05e7e8567..000000000 --- a/programs/mango-v4/src/instructions/compute_health.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::state::*; -use anchor_lang::prelude::*; -use fixed::types::I80F48; - -#[derive(Accounts)] -pub struct ComputeHealth<'info> { - pub group: AccountLoader<'info, Group>, - - #[account( - has_one = group, - )] - pub account: AccountLoader<'info, MangoAccount>, -} - -pub fn compute_health(ctx: Context, health_type: HealthType) -> Result { - let account = ctx.accounts.account.load()?; - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account)?; - let health = crate::state::compute_health(&account, health_type, &retriever)?; - msg!("health: {}", health); - - Ok(health) -} diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index e7f170380..434d63f3a 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -2,7 +2,7 @@ pub use benchmark::*; pub use close_account::*; pub use close_group::*; pub use close_stub_oracle::*; -pub use compute_health::*; +pub use compute_account_data::*; pub use create_account::*; pub use create_group::*; pub use create_stub_oracle::*; @@ -40,7 +40,7 @@ mod benchmark; mod close_account; mod close_group; mod close_stub_oracle; -mod compute_health; +mod compute_account_data; mod create_account; mod create_group; mod create_stub_oracle; diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 90dfc22ab..709232d43 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -12,15 +12,14 @@ use instructions::*; pub mod accounts_zerocopy; pub mod address_lookup_table; pub mod error; +pub mod events; pub mod instructions; pub mod logs; mod serum3_cpi; pub mod state; pub mod types; -use state::{ - HealthType, OracleConfig, OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex, -}; +use state::{OracleConfig, OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex}; declare_id!("m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD"); @@ -383,8 +382,8 @@ pub mod mango_v4 { // resolve_banktruptcy - pub fn compute_health(ctx: Context, health_type: HealthType) -> Result { - instructions::compute_health(ctx, health_type) + pub fn compute_account_data(ctx: Context) -> Result<()> { + instructions::compute_account_data(ctx) } /// diff --git a/programs/mango-v4/src/state/equity.rs b/programs/mango-v4/src/state/equity.rs new file mode 100644 index 000000000..c92802176 --- /dev/null +++ b/programs/mango-v4/src/state/equity.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; + +use anchor_lang::prelude::*; +use checked_math as cm; +use fixed::types::I80F48; + +use crate::events::{Equity, TokenEquity}; + +use super::{MangoAccount, ScanningAccountRetriever}; + +pub fn compute_equity( + account: &MangoAccount, + retriever: &ScanningAccountRetriever, +) -> Result { + let mut token_equity_map = HashMap::new(); + + // token contributions + for (_i, position) in account.tokens.iter_active().enumerate() { + let (bank, oracle_price) = retriever.scanned_bank_and_oracle(position.token_index)?; + // converts the token value to the basis token value for health computations + // TODO: health basis token == USDC? + let native = position.native(bank); + token_equity_map.insert(bank.token_index, native * oracle_price); + } + + // token contributions from Serum3 + for (_i, serum_account) in account.serum3.iter_active().enumerate() { + let oo = retriever.scanned_serum_oo(&serum_account.open_orders)?; + + // note base token value + let (_bank, oracle_price) = + retriever.scanned_bank_and_oracle(serum_account.base_token_index)?; + let accumulated_equity = token_equity_map + .get(&serum_account.base_token_index) + .unwrap_or(&I80F48::ZERO); + let native_coin_total_i80f48 = + I80F48::from_num(oo.native_coin_total + oo.referrer_rebates_accrued); + let new_equity = cm!(accumulated_equity + native_coin_total_i80f48 * oracle_price); + token_equity_map.insert(serum_account.base_token_index, new_equity); + + // note quote token value + let (_bank, oracle_price) = + retriever.scanned_bank_and_oracle(serum_account.quote_token_index)?; + let accumulated_equity = token_equity_map + .get(&serum_account.quote_token_index) + .unwrap_or(&I80F48::ZERO); + let native_pc_total_i80f48 = I80F48::from_num(oo.native_pc_total); + let new_equity = cm!(accumulated_equity + native_pc_total_i80f48 * oracle_price); + token_equity_map.insert(serum_account.quote_token_index, new_equity); + } + + let tokens = token_equity_map + .iter() + .map(|tuple| TokenEquity { + token_index: *tuple.0, + value: *tuple.1, + }) + .collect::>(); + + // TODO: perp contributions + let perps = Vec::new(); + + Ok(Equity { tokens, perps }) +} diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 196b8eaec..368c782eb 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -1,6 +1,8 @@ use anchor_lang::prelude::*; + use fixed::types::I80F48; use serum_dex::state::OpenOrders; + use std::collections::HashMap; use crate::accounts_zerocopy::*; @@ -244,15 +246,8 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { Ok((bank1, bank2, price1, price2)) } } -} -impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> { - fn bank_and_oracle( - &self, - _group: &Pubkey, - _account_index: usize, - token_index: TokenIndex, - ) -> Result<(&Bank, I80F48)> { + pub fn scanned_bank_and_oracle(&self, token_index: TokenIndex) -> Result<(&Bank, I80F48)> { let index = self.bank_index(token_index)?; let bank = self.ais[index].load_fully_unchecked::()?; let oracle = &self.ais[cm!(self.n_banks() + index)]; @@ -263,22 +258,41 @@ impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> { )) } + pub fn scanned_perp_market(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpMarket> { + let index = self.perp_market_index(perp_market_index)?; + self.ais[index].load_fully_unchecked::() + } + + pub fn scanned_serum_oo(&self, key: &Pubkey) -> Result<&OpenOrders> { + let oo = self.ais[self.begin_serum3()..] + .iter() + .find(|ai| ai.key == key) + .ok_or_else(|| error!(MangoError::SomeError))?; + serum3_cpi::load_open_orders(oo) + } +} + +impl<'a, 'info> AccountRetriever for ScanningAccountRetriever<'a, 'info> { + fn bank_and_oracle( + &self, + _group: &Pubkey, + _account_index: usize, + token_index: TokenIndex, + ) -> Result<(&Bank, I80F48)> { + self.scanned_bank_and_oracle(token_index) + } + fn perp_market( &self, _group: &Pubkey, _account_index: usize, perp_market_index: PerpMarketIndex, ) -> Result<&PerpMarket> { - let index = self.perp_market_index(perp_market_index)?; - self.ais[index].load_fully_unchecked::() + self.scanned_perp_market(perp_market_index) } fn serum_oo(&self, _account_index: usize, key: &Pubkey) -> Result<&OpenOrders> { - let oo = self.ais[self.begin_serum3()..] - .iter() - .find(|ai| ai.key == key) - .ok_or_else(|| error!(MangoError::SomeError))?; - serum3_cpi::load_open_orders(oo) + self.scanned_serum_oo(key) } } @@ -439,6 +453,7 @@ impl PerpInfo { (HealthType::Maint, true) => self.maint_liab_weight, (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) @@ -569,6 +584,7 @@ pub fn new_health_cache( }); } + // TODO: also account for perp funding in health // health contribution from perp accounts let mut perp_infos = Vec::with_capacity(account.perps.iter_active_accounts().count()); for (i, perp_account) in account.perps.iter_active_accounts().enumerate() { @@ -576,6 +592,7 @@ pub fn new_health_cache( // find the TokenInfos for the market's base and quote tokens let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?; + // TODO: base_index could be unset let base_info = &token_infos[base_index]; let base_lot_size = I80F48::from(perp_market.base_lot_size); diff --git a/programs/mango-v4/src/state/mod.rs b/programs/mango-v4/src/state/mod.rs index b7f1531bd..2ac840853 100644 --- a/programs/mango-v4/src/state/mod.rs +++ b/programs/mango-v4/src/state/mod.rs @@ -1,4 +1,5 @@ pub use bank::*; +pub use equity::*; pub use group::*; pub use health::*; pub use mango_account::*; @@ -9,6 +10,7 @@ pub use perp_market::*; pub use serum3_market::*; mod bank; +mod equity; mod group; mod health; mod mango_account; diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 94ef1fd9f..51ea81346 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -2423,22 +2423,20 @@ impl ClientInstruction for UpdateIndexInstruction { } } -pub struct ComputeHealthInstruction { +pub struct ComputeAccountDataInstruction { pub account: Pubkey, pub health_type: HealthType, } #[async_trait::async_trait(?Send)] -impl ClientInstruction for ComputeHealthInstruction { - type Accounts = mango_v4::accounts::ComputeHealth; - type Instruction = mango_v4::instruction::ComputeHealth; +impl ClientInstruction for ComputeAccountDataInstruction { + type Accounts = mango_v4::accounts::ComputeAccountData; + type Instruction = mango_v4::instruction::ComputeAccountData; 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 { - health_type: self.health_type, - }; + let instruction = Self::Instruction {}; let account: MangoAccount = account_loader.load(&self.account).await.unwrap(); diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 589e7ad56..22fdc57a7 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -87,7 +87,7 @@ async fn test_basic() -> Result<(), TransportError> { // send_tx( solana, - ComputeHealthInstruction { + ComputeAccountDataInstruction { account, health_type: HealthType::Init, }, diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 04cb57aba..29a6f15c2 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -1,6 +1,5 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; -import { PythHttpClient } from '@pythnetwork/client'; import { PublicKey } from '@solana/web3.js'; import { nativeI80F48ToUi } from '../utils'; import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48'; @@ -22,7 +21,12 @@ export class Bank { public rate1: I80F48; public util0: I80F48; public util1: I80F48; - public price: number; + public price: I80F48; + public initAssetWeight: I80F48; + public maintAssetWeight: I80F48; + public initLiabWeight: I80F48; + public maintLiabWeight: I80F48; + public liquidationFee: I80F48; static from( publicKey: PublicKey, @@ -128,6 +132,11 @@ export class Bank { this.rate0 = I80F48.from(rate0); this.util1 = I80F48.from(util1); this.rate1 = I80F48.from(rate1); + this.maintAssetWeight = I80F48.from(maintAssetWeight); + this.initAssetWeight = I80F48.from(initAssetWeight); + this.maintLiabWeight = I80F48.from(maintLiabWeight); + this.initLiabWeight = I80F48.from(initLiabWeight); + this.liquidationFee = I80F48.from(liquidationFee); this.price = undefined; } @@ -203,13 +212,6 @@ export class Bank { const utilization = totalBorrows.div(totalDeposits); return utilization.mul(borrowRate); } - - async getOraclePrice(connection) { - const pythClient = new PythHttpClient(connection, this.oracle); - const data = await pythClient.getData(); - - return data.productPrice; - } } export class MintInfo { @@ -246,10 +248,27 @@ export class MintInfo { public tokenIndex: number, ) {} - public firstBank() { + public firstBank(): PublicKey { return this.banks[0]; } - public firstVault() { + public firstVault(): PublicKey { return this.vaults[0]; } + + toString(): string { + let res = + 'mint ' + + this.mint.toBase58() + + '\n oracle ' + + this.oracle.toBase58() + + '\n banks ' + + this.banks + .filter((pk) => pk.toBase58() !== PublicKey.default.toBase58()) + .toString() + + '\n vaults ' + + this.vaults + .filter((pk) => pk.toBase58() !== PublicKey.default.toBase58()) + .toString(); + return res; + } } diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 1202023ff..24d3ea2dc 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -1,9 +1,11 @@ import { Market } from '@project-serum/serum'; +import { parsePriceData, PriceData } from '@pythnetwork/client'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; import { Id } from '../ids'; import { Bank, MintInfo } from './bank'; +import { I80F48, ONE_I80F48 } from './I80F48'; import { PerpMarket } from './perp'; import { Serum3Market } from './serum3'; @@ -21,6 +23,7 @@ export class Group { new Map(), new Map(), new Map(), + new Map(), ); } @@ -33,6 +36,7 @@ export class Group { public serum3MarketExternalsMap: Map, public perpMarketsMap: Map, public mintInfosMap: Map, + public oraclesMap: Map, ) {} public findBank(tokenIndex: number): Bank | undefined { @@ -61,8 +65,13 @@ export class Group { this.reloadSerum3Markets(client, ids), this.reloadPerpMarkets(client, ids), ]); - // requires reloadSerum3Markets to have finished loading - await this.reloadSerum3ExternalMarkets(client, ids); + + await Promise.all([ + // requires reloadBanks to have finished loading + this.reloadBankPrices(client, ids), + // requires reloadSerum3Markets to have finished loading + this.reloadSerum3ExternalMarkets(client, ids), + ]); // console.timeEnd('group.reload'); } @@ -80,7 +89,6 @@ export class Group { } this.banksMap = new Map(banks.map((bank) => [bank.name, bank])); - client.getPricesForGroup(this); } public async reloadMintInfos(client: MangoClient, ids?: Id) { @@ -159,4 +167,39 @@ export class Group { perpMarkets.map((perpMarket) => [perpMarket.name, perpMarket]), ); } + + public async reloadBankPrices(client: MangoClient, ids?: Id): Promise { + const banks = Array.from(this?.banksMap, ([, value]) => value); + const oracles = banks.map((b) => b.oracle); + console.log(oracles.toString()); + const prices = + await client.program.provider.connection.getMultipleAccountsInfo(oracles); + + for (const [index, price] of prices.entries()) { + if (banks[index].name === 'USDC') { + banks[index].price = ONE_I80F48; + } else { + banks[index].price = I80F48.fromNumber( + parsePriceData(price.data).previousPrice, + ); + } + } + } + + toString(): string { + let res = 'Group\n'; + res = res + ' pk: ' + this.publicKey.toString(); + + res = + res + + '\n mintInfos:' + + Array.from(this.mintInfosMap.entries()) + .map( + (mintInfoTuple) => + ' \n' + mintInfoTuple[0] + ') ' + mintInfoTuple[1].toString(), + ) + .join(', '); + + return res; + } } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index c248c898f..0cd0e1d67 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -5,7 +5,7 @@ import { MangoClient } from '../client'; import { nativeI80F48ToUi } from '../utils'; import { Bank } from './bank'; import { Group } from './group'; -import { I80F48, I80F48Dto, ZERO_I80F48 } from './I80F48'; +import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; export class MangoAccount { public tokens: TokenPosition[]; public serum3: Serum3Orders[]; @@ -43,6 +43,7 @@ export class MangoAccount { obj.accountNum, obj.bump, obj.reserved, + {}, ); } @@ -60,6 +61,7 @@ export class MangoAccount { accountNum: number, bump: number, reserved: number[], + public accountData: {}, ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; this.tokens = tokens.values.map((dto) => TokenPosition.from(dto)); @@ -67,8 +69,13 @@ export class MangoAccount { this.perps = perps.accounts.map((dto) => PerpPositions.from(dto)); } - async reload(client: MangoClient) { + async reload(client: MangoClient, group: Group) { Object.assign(this, await client.getMangoAccount(this)); + await this.reloadAccountData(client, group); + } + + async reloadAccountData(client: MangoClient, group: Group) { + this.accountData = await client.computeAccountData(group, this); } findToken(tokenIndex: number): TokenPosition | undefined { @@ -84,6 +91,16 @@ export class MangoAccount { return ta ? ta.native(bank) : ZERO_I80F48; } + getNativeDeposits(bank: Bank): I80F48 { + const native = this.getNative(bank); + return native.gte(ZERO_I80F48) ? native : ZERO_I80F48; + } + + getNativeBorrows(bank: Bank): I80F48 { + const native = this.getNative(bank); + return native.lte(ZERO_I80F48) ? native : ZERO_I80F48; + } + getUi(bank: Bank): number { const ta = this.findToken(bank.tokenIndex); return ta ? ta.ui(bank) : 0; @@ -99,6 +116,100 @@ export class MangoAccount { return ta ? ta.uiBorrows(bank) : 0; } + /** + * Sum of all the assets i.e. token deposits, borrows, total assets in spot open orders, (perps positions is todo) in terms of quote value. + */ + getEquity(): I80F48 { + const equity = (this.accountData as MangoAccountData).equity; + let total_equity = equity.tokens.reduce( + (a, b) => a.add(b.value), + ZERO_I80F48, + ); + return total_equity; + } + + /** + * The amount of native quote you could withdraw against your existing assets. + */ + getCollateralValue(): I80F48 { + return (this.accountData as MangoAccountData).initHealth; + } + + /** + * Similar to getEquity, but only the sum of all positive assets. + */ + getAssetsVal(): I80F48 { + const equity = (this.accountData as MangoAccountData).equity; + let total_equity = equity.tokens.reduce( + (a, b) => (b.value.gt(ZERO_I80F48) ? a.add(b.value) : a), + ZERO_I80F48, + ); + return total_equity; + } + + /** + * Similar to getEquity, but only the sum of all negative assets. Note: return value would be negative. + */ + getLiabsVal(): I80F48 { + const equity = (this.accountData as MangoAccountData).equity; + let total_equity = equity.tokens.reduce( + (a, b) => (b.value.lt(ZERO_I80F48) ? a.add(b.value) : a), + ZERO_I80F48, + ); + return total_equity; + } + + /** + * The amount of given native token you can borrow, considering all existing assets as collateral except the deposits for this token. + * The existing native deposits need to be added to get the full amount that could be withdrawn. + */ + async getMaxWithdrawWithBorrowForToken( + group: Group, + tokenName: string, + ): Promise { + const bank = group.banksMap.get(tokenName); + + const initHealth = (this.accountData as MangoAccountData).initHealth; + + const newInitHealth = initHealth.sub( + this.getNativeDeposits(bank).mul(bank.price).mul(bank.initAssetWeight), + ); + + return newInitHealth.div(bank.price.mul(bank.initLiabWeight)); + } + + /** + * The remaining native quote margin available for given market. + * + * TODO: this is a very bad estimation atm. + * It assumes quote asset is always USDC, + * it assumes that there are no interaction effects, + * it assumes that there are no existing borrows for either of the tokens in the market. + */ + getSerum3MarketMarginAvailable(group: Group, marketName: string): I80F48 { + const initHealth = (this.accountData as MangoAccountData).initHealth; + const serum3Market = group.serum3MarketsMap.get(marketName)!; + const marketAssetWeight = group.findBank( + serum3Market.baseTokenIndex, + ).initAssetWeight; + return initHealth.div(ONE_I80F48.sub(marketAssetWeight)); + } + + /** + * The remaining native quote margin available for given market. + * + * TODO: this is a very bad estimation atm. + * It assumes quote asset is always USDC, + * it assumes that there are no interaction effects, + * it assumes that there are no existing borrows for either of the tokens in the market. + */ + getPerpMarketMarginAvailable(group: Group, marketName: string): I80F48 { + const initHealth = (this.accountData as MangoAccountData).initHealth; + const perpMarket = group.perpMarketsMap.get(marketName)!; + const marketAssetWeight = perpMarket.initAssetWeight; + return initHealth.div(ONE_I80F48.sub(marketAssetWeight)); + } + tokensActive(): TokenPosition[] { return this.tokens.filter((token) => token.isActive()); } @@ -112,8 +223,9 @@ export class MangoAccount { } toString(group?: Group): string { - let res = ''; - res = res + ' name: ' + this.name; + let res = 'MangoAccount'; + res = res + '\n pk: ' + this.publicKey.toString(); + res = res + '\n name: ' + this.name; res = this.tokensActive().length > 0 @@ -293,3 +405,68 @@ export class PerpPositionDto { public takerQuoteLots: BN, ) {} } + +export class HealthType { + static maint = { maint: {} }; + static init = { init: {} }; +} + +export class MangoAccountData { + constructor( + public initHealth: I80F48, + public maintHealth: I80F48, + public equity: Equity, + ) {} + + static from(event: { + initHealth: I80F48Dto; + maintHealth: I80F48Dto; + equity: { + tokens: [{ tokenIndex: number; value: I80F48Dto }]; + perps: [{ perpMarketIndex: number; value: I80F48Dto }]; + }; + initHealthLiabs: I80F48Dto; + tokenAssets: any; + }) { + return new MangoAccountData( + I80F48.from(event.initHealth), + I80F48.from(event.maintHealth), + Equity.from(event.equity), + ); + } +} + +export class Equity { + public constructor( + public tokens: TokenEquity[], + public perps: PerpEquity[], + ) {} + + static from(dto: EquityDto): Equity { + return new Equity( + dto.tokens.map( + (token) => new TokenEquity(token.tokenIndex, I80F48.from(token.value)), + ), + dto.perps.map( + (perpAccount) => + new PerpEquity( + perpAccount.perpMarketIndex, + I80F48.from(perpAccount.value), + ), + ), + ); + } +} + +export class TokenEquity { + public constructor(public tokenIndex: number, public value: I80F48) {} +} + +export class PerpEquity { + public constructor(public perpMarketIndex: number, public value: I80F48) {} +} + +export class EquityDto { + tokens: { tokenIndex: number; value: I80F48Dto }[]; + perps: { perpMarketIndex: number; value: I80F48Dto }[]; +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index c07603869..3d53b1779 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -7,7 +7,6 @@ import { initializeAccount, WRAPPED_SOL_MINT, } from '@project-serum/serum/lib/token-instructions'; -import { parsePriceData } from '@pythnetwork/client'; import { ASSOCIATED_TOKEN_PROGRAM_ID, Token, @@ -31,7 +30,7 @@ import bs58 from 'bs58'; import { Bank, MintInfo } from './accounts/bank'; import { Group } from './accounts/group'; import { I80F48 } from './accounts/I80F48'; -import { MangoAccount } from './accounts/mangoAccount'; +import { MangoAccount, MangoAccountData } from './accounts/mangoAccount'; import { StubOracle } from './accounts/oracle'; import { OrderType, PerpMarket, Side } from './accounts/perp'; import { @@ -282,25 +281,6 @@ export class MangoClient { }); } - public async getPricesForGroup(group: Group): Promise { - if (group.banksMap.size === 0) { - await this.getBanksForGroup(group); - } - - const banks = Array.from(group?.banksMap, ([, value]) => value); - const oracles = banks.map((b) => b.oracle); - const prices = - await this.program.provider.connection.getMultipleAccountsInfo(oracles); - - for (const [index, price] of prices.entries()) { - if (banks[index].name === 'USDC') { - banks[index].price = 1; - } else { - banks[index].price = parsePriceData(price.data).previousPrice; - } - } - } - // Stub Oracle public async createStubOracle( @@ -454,6 +434,32 @@ export class MangoClient { .rpc(); } + public async computeAccountData( + group: Group, + mangoAccount: MangoAccount, + ): Promise { + const healthRemainingAccounts: PublicKey[] = + await this.buildHealthRemainingAccounts(group, mangoAccount); + + const res = await this.program.methods + .computeAccountData() + .accounts({ + group: group.publicKey, + account: mangoAccount.publicKey, + }) + .remainingAccounts( + healthRemainingAccounts.map( + (pk) => + ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), + ), + ) + .simulate(); + + return MangoAccountData.from( + res.events.find((event) => (event.name = 'MangoAccountData')).data as any, + ); + } + public async tokenDeposit( group: Group, mangoAccount: MangoAccount, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index bccf072b8..313600554 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -2141,7 +2141,7 @@ export type MangoV4 = { "args": [] }, { - "name": "computeHealth", + "name": "computeAccountData", "accounts": [ { "name": "group", @@ -2154,17 +2154,7 @@ export type MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "healthType", - "type": { - "defined": "HealthType" - } - } - ], - "returns": { - "defined": "I80F48" - } + "args": [] }, { "name": "benchmark", @@ -2875,6 +2865,66 @@ export type MangoV4 = { } ], "types": [ + { + "name": "Equity", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokens", + "type": { + "vec": { + "defined": "TokenEquity" + } + } + }, + { + "name": "perps", + "type": { + "vec": { + "defined": "PerpEquity" + } + } + } + ] + } + }, + { + "name": "TokenEquity", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokenIndex", + "type": "u16" + }, + { + "name": "value", + "type": { + "defined": "I80F48" + } + } + ] + } + }, + { + "name": "PerpEquity", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "value", + "type": { + "defined": "I80F48" + } + } + ] + } + }, { "name": "FlashLoanWithdraw", "type": { @@ -3587,6 +3637,32 @@ export type MangoV4 = { } ], "events": [ + { + "name": "MangoAccountData", + "fields": [ + { + "name": "initHealth", + "type": { + "defined": "I80F48" + }, + "index": false + }, + { + "name": "maintHealth", + "type": { + "defined": "I80F48" + }, + "index": false + }, + { + "name": "equity", + "type": { + "defined": "Equity" + }, + "index": false + } + ] + }, { "name": "PerpBalanceLog", "fields": [ @@ -6215,7 +6291,7 @@ export const IDL: MangoV4 = { "args": [] }, { - "name": "computeHealth", + "name": "computeAccountData", "accounts": [ { "name": "group", @@ -6228,17 +6304,7 @@ export const IDL: MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "healthType", - "type": { - "defined": "HealthType" - } - } - ], - "returns": { - "defined": "I80F48" - } + "args": [] }, { "name": "benchmark", @@ -6949,6 +7015,66 @@ export const IDL: MangoV4 = { } ], "types": [ + { + "name": "Equity", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokens", + "type": { + "vec": { + "defined": "TokenEquity" + } + } + }, + { + "name": "perps", + "type": { + "vec": { + "defined": "PerpEquity" + } + } + } + ] + } + }, + { + "name": "TokenEquity", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokenIndex", + "type": "u16" + }, + { + "name": "value", + "type": { + "defined": "I80F48" + } + } + ] + } + }, + { + "name": "PerpEquity", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "value", + "type": { + "defined": "I80F48" + } + } + ] + } + }, { "name": "FlashLoanWithdraw", "type": { @@ -7661,6 +7787,32 @@ export const IDL: MangoV4 = { } ], "events": [ + { + "name": "MangoAccountData", + "fields": [ + { + "name": "initHealth", + "type": { + "defined": "I80F48" + }, + "index": false + }, + { + "name": "maintHealth", + "type": { + "defined": "I80F48" + }, + "index": false + }, + { + "name": "equity", + "type": { + "defined": "Equity" + }, + "index": false + } + ] + }, { "name": "PerpBalanceLog", "fields": [ diff --git a/ts/client/src/scripts/example1-admin.ts b/ts/client/src/scripts/example1-admin.ts index 9ccc05c86..a566509ad 100644 --- a/ts/client/src/scripts/example1-admin.ts +++ b/ts/client/src/scripts/example1-admin.ts @@ -225,7 +225,7 @@ async function main() { group, btcDevnetOracle, 0, - 'BTC/USDC', + 'BTC-PERP', 0.1, 0, 6, diff --git a/ts/client/src/scripts/example1-flash-loan.ts b/ts/client/src/scripts/example1-flash-loan.ts index 51809bb97..63d9a6644 100644 --- a/ts/client/src/scripts/example1-flash-loan.ts +++ b/ts/client/src/scripts/example1-flash-loan.ts @@ -49,11 +49,11 @@ async function main() { // deposit and withdraw console.log(`Depositing...50 USDC`); await client.tokenDeposit(group, mangoAccount, 'USDC', 50); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(`Depositing...0.0005 BTC`); await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); } try { const sig = await client.marginTrade({ diff --git a/ts/client/src/scripts/example1-user-close-account.ts b/ts/client/src/scripts/example1-user-close-account.ts index d31218a53..659284343 100644 --- a/ts/client/src/scripts/example1-user-close-account.ts +++ b/ts/client/src/scripts/example1-user-close-account.ts @@ -82,7 +82,7 @@ async function main() { } // we closed a serum account, this changes the health accounts we are passing in for future ixs - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); // withdraw all tokens for (const token of mangoAccount.tokensActive()) { @@ -109,7 +109,7 @@ async function main() { } // reload and print current positions - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(`...mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString()); diff --git a/ts/client/src/scripts/example1-user.ts b/ts/client/src/scripts/example1-user.ts index 486a2d593..53f965a96 100644 --- a/ts/client/src/scripts/example1-user.ts +++ b/ts/client/src/scripts/example1-user.ts @@ -9,6 +9,7 @@ import { } from '../accounts/serum3'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; +import { toUiDecimals } from '../utils'; // // An example for users based on high level api i.e. the client @@ -44,7 +45,7 @@ async function main() { ), ); const group = await client.getGroupForAdmin(admin.publicKey, 0); - console.log(`Found group ${group.publicKey.toBase58()}`); + console.log(group.toString()); // create + fetch account console.log(`Creating mangoaccount...`); @@ -57,19 +58,28 @@ async function main() { console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString()); + await mangoAccount.reloadAccountData(client, group); + if (true) { // deposit and withdraw console.log(`Depositing...50 USDC`); await client.tokenDeposit(group, mangoAccount, 'USDC', 50); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(`Depositing...0.0005 BTC`); await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0005); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); - console.log(`Withdrawing...1 USDC`); - await client.tokenWithdraw(group, mangoAccount, 'USDC', 1, false); - await mangoAccount.reload(client); + console.log(`Withdrawing...0.1 ORCA`); + await client.tokenWithdraw2( + group, + mangoAccount, + 'ORCA', + 0.1 * Math.pow(10, group.banksMap.get('ORCA').mintDecimals), + true, + ); + await mangoAccount.reload(client, group); + console.log(mangoAccount.toString()); // serum3 console.log( @@ -88,7 +98,7 @@ async function main() { Date.now(), 10, ); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(`Placing serum3 bid way above midprice...`); await client.serum3PlaceOrder( @@ -104,7 +114,7 @@ async function main() { Date.now(), 10, ); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(`Placing serum3 ask way below midprice...`); await client.serum3PlaceOrder( @@ -159,16 +169,50 @@ async function main() { 'BTC/USDC', ); + } - // try { - // console.log(`Close OO...`); - // await client.serum3CloseOpenOrders(group, mangoAccount, 'BTC/USDC'); - // } catch (error) { - // console.log(error); - // } - - // console.log(`Close mango account...`); - // await client.closeMangoAccount(mangoAccount); + if (true) { + await mangoAccount.reload(client, group); + console.log( + 'mangoAccount.getEquity() ' + + toUiDecimals(mangoAccount.getEquity().toNumber()), + ); + console.log( + 'mangoAccount.getCollateralValue() ' + + toUiDecimals(mangoAccount.getCollateralValue().toNumber()), + ); + console.log( + 'mangoAccount.getAssetsVal() ' + + toUiDecimals(mangoAccount.getAssetsVal().toNumber()), + ); + console.log( + 'mangoAccount.getLiabsVal() ' + + toUiDecimals(mangoAccount.getLiabsVal().toNumber()), + ); + console.log( + "mangoAccount.getMaxWithdrawWithBorrowForToken(group, 'SOL') " + + toUiDecimals( + ( + await mangoAccount.getMaxWithdrawWithBorrowForToken(group, 'SOL') + ).toNumber(), + ), + ); + console.log( + "mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " + + toUiDecimals( + mangoAccount + .getSerum3MarketMarginAvailable(group, 'BTC/USDC') + .toNumber(), + ), + ); + console.log( + "mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " + + toUiDecimals( + mangoAccount + .getPerpMarketMarginAvailable(group, 'BTC-PERP') + .toNumber(), + ), + ); } if (true) { @@ -178,7 +222,7 @@ async function main() { await client.perpPlaceOrder( group, mangoAccount, - 'BTC/USDC', + 'BTC-PERP', Side.bid, 30000, 0.000001, @@ -196,7 +240,7 @@ async function main() { await client.perpPlaceOrder( group, mangoAccount, - 'BTC/USDC', + 'BTC-PERP', Side.ask, 30000, 0.000001, @@ -212,7 +256,7 @@ async function main() { console.log( `Waiting for self trade to consume (note: make sure keeper crank is running)...`, ); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(mangoAccount.toString()); } } diff --git a/ts/client/src/scripts/mb-example1-close-account.ts b/ts/client/src/scripts/mb-example1-close-account.ts index 8035e3428..212598d79 100644 --- a/ts/client/src/scripts/mb-example1-close-account.ts +++ b/ts/client/src/scripts/mb-example1-close-account.ts @@ -81,7 +81,7 @@ async function main() { } // we closed a serum account, this changes the health accounts we are passing in for future ixs - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); // withdraw all tokens for (const token of mangoAccount.tokensActive()) { @@ -102,7 +102,7 @@ async function main() { ); } - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(`...mangoAccount ${mangoAccount.publicKey}`); console.log(mangoAccount.toString()); diff --git a/ts/client/src/scripts/mb-flash-loan-3.ts b/ts/client/src/scripts/mb-flash-loan-3.ts index 0c49349a8..4f1b5a56e 100644 --- a/ts/client/src/scripts/mb-flash-loan-3.ts +++ b/ts/client/src/scripts/mb-flash-loan-3.ts @@ -265,7 +265,7 @@ async function main() { group.reloadBanks(client); console.log(`end btc bank ${group.banksMap.get('BTC').toString()}`); - await mangoAccount.reload(client); + await mangoAccount.reload(client, group); console.log(`end balance \n${mangoAccount.toString(group)}`); } } diff --git a/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts b/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts index 3c61330d2..10021ef36 100644 --- a/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts +++ b/ts/client/src/scripts/scratch/example1-create-liquidation-candidate.ts @@ -48,7 +48,7 @@ async function main() { let token = 'BTC'; console.log(`Depositing...${amount} 'BTC'`); await user1Client.tokenDeposit(group, user1MangoAccount, token, amount); - await user1MangoAccount.reload(user1Client); + await user1MangoAccount.reload(user1Client, group); console.log(`${user1MangoAccount.toString(group)}`); // user 2 @@ -77,7 +77,7 @@ async function main() { /// user2 deposits some collateral and borrows BTC console.log(`Depositing...${300} 'USDC'`); await user2Client.tokenDeposit(group, user2MangoAccount, 'USDC', 300); - await user2MangoAccount.reload(user2Client); + await user2MangoAccount.reload(user2Client, group); console.log(`${user2MangoAccount.toString(group)}`); amount = amount / 10; while (true) { @@ -95,7 +95,7 @@ async function main() { break; } } - await user2MangoAccount.reload(user2Client); + await user2MangoAccount.reload(user2Client, group); console.log(`${user2MangoAccount.toString(group)}`); /// Reduce usdc price diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index f62dfea3f..2d3724a1f 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -4,6 +4,7 @@ import { } from '@solana/spl-token'; import { AccountMeta, PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; +import { QUOTE_DECIMALS } from './accounts/bank'; import { I80F48 } from './accounts/I80F48'; export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64); @@ -72,7 +73,11 @@ export function toNativeDecimals(amount: number, decimals: number): BN { return new BN(Math.trunc(amount * Math.pow(10, decimals))); } -export function toUiDecimals(amount: number, decimals: number): number { +export function toUiDecimals( + amount: I80F48 | number, + decimals = QUOTE_DECIMALS, +): number { + amount = amount instanceof I80F48 ? amount.toNumber() : amount; return amount / Math.pow(10, decimals); }