ts: Additional serum3 support (#196)

* get bids and asks for a user on a serum3 market
* get orderbook for a market
* get max bid or ask that a user can place for a market
* simulate health if a bid or ask were to be placed

misc:
* fix remaining accounts list for health when placing perp bids

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

format

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

format

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

remove testing code

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

script adjustment

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

comments

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-08-31 11:36:44 +02:00 committed by GitHub
parent 4a1865be6c
commit c66dd882b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1500 additions and 399 deletions

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ ts/client/**/*.js
ts/client/**/*.js.map
migrations/*.js
migrations/*.js.map
ts/client/src/scripts/archive/ts.ts

0
Program Normal file
View File

View File

@ -224,10 +224,10 @@ export class I80F48 {
}
/** @internal */
export const ONE_I80F48 = I80F48.fromString('1');
export const ONE_I80F48 = I80F48.fromNumber(1);
/** @internal */
export const ZERO_I80F48 = I80F48.fromString('0');
export const ZERO_I80F48 = I80F48.fromNumber(0);
/** @internal */
export const NEG_ONE_I80F48 = I80F48.fromString('-1');
export const HUNDRED_I80F48 = I80F48.fromString('100');
export const NEG_ONE_I80F48 = I80F48.fromNumber(-1);
export const HUNDRED_I80F48 = I80F48.fromNumber(100);
export const MAX_I80F48 = new I80F48(I80F48.MAX_BN);

View File

@ -1,6 +1,11 @@
import { BorshAccountsCoder } from '@project-serum/anchor';
import { coder } from '@project-serum/anchor/dist/cjs/spl/token';
import { Market } from '@project-serum/serum';
import {
getFeeRates,
getFeeTier,
Market,
Orderbook,
} from '@project-serum/serum';
import { parsePriceData, PriceData } from '@pythnetwork/client';
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
@ -68,8 +73,9 @@ export class Group {
public banksMapByName: Map<string, Bank[]>,
public banksMapByMint: Map<string, Bank[]>,
public banksMapByTokenIndex: Map<number, Bank[]>,
public serum3MarketsMap: Map<string, Serum3Market>,
public serum3MarketsMapByExternal: Map<string, Serum3Market>,
public serum3MarketExternalsMap: Map<string, Market>,
// TODO rethink key
public perpMarketsMap: Map<string, PerpMarket>,
public mintInfosMapByTokenIndex: Map<number, MintInfo>,
public mintInfosMapByMint: Map<string, MintInfo>,
@ -77,12 +83,6 @@ export class Group {
public vaultAmountsMap: Map<string, number>,
) {}
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
return Array.from(this.serum3MarketsMap.values()).find(
(serum3Market) => serum3Market.marketIndex === marketIndex,
);
}
public async reloadAll(client: MangoClient) {
let ids: Id | undefined = undefined;
@ -180,14 +180,17 @@ export class Group {
serum3Markets = await client.serum3GetMarkets(this);
}
this.serum3MarketsMap = new Map(
serum3Markets.map((serum3Market) => [serum3Market.name, serum3Market]),
this.serum3MarketsMapByExternal = new Map(
serum3Markets.map((serum3Market) => [
serum3Market.serumMarketExternal.toBase58(),
serum3Market,
]),
);
}
public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) {
const externalMarkets = await Promise.all(
Array.from(this.serum3MarketsMap.values()).map((serum3Market) =>
Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) =>
Market.load(
client.program.provider.connection,
serum3Market.serumMarketExternal,
@ -198,10 +201,12 @@ export class Group {
);
this.serum3MarketExternalsMap = new Map(
Array.from(this.serum3MarketsMap.values()).map((serum3Market, index) => [
serum3Market.name,
externalMarkets[index],
]),
Array.from(this.serum3MarketsMapByExternal.values()).map(
(serum3Market, index) => [
serum3Market.serumMarketExternal.toBase58(),
externalMarkets[index],
],
),
);
}
@ -316,6 +321,37 @@ export class Group {
return I80F48.fromNumber(amount);
}
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
return Array.from(this.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.marketIndex === marketIndex,
);
}
public async loadSerum3BidsForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
return await this.serum3MarketsMapByExternal
.get(externalMarketPk.toBase58())
.loadBids(client, this);
}
public async loadSerum3AsksForMarket(
client: MangoClient,
externalMarketPk: PublicKey,
): Promise<Orderbook> {
return await this.serum3MarketsMapByExternal
.get(externalMarketPk.toBase58())
.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;
}
/**
*
* @param mintPk

View File

@ -11,6 +11,7 @@ import {
ZERO_I80F48,
} from './I80F48';
import { HealthType } from './mangoAccount';
import { Serum3Market, Serum3Side } from './serum3';
// ░░░░
//
@ -37,14 +38,18 @@ import { HealthType } from './mangoAccount';
// warning: this code is copy pasta from rust, keep in sync with health.rs
export class HealthCache {
tokenInfos: TokenInfo[];
serum3Infos: Serum3Info[];
perpInfos: PerpInfo[];
constructor(
public tokenInfos: TokenInfo[],
public serum3Infos: Serum3Info[],
public perpInfos: PerpInfo[],
) {}
constructor(dto: HealthCacheDto) {
this.tokenInfos = dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto));
this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto));
this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto));
static fromDto(dto) {
return new HealthCache(
dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
dto.perpInfos.map((dto) => new PerpInfo(dto)),
);
}
public health(healthType: HealthType): I80F48 {
@ -172,10 +177,54 @@ export class HealthCache {
return this.findTokenInfoIndex(bank.tokenIndex);
}
private static logHealthCache(debug: string, healthCache: HealthCache) {
console.log(debug);
adjustSerum3Reserved(
// todo change indices to types from numbers
marketIndex: number,
baseTokenIndex: number,
reservedBaseChange: I80F48,
freeBaseChange: I80F48,
quoteTokenIndex: number,
reservedQuoteChange: I80F48,
freeQuoteChange: I80F48,
) {
const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex);
const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex);
let reservedAmount = ZERO_I80F48;
const baseEntry = this.tokenInfos[baseEntryIndex];
reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice);
const quoteEntry = this.tokenInfos[quoteEntryIndex];
reservedAmount = reservedAmount.add(
reservedQuoteChange.mul(quoteEntry.oraclePrice),
);
// Apply it to the tokens
baseEntry.serum3MaxReserved =
baseEntry.serum3MaxReserved.add(reservedAmount);
baseEntry.balance = baseEntry.balance.add(
freeBaseChange.mul(baseEntry.oraclePrice),
);
quoteEntry.serum3MaxReserved =
quoteEntry.serum3MaxReserved.add(reservedAmount);
quoteEntry.balance = quoteEntry.balance.add(
freeQuoteChange.mul(quoteEntry.oraclePrice),
);
// Apply it to the serum3 info
const serum3Info = this.serum3Infos.find(
(serum3Info) => serum3Info.marketIndex === marketIndex,
);
serum3Info.reserved = serum3Info.reserved.add(reservedAmount);
}
public static logHealthCache(debug: string, healthCache: HealthCache) {
if (debug) console.log(debug);
for (const token of healthCache.tokenInfos) {
console.log(`${token.toString()}`);
console.log(` {token.toString()}`);
}
for (const serum3Info of healthCache.serum3Infos) {
console.log(` {serum3Info.toString(healthCache.tokenInfos)}`);
}
console.log(
` assets ${healthCache.assets(
@ -213,6 +262,118 @@ export class HealthCache {
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3BidChanges(
group: Group,
bidNativeQuoteAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const quoteBank = group.banksMapByTokenIndex.get(
serum3Market.quoteTokenIndex,
)[0];
const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank);
const quote = adjustedCache.tokenInfos[quoteIndex];
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for quote
adjustedCache.tokenInfos[quoteIndex].balance = adjustedCache.tokenInfos[
quoteIndex
].balance.sub(bidNativeQuoteAmount.mul(quote.oraclePrice));
// Increase reserved in Serum3Info for quote
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
ZERO_I80F48,
ZERO_I80F48,
serum3Market.quoteTokenIndex,
bidNativeQuoteAmount,
ZERO_I80F48,
);
return adjustedCache.healthRatio(healthType);
}
simHealthRatioWithSerum3AskChanges(
group: Group,
askNativeBaseAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
const adjustedCache: HealthCache = _.cloneDeep(this);
const baseBank = group.banksMapByTokenIndex.get(
serum3Market.baseTokenIndex,
)[0];
const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank);
const base = adjustedCache.tokenInfos[baseIndex];
// Move token balance to reserved funds in open orders,
// essentially simulating a place order
// Reduce token balance for base
adjustedCache.tokenInfos[baseIndex].balance = adjustedCache.tokenInfos[
baseIndex
].balance.sub(askNativeBaseAmount.mul(base.oraclePrice));
// Increase reserved in Serum3Info for base
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
askNativeBaseAmount,
ZERO_I80F48,
serum3Market.quoteTokenIndex,
ZERO_I80F48,
ZERO_I80F48,
);
return adjustedCache.healthRatio(healthType);
}
private static binaryApproximationSearch(
left: I80F48,
leftRatio: I80F48,
right: I80F48,
rightRatio: I80F48,
targetRatio: I80F48,
healthRatioAfterActionFn: (I80F48) => I80F48,
) {
const maxIterations = 40;
// TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48.fromNumber(0.001);
if (
(leftRatio.sub(targetRatio).isPos() &&
rightRatio.sub(targetRatio).isPos()) ||
(leftRatio.sub(targetRatio).isNeg() &&
rightRatio.sub(targetRatio).isNeg())
) {
throw new Error(
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
);
}
let newAmount;
for (const key of Array(maxIterations).fill(0).keys()) {
newAmount = left.add(right).mul(I80F48.fromNumber(0.5));
const newAmountRatio = healthRatioAfterActionFn(newAmount);
const error = newAmountRatio.sub(targetRatio);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
left = newAmount;
} else {
right = newAmount;
rightRatio = newAmountRatio;
}
}
console.error(
`Unable to get targetRatio within ${maxIterations} iterations`,
);
return newAmount;
}
getMaxSourceForTokenSwap(
group: Group,
sourceMintPk: PublicKey,
@ -287,54 +448,10 @@ export class HealthCache {
.max(ZERO_I80F48);
const cache0 = cacheAfterSwap(point0Amount);
const point0Ratio = cache0.healthRatio(HealthType.init);
const point0Health = cache0.health(HealthType.init);
const cache1 = cacheAfterSwap(point1Amount);
const point1Ratio = cache1.healthRatio(HealthType.init);
const point1Health = cache1.health(HealthType.init);
function binaryApproximationSearch(
left: I80F48,
leftRatio: I80F48,
right: I80F48,
rightRatio: I80F48,
targetRatio: I80F48,
) {
const maxIterations = 20;
// TODO: make relative to health ratio decimals? Might be over engineering
const targetError = I80F48.fromString('0.001');
if (
(leftRatio.sub(targetRatio).isPos() &&
rightRatio.sub(targetRatio).isPos()) ||
(leftRatio.sub(targetRatio).isNeg() &&
rightRatio.sub(targetRatio).isNeg())
) {
throw new Error(
`internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`,
);
}
let newAmount;
for (const key of Array(maxIterations).fill(0).keys()) {
newAmount = left.add(right).mul(I80F48.fromString('0.5'));
const newAmountRatio = healthRatioAfterSwap(newAmount);
const error = newAmountRatio.sub(targetRatio);
if (error.isPos() && error.lt(targetError)) {
return newAmount;
}
if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) {
left = newAmount;
} else {
right = newAmount;
rightRatio = newAmountRatio;
}
}
console.error(
`Unable to get targetRatio within ${maxIterations} iterations`,
);
return newAmount;
}
let amount: I80F48;
if (
@ -365,30 +482,24 @@ export class HealthCache {
const zeroHealthAmount = point1Amount.add(
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)),
);
// console.log(`point1Amount ${point1Amount}`);
// console.log(`point1Health ${point1Health}`);
// console.log(`point1Ratio ${point1Ratio}`);
// console.log(`point0Amount ${point0Amount}`);
// console.log(`point0Health ${point0Health}`);
// console.log(`point0Ratio ${point0Ratio}`);
// console.log(`zeroHealthAmount ${zeroHealthAmount}`);
const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
// console.log(`zeroHealthRatio ${zeroHealthRatio}`);
amount = binaryApproximationSearch(
amount = HealthCache.binaryApproximationSearch(
point1Amount,
point1Ratio,
zeroHealthAmount,
zeroHealthRatio,
minRatio,
healthRatioAfterSwap,
);
} else if (point0Ratio.gte(minRatio)) {
// Must be between point0Amount and point1Amount.
amount = binaryApproximationSearch(
amount = HealthCache.binaryApproximationSearch(
point0Amount,
point0Ratio,
point1Amount,
point1Ratio,
minRatio,
healthRatioAfterSwap,
);
} else {
throw new Error(
@ -404,6 +515,122 @@ export class HealthCache {
),
);
}
getMaxForSerum3Order(
group: Group,
serum3Market: Serum3Market,
side: Serum3Side,
minRatio: I80F48,
) {
const baseBank = group.banksMapByTokenIndex.get(
serum3Market.baseTokenIndex,
)[0];
const quoteBank = group.banksMapByTokenIndex.get(
serum3Market.quoteTokenIndex,
)[0];
const healthCacheClone: HealthCache = _.cloneDeep(this);
const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank);
const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex(quoteBank);
const base = healthCacheClone.tokenInfos[baseIndex];
const quote = healthCacheClone.tokenInfos[quoteIndex];
// Binary search between current health (0 sized new order) and
// an amount to trade which will bring health to 0.
// Current health and amount i.e. 0
const initialAmount = ZERO_I80F48;
const initialHealth = this.health(HealthType.init);
const initialRatio = this.healthRatio(HealthType.init);
if (initialRatio.lte(ZERO_I80F48)) {
return ZERO_I80F48;
}
// Amount which would bring health to 0
// amount = max(A_deposits, B_borrows) + init_health / (A_liab_weight - B_asset_weight)
// A is what we would be essentially swapping for B
// So when its an ask, then base->quote,
// and when its a bid, then quote->bid
let zeroAmount;
if (side == Serum3Side.ask) {
const quoteBorrows = quote.balance.lt(ZERO_I80F48)
? quote.balance.abs()
: ZERO_I80F48;
zeroAmount = base.balance
.max(quoteBorrows)
.add(
initialHealth.div(
base
.liabWeight(HealthType.init)
.sub(quote.assetWeight(HealthType.init)),
),
);
} else {
const baseBorrows = base.balance.lt(ZERO_I80F48)
? base.balance.abs()
: ZERO_I80F48;
zeroAmount = quote.balance
.max(baseBorrows)
.add(
initialHealth.div(
quote
.liabWeight(HealthType.init)
.sub(base.assetWeight(HealthType.init)),
),
);
}
const cache = cacheAfterPlacingOrder(zeroAmount);
const zeroAmountRatio = cache.healthRatio(HealthType.init);
function cacheAfterPlacingOrder(amount: I80F48) {
const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone);
side === Serum3Side.ask
? (adjustedCache.tokenInfos[baseIndex].balance =
adjustedCache.tokenInfos[baseIndex].balance.sub(amount))
: (adjustedCache.tokenInfos[quoteIndex].balance =
adjustedCache.tokenInfos[quoteIndex].balance.sub(amount));
adjustedCache.adjustSerum3Reserved(
serum3Market.marketIndex,
serum3Market.baseTokenIndex,
side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48,
ZERO_I80F48,
serum3Market.quoteTokenIndex,
side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48,
ZERO_I80F48,
);
return adjustedCache;
}
function healthRatioAfterPlacingOrder(amount: I80F48): I80F48 {
return cacheAfterPlacingOrder(amount).healthRatio(HealthType.init);
}
const amount = HealthCache.binaryApproximationSearch(
initialAmount,
initialRatio,
zeroAmount,
zeroAmountRatio,
minRatio,
healthRatioAfterPlacingOrder,
);
// If its a bid then the reserved fund and potential loan is in quote,
// If its a ask then the reserved fund and potential loan is in base,
// also keep some buffer for fees, use taker fees for worst case simulation.
return side === Serum3Side.bid
? amount
.div(quote.oraclePrice)
.div(ONE_I80F48.add(baseBank.loanOriginationFeeRate))
.div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false))))
: amount
.div(base.oraclePrice)
.div(ONE_I80F48.add(quoteBank.loanOriginationFeeRate))
.div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false))));
}
}
export class TokenInfo {
@ -468,20 +695,30 @@ export class TokenInfo {
}
toString() {
return ` tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`;
return ` tokenIndex: ${this.tokenIndex}, balance: ${
this.balance
}, serum3MaxReserved: ${
this.serum3MaxReserved
}, initHealth ${this.healthContribution(HealthType.init)}`;
}
}
export class Serum3Info {
constructor(dto: Serum3InfoDto) {
this.reserved = I80F48.from(dto.reserved);
this.baseIndex = dto.baseIndex;
this.quoteIndex = dto.quoteIndex;
}
constructor(
public reserved: I80F48,
public baseIndex: number,
public quoteIndex: number,
public marketIndex: number,
) {}
reserved: I80F48;
baseIndex: number;
quoteIndex: number;
static fromDto(dto: Serum3InfoDto) {
return new Serum3Info(
I80F48.from(dto.reserved),
dto.baseIndex,
dto.quoteIndex,
dto.marketIndex,
);
}
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
const baseInfo = tokenInfos[this.baseIndex];
@ -512,7 +749,6 @@ export class Serum3Info {
assetPart = maxBalance;
liabPart = reserved.sub(maxBalance);
}
const assetWeight = tokenInfo.assetWeight(healthType);
const liabWeight = tokenInfo.liabWeight(healthType);
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
@ -522,6 +758,14 @@ export class Serum3Info {
const reservedAsQuote = computeHealthEffect(quoteInfo);
return reservedAsBase.min(reservedAsQuote);
}
toString(tokenInfos: TokenInfo[]) {
return ` marketIndex: ${this.marketIndex}, baseIndex: ${
this.baseIndex
}, quoteIndex: ${this.quoteIndex}, reserved: ${
this.reserved
}, initHealth ${this.healthContribution(HealthType.init, tokenInfos)}`;
}
}
export class PerpInfo {
@ -578,12 +822,39 @@ export class TokenInfoDto {
balance: I80F48Dto;
// in health-reference-token native units
serum3MaxReserved: I80F48Dto;
constructor(
tokenIndex: number,
maintAssetWeight: I80F48Dto,
initAssetWeight: I80F48Dto,
maintLiabWeight: I80F48Dto,
initLiabWeight: I80F48Dto,
oraclePrice: I80F48Dto,
balance: I80F48Dto,
serum3MaxReserved: I80F48Dto,
) {
this.tokenIndex = tokenIndex;
this.maintAssetWeight = maintAssetWeight;
this.initAssetWeight = initAssetWeight;
this.maintLiabWeight = maintLiabWeight;
this.initLiabWeight = initLiabWeight;
this.oraclePrice = oraclePrice;
this.balance = balance;
this.serum3MaxReserved = serum3MaxReserved;
}
}
export class Serum3InfoDto {
reserved: I80F48Dto;
baseIndex: number;
quoteIndex: number;
marketIndex: number;
constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) {
this.reserved = reserved;
this.baseIndex = baseIndex;
this.quoteIndex = quoteIndex;
}
}
export class PerpInfoDto {

View File

@ -1,5 +1,6 @@
import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { Order, Orderbook } from '@project-serum/serum/lib/market';
import { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils';
@ -7,6 +8,7 @@ import { Bank } from './bank';
import { Group } from './group';
import { HealthCache, HealthCacheDto } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { Serum3Market, Serum3Side } from './serum3';
export class MangoAccount {
public tokens: TokenPosition[];
public serum3: Serum3Orders[];
@ -89,6 +91,18 @@ export class MangoAccount {
return this;
}
tokensActive(): TokenPosition[] {
return this.tokens.filter((token) => token.isActive());
}
serum3Active(): Serum3Orders[] {
return this.serum3.filter((serum3) => serum3.isActive());
}
perpActive(): PerpPosition[] {
return this.perps.filter((perp) => perp.isActive());
}
findToken(tokenIndex: number): TokenPosition | undefined {
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
}
@ -406,21 +420,184 @@ export class MangoAccount {
.toNumber();
}
public async loadSerum3OpenOrdersForMarket(
client: MangoClient,
group: Group,
externalMarketPk: PublicKey,
): Promise<Order[]> {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
const serum3OO = this.serum3Active().find(
(s) => s.marketIndex === serum3Market.marketIndex,
);
if (!serum3OO) {
throw new Error(`No open orders account found for ${externalMarketPk}`);
}
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
const [bidsInfo, asksInfo] =
await client.program.provider.connection.getMultipleAccountsInfo([
serum3MarketExternal.bidsAddress,
serum3MarketExternal.asksAddress,
]);
const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data);
const asks = Orderbook.decode(serum3MarketExternal, asksInfo.data);
return [...bids, ...asks].filter((o) =>
o.openOrdersAddress.equals(serum3OO.openOrders),
);
}
/**
* The remaining native quote margin available for given market.
*
* TODO: this is a very bad estimation atm.
* It assumes quote asset is always quote,
* it assumes that there are no interaction effects,
* it assumes that there are no existing borrows for either of the tokens in the market.
* @param group
* @param serum3Market
* @returns maximum native quote which can be traded for base token given current health
*/
getSerum3MarketMarginAvailable(group: Group, marketName: string): I80F48 {
const initHealth = (this.accountData as MangoAccountData).initHealth;
const serum3Market = group.serum3MarketsMap.get(marketName)!;
const marketAssetWeight = group.getFirstBankByTokenIndex(
serum3Market.baseTokenIndex,
).initAssetWeight;
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
public getMaxQuoteForSerum3Bid(
group: Group,
serum3Market: Serum3Market,
): I80F48 {
return this.accountData.healthCache.getMaxForSerum3Order(
group,
serum3Market,
Serum3Side.bid,
I80F48.fromNumber(3),
);
}
public getMaxQuoteForSerum3BidUi(
group: Group,
externalMarketPk: PublicKey,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
const nativeAmount = this.getMaxQuoteForSerum3Bid(group, serum3Market);
return toUiDecimals(
nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals,
);
}
/**
*
* @param group
* @param serum3Market
* @returns maximum native base which can be traded for quote token given current health
*/
public getMaxBaseForSerum3Ask(
group: Group,
serum3Market: Serum3Market,
): I80F48 {
return this.accountData.healthCache.getMaxForSerum3Order(
group,
serum3Market,
Serum3Side.ask,
I80F48.fromNumber(3),
);
}
public getMaxBaseForSerum3AskUi(
group: Group,
externalMarketPk: PublicKey,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
const nativeAmount = this.getMaxBaseForSerum3Ask(group, serum3Market);
return toUiDecimals(
nativeAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals,
);
}
/**
*
* @param group
* @param nativeQuoteAmount
* @param serum3Market
* @param healthType
* @returns health ratio after a bid with nativeQuoteAmount is placed
*/
simHealthRatioWithSerum3BidChanges(
group: Group,
nativeQuoteAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
return this.accountData.healthCache.simHealthRatioWithSerum3BidChanges(
group,
nativeQuoteAmount,
serum3Market,
healthType,
);
}
simHealthRatioWithSerum3BidUiChanges(
group: Group,
uiQuoteAmount: number,
externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
return this.simHealthRatioWithSerum3BidChanges(
group,
toNative(
uiQuoteAmount,
group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex)
.mintDecimals,
),
serum3Market,
healthType,
).toNumber();
}
/**
*
* @param group
* @param nativeBaseAmount
* @param serum3Market
* @param healthType
* @returns health ratio after an ask with nativeBaseAmount is placed
*/
simHealthRatioWithSerum3AskChanges(
group: Group,
nativeBaseAmount: I80F48,
serum3Market: Serum3Market,
healthType: HealthType = HealthType.init,
): I80F48 {
return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges(
group,
nativeBaseAmount,
serum3Market,
healthType,
);
}
simHealthRatioWithSerum3AskUiChanges(
group: Group,
uiBaseAmount: number,
externalMarketPk: PublicKey,
healthType: HealthType = HealthType.init,
): number {
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
);
return this.simHealthRatioWithSerum3AskChanges(
group,
toNative(
uiBaseAmount,
group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex)
.mintDecimals,
),
serum3Market,
healthType,
).toNumber();
}
/**
@ -438,18 +615,6 @@ export class MangoAccount {
return initHealth.div(ONE_I80F48.sub(marketAssetWeight));
}
tokensActive(): TokenPosition[] {
return this.tokens.filter((token) => token.isActive());
}
serum3Active(): Serum3Orders[] {
return this.serum3.filter((serum3) => serum3.isActive());
}
perpActive(): PerpPosition[] {
return this.perps.filter((perp) => perp.isActive());
}
toString(group?: Group): string {
let res = 'MangoAccount';
res = res + '\n pk: ' + this.publicKey.toString();
@ -701,7 +866,7 @@ export class MangoAccountData {
tokenAssets: any;
}) {
return new MangoAccountData(
new HealthCache(event.healthCache),
HealthCache.fromDto(event.healthCache),
I80F48.from(event.initHealth),
I80F48.from(event.maintHealth),
Equity.from(event.equity),

View File

@ -1,6 +1,10 @@
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes';
import { PublicKey } from '@solana/web3.js';
import { Market, Orderbook } from '@project-serum/serum/lib/market';
import { Cluster, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import { MangoClient } from '../client';
import { SERUM3_PROGRAM_ID } from '../constants';
import { Group } from './group';
export class Serum3Market {
public name: string;
@ -44,6 +48,43 @@ export class Serum3Market {
) {
this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0];
}
async loadBids(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
);
return await serum3MarketExternal.loadBids(
client.program.provider.connection,
);
}
async loadAsks(client: MangoClient, group: Group): Promise<Orderbook> {
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
this.serumMarketExternal.toBase58(),
);
return await serum3MarketExternal.loadAsks(
client.program.provider.connection,
);
}
async logOb(client: MangoClient, group: Group): Promise<string> {
let res = ``;
res += ` ${this.name} OrderBook`;
let orders = await this?.loadAsks(client, group);
for (const order of orders!.items(true)) {
res += `\n ${order.price.toString().padStart(10)}, ${order.size
.toString()
.padStart(10)}`;
}
res += `\n --------------------------`;
orders = await this?.loadBids(client, group);
for (const order of orders!.items(true)) {
res += `\n ${order.price.toString().padStart(10)}, ${order.size
.toString()
.padStart(10)}`;
}
return res;
}
}
export class Serum3SelfTradeBehavior {
@ -62,3 +103,21 @@ export class Serum3Side {
static bid = { bid: {} };
static ask = { ask: {} };
}
export async function generateSerum3MarketExternalVaultSignerAddress(
cluster: Cluster,
serum3Market: Serum3Market,
serum3MarketExternal: Market,
): Promise<PublicKey> {
return await PublicKey.createProgramAddress(
[
serum3Market.serumMarketExternal.toBuffer(),
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
Buffer,
'le',
8,
),
],
SERUM3_PROGRAM_ID[cluster],
);
}

View File

@ -1,6 +1,4 @@
import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor';
import { getFeeRates, getFeeTier } from '@project-serum/serum';
import { Order } from '@project-serum/serum/lib/market';
import {
closeAccount,
initializeAccount,
@ -37,6 +35,7 @@ import {
import { StubOracle } from './accounts/oracle';
import { OrderType, PerpMarket, Side } from './accounts/perp';
import {
generateSerum3MarketExternalVaultSignerAddress,
Serum3Market,
Serum3OrderType,
Serum3SelfTradeBehavior,
@ -678,9 +677,13 @@ export class MangoClient {
mangoAccount: MangoAccount,
): Promise<MangoAccountData> {
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
mangoAccount,
]);
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[],
);
// Use our custom simulate fn in utils/anchor.ts so signing the tx is not required
this.program.provider.simulate = simulate;
@ -778,6 +781,7 @@ export class MangoClient {
group,
[mangoAccount],
[bank],
[],
);
const transaction = await this.program.methods
@ -852,6 +856,7 @@ export class MangoClient {
group,
[mangoAccount],
[bank],
[],
);
const transaction = await this.program.methods
@ -917,9 +922,11 @@ export class MangoClient {
public async serum3deregisterMarket(
group: Group,
serum3MarketName: string,
externalMarketPk: PublicKey,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
return await this.program.methods
.serum3DeregisterMarket()
@ -981,7 +988,8 @@ export class MangoClient {
mangoAccount: MangoAccount,
marketName: string,
): Promise<TransactionSignature> {
const serum3Market: Serum3Market = group.serum3MarketsMap.get(marketName)!;
const serum3Market: Serum3Market =
group.serum3MarketsMapByExternal.get(marketName)!;
return await this.program.methods
.serum3CreateOpenOrders()
@ -1000,9 +1008,11 @@ export class MangoClient {
public async serum3CloseOpenOrders(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const openOrders = mangoAccount.serum3.find(
(account) => account.marketIndex === serum3Market.marketIndex,
@ -1026,7 +1036,7 @@ export class MangoClient {
public async serum3PlaceOrder(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
side: Serum3Side,
price: number,
size: number,
@ -1035,46 +1045,41 @@ export class MangoClient {
clientOrderId: number,
limit: number,
) {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) {
await this.serum3CreateOpenOrders(group, mangoAccount, 'BTC/USDC');
mangoAccount = await this.getMangoAccount(mangoAccount);
}
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternalVaultSigner =
await PublicKey.createProgramAddress(
[
serum3Market.serumMarketExternal.toBuffer(),
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
Buffer,
'le',
8,
),
],
SERUM3_PROGRAM_ID[this.cluster],
await generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
);
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
mangoAccount,
]);
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[],
);
const limitPrice = serum3MarketExternal.priceNumberToLots(price);
const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size);
const feeTier = getFeeTier(0, 0 /** TODO: fix msrm/srm balance */);
const rates = getFeeRates(feeTier);
const maxQuoteQuantity = new BN(
serum3MarketExternal.decoded.quoteLotSize.toNumber() *
(1 + rates.taker) /** TODO: fix taker/maker */,
).mul(
serum3MarketExternal
.baseSizeNumberToLots(size)
.mul(serum3MarketExternal.priceNumberToLots(price)),
);
const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize
.mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly)))
.mul(
serum3MarketExternal
.baseSizeNumberToLots(size)
.mul(serum3MarketExternal.priceNumberToLots(price)),
);
const payerTokenIndex = (() => {
if (side == Serum3Side.bid) {
return serum3Market.quoteTokenIndex;
@ -1125,13 +1130,16 @@ export class MangoClient {
async serum3CancelAllorders(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
limit: number,
) {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
return await this.program.methods
.serum3CancelAllOrders(limit)
@ -1154,25 +1162,19 @@ export class MangoClient {
async serum3SettleFunds(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternalVaultSigner =
// TODO: put into a helper method, and remove copy pasta
await PublicKey.createProgramAddress(
[
serum3Market.serumMarketExternal.toBuffer(),
serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike(
Buffer,
'le',
8,
),
],
SERUM3_PROGRAM_ID[this.cluster],
await generateSerum3MarketExternalVaultSignerAddress(
this.cluster,
serum3Market,
serum3MarketExternal,
);
return await this.program.methods
@ -1204,14 +1206,17 @@ export class MangoClient {
async serum3CancelOrder(
group: Group,
mangoAccount: MangoAccount,
serum3MarketName: string,
externalMarketPk: PublicKey,
side: Serum3Side,
orderId: BN,
): Promise<TransactionSignature> {
const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!;
const serum3Market = group.serum3MarketsMapByExternal.get(
externalMarketPk.toBase58(),
)!;
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
const serum3MarketExternal = group.serum3MarketExternalsMap.get(
externalMarketPk.toBase58(),
)!;
return await this.program.methods
.serum3CancelOrder(side, orderId)
@ -1230,20 +1235,6 @@ export class MangoClient {
.rpc();
}
async getSerum3Orders(
group: Group,
serum3MarketName: string,
): Promise<Order[]> {
const serum3MarketExternal =
group.serum3MarketExternalsMap.get(serum3MarketName)!;
// TODO: filter for mango account
return await serum3MarketExternal.loadOrdersForOwner(
this.program.provider.connection,
group.publicKey,
);
}
/// perps
async perpCreateMarket(
@ -1466,9 +1457,13 @@ export class MangoClient {
const perpMarket = group.perpMarketsMap.get(perpMarketName)!;
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [
mangoAccount,
]);
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[perpMarket],
);
const [nativePrice, nativeQuantity] = perpMarket.uiToNativePriceQuantity(
price,
@ -1539,6 +1534,7 @@ export class MangoClient {
group,
[mangoAccount],
[inputBank, outputBank],
[],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
@ -1723,6 +1719,7 @@ export class MangoClient {
group,
[liqor, liqee],
[assetBank, liabBank],
[],
);
const parsedHealthAccounts = healthRemainingAccounts.map(
@ -1798,31 +1795,39 @@ export class MangoClient {
/// private
// todo make private
public buildHealthRemainingAccounts(
retriever: AccountRetriever,
group: Group,
mangoAccounts: MangoAccount[],
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
banks: Bank[],
perpMarkets: PerpMarket[],
): PublicKey[] {
if (retriever === AccountRetriever.Fixed) {
return this.buildFixedAccountRetrieverHealthAccounts(
group,
mangoAccounts[0],
banks,
perpMarkets,
);
} else {
return this.buildScanningAccountRetrieverHealthAccounts(
group,
mangoAccounts,
banks,
perpMarkets,
);
}
}
// todo make private
public buildFixedAccountRetrieverHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
// Banks and perpMarkets for whom positions don't exist on mango account,
// but user would potentially open new positions.
banks: Bank[],
perpMarkets: PerpMarket[],
): PublicKey[] {
const healthRemainingAccounts: PublicKey[] = [];
@ -1853,11 +1858,7 @@ export class MangoClient {
healthRemainingAccounts.push(
...mintInfos.map((mintInfo) => mintInfo.oracle),
);
healthRemainingAccounts.push(
...mangoAccount.serum3
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
healthRemainingAccounts.push(
...mangoAccount.perps
.filter((perp) => perp.marketIndex !== 65535)
@ -1868,14 +1869,36 @@ export class MangoClient {
)[0].publicKey,
),
);
for (const perpMarket of perpMarkets) {
const alreadyAdded = mangoAccount.perps.find(
(p) => p.marketIndex === perpMarket.perpMarketIndex,
);
if (!alreadyAdded) {
healthRemainingAccounts.push(
Array.from(group.perpMarketsMap.values()).filter(
(p) => p.perpMarketIndex === perpMarket.perpMarketIndex,
)[0].publicKey,
);
}
}
healthRemainingAccounts.push(
...mangoAccount.serum3
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
// debugHealthAccounts(group, mangoAccount, healthRemainingAccounts);
return healthRemainingAccounts;
}
// todo make private
public buildScanningAccountRetrieverHealthAccounts(
group: Group,
mangoAccounts: MangoAccount[],
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
banks: Bank[],
perpMarkets: PerpMarket[],
): PublicKey[] {
const healthRemainingAccounts: PublicKey[] = [];
@ -1903,6 +1926,7 @@ export class MangoClient {
healthRemainingAccounts.push(
...mintInfos.map((mintInfo) => mintInfo.oracle),
);
for (const mangoAccount of mangoAccounts) {
healthRemainingAccounts.push(
...mangoAccount.serum3
@ -1910,6 +1934,7 @@ export class MangoClient {
.map((serum3Account) => serum3Account.openOrders),
);
}
for (const mangoAccount of mangoAccounts) {
healthRemainingAccounts.push(
...mangoAccount.perps
@ -1922,6 +1947,20 @@ export class MangoClient {
),
);
}
for (const mangoAccount of mangoAccounts) {
for (const perpMarket of perpMarkets) {
const alreadyAdded = mangoAccount.perps.find(
(p) => p.marketIndex === perpMarket.perpMarketIndex,
);
if (!alreadyAdded) {
healthRemainingAccounts.push(
Array.from(group.perpMarketsMap.values()).filter(
(p) => p.perpMarketIndex === perpMarket.perpMarketIndex,
)[0].publicKey,
);
}
}
}
return healthRemainingAccounts;
}

View File

@ -20,10 +20,15 @@ async function debugUser(
console.log(
'buildFixedAccountRetrieverHealthAccounts ' +
client
.buildFixedAccountRetrieverHealthAccounts(group, mangoAccount, [
group.banksMapByName.get('BTC')[0],
group.banksMapByName.get('USDC')[0],
])
.buildFixedAccountRetrieverHealthAccounts(
group,
mangoAccount,
[
group.banksMapByName.get('BTC')[0],
group.banksMapByName.get('USDC')[0],
],
[],
)
.map((pk) => pk.toBase58())
.join(', '),
);

View File

@ -17,6 +17,8 @@ import { MANGO_V4_ID } from '../constants';
const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
['ETH/USDC', 'BkAraCyL9TTLbeMY3L1VWrPcv32DvSi5QDDQjik1J6Ac'],
['SRM/USDC', '249LDNPLLL29nRq8kjBTg9hKdXMcZf4vK2UvxszZYcuZ'],
]);
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
@ -24,12 +26,16 @@ const DEVNET_MINTS = new Map([
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
]);
const DEVNET_ORACLES = new Map([
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
@ -88,19 +94,19 @@ async function main() {
0.1,
0, // tokenIndex
'USDC',
0.01,
0.4,
0.07,
0.8,
0.9,
1.5,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.0005,
0.8,
0.6,
1.2,
1.4,
0.02,
1,
1,
1,
1,
0,
);
await group.reloadAll(client);
} catch (error) {}
@ -117,19 +123,19 @@ async function main() {
0.1,
1, // tokenIndex
'BTC',
0.01,
0.4,
0.07,
0.8,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.88,
0.0005,
0.0005,
0.8,
0.6,
1.1,
1.2,
1.4,
0.02,
0.05,
);
await group.reloadAll(client);
} catch (error) {
@ -148,19 +154,19 @@ async function main() {
0.1,
2, // tokenIndex
'SOL',
0.01,
0.4,
0.07,
0.8,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.63,
0.0005,
0.0005,
0.8,
0.6,
1.1,
1.2,
1.4,
0.02,
0.05,
);
await group.reloadAll(client);
} catch (error) {
@ -198,14 +204,75 @@ async function main() {
console.log(error);
}
// register token 4
// register token 7
console.log(`Registering ETH...`);
const ethDevnetMint = new PublicKey(DEVNET_MINTS.get('ETH')!);
const ethDevnetOracle = new PublicKey(DEVNET_ORACLES.get('ETH')!);
try {
await client.tokenRegister(
group,
ethDevnetMint,
ethDevnetOracle,
0.1,
7, // tokenIndex
'ETH',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
// register token 5
console.log(`Registering SRM...`);
const srmDevnetMint = new PublicKey(DEVNET_MINTS.get('SRM')!);
const srmDevnetOracle = new PublicKey(DEVNET_ORACLES.get('SRM')!);
try {
await client.tokenRegister(
group,
srmDevnetMint,
srmDevnetOracle,
0.1,
5, // tokenIndex
'SRM',
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
0.05,
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
console.log(
`Editing group, setting existing admin as fastListingAdmin to be able to add MNGO truslessly...`,
);
let sig = await client.groupEdit(
group,
group.admin,
new PublicKey('Efhak3qj3MiyzgJr3cUUqXXz5wr3oYHt9sPzuqJf9eBN'),
group.admin,
undefined,
undefined,
);
@ -231,7 +298,7 @@ async function main() {
// register serum market
console.log(`Registering serum3 market...`);
const serumMarketExternalPk = new PublicKey(
let serumMarketExternalPk = new PublicKey(
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
try {
@ -251,7 +318,35 @@ async function main() {
group.getFirstBankByMint(btcDevnetMint).tokenIndex,
group.getFirstBankByMint(usdcDevnetMint).tokenIndex,
);
console.log(`...registerd serum3 market ${markets[0].publicKey}`);
console.log(`...registered serum3 market ${markets[0].publicKey}`);
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('ETH/USDC')!);
try {
await client.serum3RegisterMarket(
group,
serumMarketExternalPk,
group.getFirstBankByMint(ethDevnetMint),
group.getFirstBankByMint(usdcDevnetMint),
0,
'ETH/USDC',
);
} catch (error) {
console.log(error);
}
serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('SRM/USDC')!);
try {
await client.serum3RegisterMarket(
group,
serumMarketExternalPk,
group.getFirstBankByMint(srmDevnetMint),
group.getFirstBankByMint(usdcDevnetMint),
0,
'SRM/USDC',
);
} catch (error) {
console.log(error);
}
// register perp market
console.log(`Registering perp market...`);
@ -292,114 +387,93 @@ async function main() {
// edit
//
console.log(`Editing USDC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
btcDevnetOracle,
0.1,
undefined,
0.01,
0.3,
0.08,
0.81,
0.91,
0.75,
0.0007,
1.7,
0.9,
0.7,
1.3,
1.5,
0.04,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
} catch (error) {
throw error;
}
console.log(`Resetting USDC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.01,
0.4,
0.07,
0.8,
0.9,
1.5,
0.0005,
0.0005,
1.0,
1.0,
1.0,
1.0,
0.02,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(usdcDevnetMint).toString());
} catch (error) {
throw error;
}
if (true) {
console.log(`Editing USDC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
1,
1,
1,
1,
0,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.getFirstBankByMint(btcDevnetMint).toString());
} catch (error) {
throw error;
}
console.log(`Editing perp market...`);
try {
let sig = await client.perpEditMarket(
group,
'BTC-PERP',
btcDevnetOracle,
0.2,
0,
6,
0.9,
0.9,
1.035,
1.06,
0.013,
0.0003,
0.1,
0.07,
0.07,
1001,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.perpMarketsMap.get('BTC-PERP')!.toString());
} catch (error) {
console.log(error);
}
console.log(`Resetting perp market...`);
try {
let sig = await client.perpEditMarket(
group,
'BTC-PERP',
btcDevnetOracle,
0.1,
1,
6,
1,
0.95,
1.025,
1.05,
0.012,
0.0002,
0.0,
0.05,
0.05,
100,
);
console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`);
await group.reloadAll(client);
console.log(group.perpMarketsMap.get('BTC-PERP')!.toString());
} catch (error) {
console.log(error);
console.log(`Editing BTC...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
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;
}
console.log(`Editing SOL...`);
try {
let sig = await client.tokenEdit(
group,
usdcDevnetMint,
usdcDevnetOracle.publicKey,
0.1,
undefined,
0.004,
0.7,
0.1,
0.85,
0.2,
2.0,
0.005,
0.0005,
0.9,
0.8,
1.1,
1.2,
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;
}
}
process.exit();

View File

@ -0,0 +1,136 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair } from '@solana/web3.js';
import fs from 'fs';
import { Serum3Side } from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// script which shows how to close a mango account cleanly i.e. close all active positions, withdraw all tokens, etc.
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times
async function main() {
const options = AnchorProvider.defaultOptions();
// note: see note above
// options.commitment = 'finalized';
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
// user
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
try {
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`Found group ${group.publicKey.toBase58()}`);
// fetch account
const mangoAccount = (
await client.getMangoAccountsForOwner(group, user.publicKey)
)[0];
console.log(`...found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
// close mango account serum3 positions, closing might require cancelling orders and settling
for (const serum3Account of mangoAccount.serum3Active()) {
let orders = await client.getSerum3Orders(
group,
group.findSerum3Market(serum3Account.marketIndex)!.name,
);
for (const order of orders) {
console.log(
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
);
console.log(` - Cancelling order with ${order.orderId}`);
await client.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
await client.serum3SettleFunds(
group,
mangoAccount,
group.findSerum3Market(serum3Account.marketIndex)!.name,
);
await client.serum3CloseOpenOrders(
group,
mangoAccount,
group.findSerum3Market(serum3Account.marketIndex)!.name,
);
}
// we closed a serum account, this changes the health accounts we are passing in for future ixs
await mangoAccount.reload(client, group);
// withdraw all tokens
for (const token of mangoAccount.tokensActive()) {
let native = token.balance(
group.getFirstBankByTokenIndex(token.tokenIndex),
);
// to avoid rounding issues
if (native.toNumber() < 1) {
continue;
}
let nativeFlooredNumber = Math.floor(native.toNumber());
console.log(
`withdrawing token ${
group.getFirstBankByTokenIndex(token.tokenIndex).name
} native amount ${nativeFlooredNumber} `,
);
await client.tokenWithdrawNative(
group,
mangoAccount,
group.getFirstBankByTokenIndex(token.tokenIndex).mint,
nativeFlooredNumber - 1 /* see comment in token_withdraw in program */,
false,
);
}
// reload and print current positions
await mangoAccount.reload(client, group);
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
// close account
console.log(`Close mango account...`);
const res = await client.closeMangoAccount(group, mangoAccount);
} catch (error) {
console.log(error);
}
process.exit();
}
main();

View File

@ -0,0 +1,134 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// An example for users based on high level api i.e. the client
// Create
// process.env.USER_KEYPAIR - mango account owner keypair path
// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group)
//
// This script deposits some tokens, places some serum orders, cancels them, places some perp orders
//
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'],
['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'],
]);
const DEVNET_ORACLES = new Map([
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'],
['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'],
['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'],
]);
export const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const user = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')),
),
);
const userWallet = new Wallet(user);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'devnet',
MANGO_V4_ID['devnet'],
{},
'get-program-accounts',
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`${group}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
user.publicKey,
);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString(group));
if (true) {
// deposit and withdraw
try {
console.log(`...depositing`);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('USDC')!),
1000,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
100,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('ETH')!),
500,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SRM')!),
500,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('BTC')!),
1,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString(group));
} catch (error) {
console.log(error);
}
}
process.exit();
}
main();

View File

@ -94,7 +94,7 @@ async function main() {
// withdraw all tokens
for (const token of mangoAccount.tokensActive()) {
let native = token.native(
let native = token.balance(
group.getFirstBankByTokenIndex(token.tokenIndex),
);

View File

@ -1,6 +1,7 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { I80F48 } from '../accounts/I80F48';
import { HealthType } from '../accounts/mangoAccount';
import { OrderType, Side } from '../accounts/perp';
import {
@ -28,6 +29,10 @@ const DEVNET_MINTS = new Map([
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'],
]);
export const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')],
['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')],
]);
const GROUP_NUM = Number(process.env.GROUP_NUM || 0);
@ -61,7 +66,7 @@ async function main() {
),
);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
console.log(`${group}`);
// create + fetch account
console.log(`Creating mangoaccount...`);
@ -70,9 +75,11 @@ async function main() {
user.publicKey,
);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
console.log(mangoAccount.toString(group));
if (true) {
await mangoAccount.reload(client, group);
if (false) {
// set delegate, and change name
console.log(`...changing mango account name, and setting a delegate`);
const randomKey = new PublicKey(
@ -99,7 +106,8 @@ async function main() {
console.log(mangoAccount.toString());
}
if (true) {
if (false) {
// expand account
console.log(
`...expanding mango account to have serum3 and perp position slots`,
);
@ -107,11 +115,11 @@ async function main() {
await mangoAccount.reload(client, group);
}
if (true) {
if (false) {
// deposit and withdraw
try {
console.log(`...depositing 50 USDC`);
console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`);
await client.tokenDeposit(
group,
mangoAccount,
@ -120,6 +128,22 @@ async function main() {
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('SOL')!),
1,
);
await mangoAccount.reload(client, group);
await client.tokenDeposit(
group,
mangoAccount,
new PublicKey(DEVNET_MINTS.get('MNGO')!),
1,
);
await mangoAccount.reload(client, group);
console.log(`...withdrawing 1 USDC`);
await client.tokenWithdraw(
group,
@ -138,21 +162,44 @@ async function main() {
0.0005,
);
await mangoAccount.reload(client, group);
console.log(mangoAccount.toString(group));
} catch (error) {
console.log(error);
}
}
if (false) {
// 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')!,
);
const lowestAsk = Array.from(asks!)[0];
const bids = await group.loadSerum3BidsForMarket(
client,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
const highestBid = Array.from(asks!)![0];
let price = 20;
let qty = 0.0001;
console.log(
`...placing serum3 bid which would not be settled since its relatively low then midprice`,
`...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid,
20,
0.0001,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
@ -160,15 +207,18 @@ async function main() {
);
await mangoAccount.reload(client, group);
console.log(`...placing serum3 bid way above midprice`);
price = lowestAsk.price + lowestAsk.price / 2;
qty = 0.0001;
console.log(
`...placing serum3 bid way above midprice at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.bid,
90000,
0.0001,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
@ -176,15 +226,18 @@ async function main() {
);
await mangoAccount.reload(client, group);
console.log(`...placing serum3 ask way below midprice`);
price = highestBid.price - highestBid.price / 2;
qty = 0.0001;
console.log(
`...placing serum3 ask way below midprice at ${price} for ${qty}`,
);
await client.serum3PlaceOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
Serum3Side.ask,
30000,
0.0001,
price,
qty,
Serum3SelfTradeBehavior.decrementTake,
Serum3OrderType.limit,
Date.now(),
@ -192,10 +245,10 @@ async function main() {
);
console.log(`...current own orders on OB`);
let orders = await client.getSerum3Orders(
let orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
for (const order of orders) {
console.log(
@ -205,18 +258,17 @@ async function main() {
await client.serum3CancelOrder(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
order.orderId,
);
}
console.log(`...current own orders on OB`);
orders = await client.getSerum3Orders(
orders = await mangoAccount.loadSerum3OpenOrdersForMarket(
client,
group,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
for (const order of orders) {
console.log(order);
@ -226,12 +278,19 @@ async function main() {
await client.serum3SettleFunds(
group,
mangoAccount,
'BTC/USDC',
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
}
if (true) {
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);
console.log(
'...mangoAccount.getEquity() ' +
@ -252,13 +311,13 @@ async function main() {
console.log(
'...mangoAccount.getAssetsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getAssetsVal(HealthType.init).toNumber(),
mangoAccount.getAssetsValue(HealthType.init).toNumber(),
),
);
console.log(
'...mangoAccount.getLiabsVal() ' +
toUiDecimalsForQuote(
mangoAccount.getLiabsVal(HealthType.init).toNumber(),
mangoAccount.getLiabsValue(HealthType.init).toNumber(),
),
);
console.log(
@ -272,14 +331,80 @@ async function main() {
).toNumber(),
),
);
console.log(
"...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " +
toUiDecimalsForQuote(
mangoAccount
.getSerum3MarketMarginAvailable(group, 'BTC/USDC')
.toNumber(),
),
}
if (true) {
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];
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(),
);
}
for (const srcToken of Array.from(group.banksMapByName.keys())) {
for (const tgtToken of Array.from(group.banksMapByName.keys())) {
getMaxSourceForTokenSwapWrapper(srcToken, tgtToken);
}
}
const maxQuoteForSerum3BidUi = mangoAccount.getMaxQuoteForSerum3BidUi(
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
console.log(
"...mangoAccount.getMaxQuoteForSerum3BidUi(group, 'BTC/USDC') " +
maxQuoteForSerum3BidUi,
);
const maxBaseForSerum3AskUi = mangoAccount.getMaxBaseForSerum3AskUi(
group,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
);
console.log(
"...mangoAccount.getMaxBaseForSerum3AskUi(group, 'BTC/USDC') " +
maxBaseForSerum3AskUi,
);
console.log(
`simHealthRatioWithSerum3BidUiChanges ${mangoAccount.simHealthRatioWithSerum3BidUiChanges(
group,
785,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
)}`,
);
console.log(
`simHealthRatioWithSerum3AskUiChanges ${mangoAccount.simHealthRatioWithSerum3AskUiChanges(
group,
0.033,
DEVNET_SERUM3_MARKETS.get('BTC/USDC')!,
)}`,
);
}
if (false) {
console.log(
"...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " +
toUiDecimalsForQuote(
@ -290,7 +415,7 @@ async function main() {
);
}
if (true) {
if (false) {
// perps
console.log(`...placing perp bid`);
try {

View File

@ -9,8 +9,11 @@ import {
TransactionInstruction,
} from '@solana/web3.js';
import BN from 'bn.js';
import { QUOTE_DECIMALS } from './accounts/bank';
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';
export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64);
@ -26,6 +29,58 @@ export function debugAccountMetas(ams: AccountMeta[]) {
}
}
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) => {
return [
serum3.openOrders.toBase58(),
`${
Array.from(group.serum3MarketsMapByExternal.values()).find(
(serum3Market) => serum3Market.marketIndex === serum3.marketIndex,
).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 async function findOrCreate<T>(
entityName: string,
findMethod: (...x: any) => any,