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 ts/client/**/*.js.map
migrations/*.js migrations/*.js
migrations/*.js.map 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 */ /** @internal */
export const ONE_I80F48 = I80F48.fromString('1'); export const ONE_I80F48 = I80F48.fromNumber(1);
/** @internal */ /** @internal */
export const ZERO_I80F48 = I80F48.fromString('0'); export const ZERO_I80F48 = I80F48.fromNumber(0);
/** @internal */ /** @internal */
export const NEG_ONE_I80F48 = I80F48.fromString('-1'); export const NEG_ONE_I80F48 = I80F48.fromNumber(-1);
export const HUNDRED_I80F48 = I80F48.fromString('100'); export const HUNDRED_I80F48 = I80F48.fromNumber(100);
export const MAX_I80F48 = new I80F48(I80F48.MAX_BN); export const MAX_I80F48 = new I80F48(I80F48.MAX_BN);

View File

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

View File

@ -11,6 +11,7 @@ import {
ZERO_I80F48, ZERO_I80F48,
} from './I80F48'; } from './I80F48';
import { HealthType } from './mangoAccount'; 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 // warning: this code is copy pasta from rust, keep in sync with health.rs
export class HealthCache { export class HealthCache {
tokenInfos: TokenInfo[]; constructor(
serum3Infos: Serum3Info[]; public tokenInfos: TokenInfo[],
perpInfos: PerpInfo[]; public serum3Infos: Serum3Info[],
public perpInfos: PerpInfo[],
) {}
constructor(dto: HealthCacheDto) { static fromDto(dto) {
this.tokenInfos = dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)); return new HealthCache(
this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto)); dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)),
this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto)); dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)),
dto.perpInfos.map((dto) => new PerpInfo(dto)),
);
} }
public health(healthType: HealthType): I80F48 { public health(healthType: HealthType): I80F48 {
@ -172,10 +177,54 @@ export class HealthCache {
return this.findTokenInfoIndex(bank.tokenIndex); return this.findTokenInfoIndex(bank.tokenIndex);
} }
private static logHealthCache(debug: string, healthCache: HealthCache) { adjustSerum3Reserved(
console.log(debug); // 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) { 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( console.log(
` assets ${healthCache.assets( ` assets ${healthCache.assets(
@ -213,6 +262,118 @@ export class HealthCache {
return adjustedCache.healthRatio(healthType); 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( getMaxSourceForTokenSwap(
group: Group, group: Group,
sourceMintPk: PublicKey, sourceMintPk: PublicKey,
@ -287,54 +448,10 @@ export class HealthCache {
.max(ZERO_I80F48); .max(ZERO_I80F48);
const cache0 = cacheAfterSwap(point0Amount); const cache0 = cacheAfterSwap(point0Amount);
const point0Ratio = cache0.healthRatio(HealthType.init); const point0Ratio = cache0.healthRatio(HealthType.init);
const point0Health = cache0.health(HealthType.init);
const cache1 = cacheAfterSwap(point1Amount); const cache1 = cacheAfterSwap(point1Amount);
const point1Ratio = cache1.healthRatio(HealthType.init); const point1Ratio = cache1.healthRatio(HealthType.init);
const point1Health = cache1.health(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; let amount: I80F48;
if ( if (
@ -365,30 +482,24 @@ export class HealthCache {
const zeroHealthAmount = point1Amount.add( const zeroHealthAmount = point1Amount.add(
point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)), 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); const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount);
// console.log(`zeroHealthRatio ${zeroHealthRatio}`); amount = HealthCache.binaryApproximationSearch(
amount = binaryApproximationSearch(
point1Amount, point1Amount,
point1Ratio, point1Ratio,
zeroHealthAmount, zeroHealthAmount,
zeroHealthRatio, zeroHealthRatio,
minRatio, minRatio,
healthRatioAfterSwap,
); );
} else if (point0Ratio.gte(minRatio)) { } else if (point0Ratio.gte(minRatio)) {
// Must be between point0Amount and point1Amount. // Must be between point0Amount and point1Amount.
amount = binaryApproximationSearch( amount = HealthCache.binaryApproximationSearch(
point0Amount, point0Amount,
point0Ratio, point0Ratio,
point1Amount, point1Amount,
point1Ratio, point1Ratio,
minRatio, minRatio,
healthRatioAfterSwap,
); );
} else { } else {
throw new Error( 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 { export class TokenInfo {
@ -468,20 +695,30 @@ export class TokenInfo {
} }
toString() { 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 { export class Serum3Info {
constructor(dto: Serum3InfoDto) { constructor(
this.reserved = I80F48.from(dto.reserved); public reserved: I80F48,
this.baseIndex = dto.baseIndex; public baseIndex: number,
this.quoteIndex = dto.quoteIndex; public quoteIndex: number,
} public marketIndex: number,
) {}
reserved: I80F48; static fromDto(dto: Serum3InfoDto) {
baseIndex: number; return new Serum3Info(
quoteIndex: number; I80F48.from(dto.reserved),
dto.baseIndex,
dto.quoteIndex,
dto.marketIndex,
);
}
healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 { healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 {
const baseInfo = tokenInfos[this.baseIndex]; const baseInfo = tokenInfos[this.baseIndex];
@ -512,7 +749,6 @@ export class Serum3Info {
assetPart = maxBalance; assetPart = maxBalance;
liabPart = reserved.sub(maxBalance); liabPart = reserved.sub(maxBalance);
} }
const assetWeight = tokenInfo.assetWeight(healthType); const assetWeight = tokenInfo.assetWeight(healthType);
const liabWeight = tokenInfo.liabWeight(healthType); const liabWeight = tokenInfo.liabWeight(healthType);
return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart)); return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart));
@ -522,6 +758,14 @@ export class Serum3Info {
const reservedAsQuote = computeHealthEffect(quoteInfo); const reservedAsQuote = computeHealthEffect(quoteInfo);
return reservedAsBase.min(reservedAsQuote); 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 { export class PerpInfo {
@ -578,12 +822,39 @@ export class TokenInfoDto {
balance: I80F48Dto; balance: I80F48Dto;
// in health-reference-token native units // in health-reference-token native units
serum3MaxReserved: I80F48Dto; 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 { export class Serum3InfoDto {
reserved: I80F48Dto; reserved: I80F48Dto;
baseIndex: number; baseIndex: number;
quoteIndex: number; quoteIndex: number;
marketIndex: number;
constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) {
this.reserved = reserved;
this.baseIndex = baseIndex;
this.quoteIndex = quoteIndex;
}
} }
export class PerpInfoDto { export class PerpInfoDto {

View File

@ -1,5 +1,6 @@
import { BN } from '@project-serum/anchor'; import { BN } from '@project-serum/anchor';
import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; 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 { PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client'; import { MangoClient } from '../client';
import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils'; import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils';
@ -7,6 +8,7 @@ import { Bank } from './bank';
import { Group } from './group'; import { Group } from './group';
import { HealthCache, HealthCacheDto } from './healthCache'; import { HealthCache, HealthCacheDto } from './healthCache';
import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48';
import { Serum3Market, Serum3Side } from './serum3';
export class MangoAccount { export class MangoAccount {
public tokens: TokenPosition[]; public tokens: TokenPosition[];
public serum3: Serum3Orders[]; public serum3: Serum3Orders[];
@ -89,6 +91,18 @@ export class MangoAccount {
return this; 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 { findToken(tokenIndex: number): TokenPosition | undefined {
return this.tokens.find((ta) => ta.tokenIndex == tokenIndex); return this.tokens.find((ta) => ta.tokenIndex == tokenIndex);
} }
@ -406,21 +420,184 @@ export class MangoAccount {
.toNumber(); .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. * @param group
* It assumes quote asset is always quote, * @param serum3Market
* it assumes that there are no interaction effects, * @returns maximum native quote which can be traded for base token given current health
* it assumes that there are no existing borrows for either of the tokens in the market.
*/ */
getSerum3MarketMarginAvailable(group: Group, marketName: string): I80F48 { public getMaxQuoteForSerum3Bid(
const initHealth = (this.accountData as MangoAccountData).initHealth; group: Group,
const serum3Market = group.serum3MarketsMap.get(marketName)!; serum3Market: Serum3Market,
const marketAssetWeight = group.getFirstBankByTokenIndex( ): I80F48 {
serum3Market.baseTokenIndex, return this.accountData.healthCache.getMaxForSerum3Order(
).initAssetWeight; group,
return initHealth.div(ONE_I80F48.sub(marketAssetWeight)); 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)); 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 { toString(group?: Group): string {
let res = 'MangoAccount'; let res = 'MangoAccount';
res = res + '\n pk: ' + this.publicKey.toString(); res = res + '\n pk: ' + this.publicKey.toString();
@ -701,7 +866,7 @@ export class MangoAccountData {
tokenAssets: any; tokenAssets: any;
}) { }) {
return new MangoAccountData( return new MangoAccountData(
new HealthCache(event.healthCache), HealthCache.fromDto(event.healthCache),
I80F48.from(event.initHealth), I80F48.from(event.initHealth),
I80F48.from(event.maintHealth), I80F48.from(event.maintHealth),
Equity.from(event.equity), Equity.from(event.equity),

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,11 @@ import {
TransactionInstruction, TransactionInstruction,
} from '@solana/web3.js'; } from '@solana/web3.js';
import BN from 'bn.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 { 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); 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>( export async function findOrCreate<T>(
entityName: string, entityName: string,
findMethod: (...x: any) => any, findMethod: (...x: any) => any,