client functions via program simulation

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-07-04 12:29:35 +02:00
parent 39284c5705
commit dff3f7cd8c
23 changed files with 713 additions and 147 deletions

View File

@ -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<TokenEquity>,
pub perps: Vec<PerpEquity>,
}
#[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
}

View File

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

View File

@ -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<ComputeHealth>, health_type: HealthType) -> Result<I80F48> {
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)
}

View File

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

View File

@ -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<ComputeHealth>, health_type: HealthType) -> Result<I80F48> {
instructions::compute_health(ctx, health_type)
pub fn compute_account_data(ctx: Context<ComputeAccountData>) -> Result<()> {
instructions::compute_account_data(ctx)
}
///

View File

@ -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<Equity> {
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::<Vec<TokenEquity>>();
// TODO: perp contributions
let perps = Vec::new();
Ok(Equity { tokens, perps })
}

View File

@ -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::<Bank>()?;
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::<PerpMarket>()
}
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::<PerpMarket>()
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);

View File

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

View File

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

View File

@ -87,7 +87,7 @@ async fn test_basic() -> Result<(), TransportError> {
//
send_tx(
solana,
ComputeHealthInstruction {
ComputeAccountDataInstruction {
account,
health_type: HealthType::Init,
},

View File

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

View File

@ -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<string, Market>,
public perpMarketsMap: Map<string, PerpMarket>,
public mintInfosMap: Map<number, MintInfo>,
public oraclesMap: Map<string, PriceData>,
) {}
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<void> {
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;
}
}

View File

@ -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<I80F48> {
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 }[];
}

View File

@ -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<void> {
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<MangoAccountData> {
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,

View File

@ -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": [

View File

@ -225,7 +225,7 @@ async function main() {
group,
btcDevnetOracle,
0,
'BTC/USDC',
'BTC-PERP',
0.1,
0,
6,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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