diff --git a/.gitignore b/.gitignore index dd40eca29..b3e84ddc6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ ts/client/**/*.js ts/client/**/*.js.map migrations/*.js migrations/*.js.map + +ts/client/src/scripts/archive/ts.ts \ No newline at end of file diff --git a/Program b/Program new file mode 100644 index 000000000..e69de29bb diff --git a/ts/client/src/accounts/I80F48.ts b/ts/client/src/accounts/I80F48.ts index ec920cccc..9c67c95f5 100644 --- a/ts/client/src/accounts/I80F48.ts +++ b/ts/client/src/accounts/I80F48.ts @@ -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); diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index e88bd6e4b..a49c82c13 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -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, public banksMapByMint: Map, public banksMapByTokenIndex: Map, - public serum3MarketsMap: Map, + public serum3MarketsMapByExternal: Map, public serum3MarketExternalsMap: Map, + // TODO rethink key public perpMarketsMap: Map, public mintInfosMapByTokenIndex: Map, public mintInfosMapByMint: Map, @@ -77,12 +83,6 @@ export class Group { public vaultAmountsMap: Map, ) {} - 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 { + return await this.serum3MarketsMapByExternal + .get(externalMarketPk.toBase58()) + .loadBids(client, this); + } + + public async loadSerum3AsksForMarket( + client: MangoClient, + externalMarketPk: PublicKey, + ): Promise { + 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 diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 55b6e0a7a..1f7eb9187 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -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 { diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 52312a41e..38cb7b52c 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -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 { + 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), diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index cd656d107..03b117fa8 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -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 { + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + this.serumMarketExternal.toBase58(), + ); + return await serum3MarketExternal.loadBids( + client.program.provider.connection, + ); + } + + async loadAsks(client: MangoClient, group: Group): Promise { + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + this.serumMarketExternal.toBase58(), + ); + return await serum3MarketExternal.loadAsks( + client.program.provider.connection, + ); + } + + async logOb(client: MangoClient, group: Group): Promise { + 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 { + return await PublicKey.createProgramAddress( + [ + serum3Market.serumMarketExternal.toBuffer(), + serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike( + Buffer, + 'le', + 8, + ), + ], + SERUM3_PROGRAM_ID[cluster], + ); +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 866f44569..ee99f52df 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -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 { 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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; } diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index b5e70883d..764adc27a 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -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(', '), ); diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index 81f808413..7db068930 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -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(); diff --git a/ts/client/src/scripts/devnet-user-2-close-account.ts b/ts/client/src/scripts/devnet-user-2-close-account.ts new file mode 100644 index 000000000..52afc4907 --- /dev/null +++ b/ts/client/src/scripts/devnet-user-2-close-account.ts @@ -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(); diff --git a/ts/client/src/scripts/devnet-user-2.ts b/ts/client/src/scripts/devnet-user-2.ts new file mode 100644 index 000000000..14b532968 --- /dev/null +++ b/ts/client/src/scripts/devnet-user-2.ts @@ -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(); diff --git a/ts/client/src/scripts/devnet-user-close-account.ts b/ts/client/src/scripts/devnet-user-close-account.ts index a7728065b..6eb7538b0 100644 --- a/ts/client/src/scripts/devnet-user-close-account.ts +++ b/ts/client/src/scripts/devnet-user-close-account.ts @@ -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), ); diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index 17d2338c3..b450aad96 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -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 { diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index 2643d5f95..31a4bd4f9 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -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( entityName: string, findMethod: (...x: any) => any,