From 3f6cb9356604a08efe649b264c85d8955a759781 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Thu, 16 May 2024 21:37:52 +0200 Subject: [PATCH] health cache, close oo account fix, some typings, idl update Signed-off-by: microwavedcola1 more ts client update Signed-off-by: microwavedcola1 testing script Signed-off-by: microwavedcola1 adjust max spot bid/ask family of functions for openbook v2 Signed-off-by: microwavedcola1 --- .../openbook_v2_close_open_orders.rs | 2 +- ts/client/scripts/archive/devnet-user.ts | 987 +++++++++--------- ts/client/src/accounts/group.ts | 10 +- ts/client/src/accounts/healthCache.spec.ts | 16 +- ts/client/src/accounts/healthCache.ts | 185 +++- ts/client/src/accounts/mangoAccount.ts | 240 ++++- ts/client/src/accounts/openbookV2.ts | 8 +- ts/client/src/client.ts | 43 +- ts/client/src/mango_v4.ts | 12 +- 9 files changed, 900 insertions(+), 603 deletions(-) diff --git a/programs/mango-v4/src/instructions/openbook_v2_close_open_orders.rs b/programs/mango-v4/src/instructions/openbook_v2_close_open_orders.rs index 702a12fd57..2ca5f5f639 100644 --- a/programs/mango-v4/src/instructions/openbook_v2_close_open_orders.rs +++ b/programs/mango-v4/src/instructions/openbook_v2_close_open_orders.rs @@ -102,7 +102,7 @@ fn cpi_close_open_orders(ctx: &OpenbookV2CloseOpenOrders, seeds: &[&[&[u8]]]) -> seeds, ); openbook_v2::cpi::close_open_orders_indexer(cpi_ctx)?; - } +} Ok(()) } diff --git a/ts/client/scripts/archive/devnet-user.ts b/ts/client/scripts/archive/devnet-user.ts index fb7a1a45fa..ee5fb3707b 100644 --- a/ts/client/scripts/archive/devnet-user.ts +++ b/ts/client/scripts/archive/devnet-user.ts @@ -1,10 +1,9 @@ -import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { expect } from 'chai'; import fs from 'fs'; import { Group } from '../../src/accounts/group'; -import { MangoAccount } from '../../src/accounts/mangoAccount'; -import { PerpOrderSide, PerpOrderType } from '../../src/accounts/perp'; +import { HealthType, MangoAccount } from '../../src/accounts/mangoAccount'; import { MangoClient } from '../../src/client'; import { MANGO_V4_ID } from '../../src/constants'; import { toUiDecimalsForQuote } from '../../src/utils'; @@ -31,10 +30,7 @@ const GROUP_NUM = Number(process.env.GROUP_NUM || 0); async function main(): Promise { const options = AnchorProvider.defaultOptions(); - const connection = new Connection( - 'https://mango.devnet.rpcpool.com', - options, - ); + const connection = new Connection(process.env.DEVNET_CLUSTER_URL!, options); const user = Keypair.fromSecretKey( Buffer.from( @@ -59,24 +55,32 @@ async function main(): Promise { JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')), ), ); - const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + // const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + const group = await client.getGroup( + new PublicKey('CKU8J1mgtdcJJhBvXrH6xRx1MmcrRWDv8WQdNxPKW3gk'), + ); // create + fetch account console.log(`Creating mangoaccount...`); - const mangoAccount = (await client.getMangoAccountForOwner( + let mangoAccount = (await client.getMangoAccountForOwner( group, user.publicKey, - 0, + 2, )) as MangoAccount; - await mangoAccount!.reload(client); if (!mangoAccount) { - throw new Error(`MangoAccount not found for user ${user.publicKey}`); + await client.createMangoAccount(group, 2, 'one', 2, 0, 0, 0); + mangoAccount = (await client.getMangoAccountForOwner( + group, + user.publicKey, + 2, + )) as MangoAccount; } + await mangoAccount!.reload(client); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); // set delegate, and change name // eslint-disable-next-line no-constant-condition - if (true) { + if (false) { console.log(`...changing mango account name, and setting a delegate`); const newName = 'my_changed_name'; const randomKey = new PublicKey( @@ -102,34 +106,31 @@ async function main(): Promise { } // expand account - if ( - mangoAccount.tokens.length < 16 || - mangoAccount.serum3.length < 8 || - mangoAccount.perps.length < 8 || - mangoAccount.perpOpenOrders.length < 8 - ) { + if (false) { console.log( `...expanding mango account to max 16 token positions, 8 serum3, 8 perp position and 8 perp oo slots, previous (tokens ${mangoAccount.tokens.length}, serum3 ${mangoAccount.serum3.length}, perps ${mangoAccount.perps.length}, perps oo ${mangoAccount.perpOpenOrders.length})`, ); - const sig = await client.expandMangoAccount( + const sig = await client.accountExpandV3( group, mangoAccount, - 16, - 8, - 8, - 8, + 2, + 0, + 0, + 0, + 0, + 2, ); console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); await mangoAccount.reload(client); - expect(mangoAccount.tokens.length).equals(16); - expect(mangoAccount.serum3.length).equals(8); - expect(mangoAccount.perps.length).equals(8); - expect(mangoAccount.perpOpenOrders.length).equals(8); + // expect(mangoAccount.tokens.length).equals(16); + // expect(mangoAccount.serum3.length).equals(8); + // expect(mangoAccount.perps.length).equals(8); + // expect(mangoAccount.perpOpenOrders.length).equals(8); } // deposit and withdraw // eslint-disable-next-line no-constant-condition - if (true) { + if (false) { console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`); // deposit USDC @@ -159,43 +160,43 @@ async function main(): Promise { ); await mangoAccount.reload(client); - // deposit MNGO - await client.tokenDeposit( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('MNGO')!), - 1, - ); - await mangoAccount.reload(client); - - // withdraw USDC - console.log(`...withdrawing 1 USDC`); - oldBalance = mangoAccount.getTokenBalance( - group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), - ); - await client.tokenWithdraw( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('USDC')!), - 1, - true, - ); - await mangoAccount.reload(client); - newBalance = mangoAccount.getTokenBalance( - group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), - ); - expect(toUiDecimalsForQuote(oldBalance.sub(newBalance)).toString()).equals( - '1', - ); - - console.log(`...depositing 0.0005 BTC`); - await client.tokenDeposit( - group, - mangoAccount, - new PublicKey(DEVNET_MINTS.get('BTC')!), - 0.0005, - ); - await mangoAccount.reload(client); + // // deposit MNGO + // await client.tokenDeposit( + // group, + // mangoAccount, + // new PublicKey(DEVNET_MINTS.get('MNGO')!), + // 1, + // ); + // await mangoAccount.reload(client); + + // // withdraw USDC + // console.log(`...withdrawing 1 USDC`); + // oldBalance = mangoAccount.getTokenBalance( + // group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), + // ); + // await client.tokenWithdraw( + // group, + // mangoAccount, + // new PublicKey(DEVNET_MINTS.get('USDC')!), + // 1, + // true, + // ); + // await mangoAccount.reload(client); + // newBalance = mangoAccount.getTokenBalance( + // group.getFirstBankByMint(new PublicKey(DEVNET_MINTS.get('USDC')!)), + // ); + // expect(toUiDecimalsForQuote(oldBalance.sub(newBalance)).toString()).equals( + // '1', + // ); + + // console.log(`...depositing 0.0005 BTC`); + // await client.tokenDeposit( + // group, + // mangoAccount, + // new PublicKey(DEVNET_MINTS.get('BTC')!), + // 0.0005, + // ); + // await mangoAccount.reload(client); } // Note: Disable for now until we have openbook devnet markets @@ -327,419 +328,473 @@ async function main(): Promise { // eslint-disable-next-line no-constant-condition if (true) { - await mangoAccount.reload(client); - console.log( - '...mangoAccount.getEquity() ' + - toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()), - ); - console.log( - '...mangoAccount.getCollateralValue() ' + - toUiDecimalsForQuote( - mangoAccount.getCollateralValue(group)!.toNumber(), - ), - ); - console.log( - '...mangoAccount.getAssetsVal() ' + - toUiDecimalsForQuote(mangoAccount.getAssetsValue(group)!.toNumber()), - ); - console.log( - '...mangoAccount.getLiabsVal() ' + - toUiDecimalsForQuote(mangoAccount.getLiabsValue(group)!.toNumber()), - ); - console.log( - '...mangoAccount.getMaxWithdrawWithBorrowForToken(group, "SOL") ' + - toUiDecimalsForQuote( - mangoAccount - .getMaxWithdrawWithBorrowForToken( - group, - new PublicKey(DEVNET_MINTS.get('SOL')!), - )! - .toNumber(), - ), - ); + console.log(mangoAccount.getHealth(group, HealthType.maint)); + // await mangoAccount.reload(client); + // console.log( + // '...mangoAccount.getEquity() ' + + // toUiDecimalsForQuote(mangoAccount.getEquity(group)!.toNumber()), + // ); + // console.log( + // '...mangoAccount.getCollateralValue() ' + + // toUiDecimalsForQuote( + // mangoAccount.getCollateralValue(group)!.toNumber(), + // ), + // ); + // console.log( + // '...mangoAccount.getAssetsVal() ' + + // toUiDecimalsForQuote(mangoAccount.getAssetsValue(group)!.toNumber()), + // ); + // console.log( + // '...mangoAccount.getLiabsVal() ' + + // toUiDecimalsForQuote(mangoAccount.getLiabsValue(group)!.toNumber()), + // ); + // console.log( + // '...mangoAccount.getMaxWithdrawWithBorrowForToken(group, "SOL") ' + + // toUiDecimalsForQuote( + // mangoAccount + // .getMaxWithdrawWithBorrowForToken( + // group, + // new PublicKey(DEVNET_MINTS.get('SOL')!), + // )! + // .toNumber(), + // ), + // ); } - // eslint-disable-next-line no-constant-condition - if (true) { - // eslint-disable-next-line no-inner-declarations - function getMaxSourceForTokenSwapWrapper(src, tgt): void { - console.log( - `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + - mangoAccount.getMaxSourceUiForTokenSwap( - group, - group.banksMapByName.get(src)![0].mint, - group.banksMapByName.get(tgt)![0].mint, - 1, - )!, - ); - } - 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, - ); + // // eslint-disable-next-line no-constant-condition + // if (true) { + // // eslint-disable-next-line no-inner-declarations + // function getMaxSourceForTokenSwapWrapper(src, tgt): void { + // console.log( + // `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + + // mangoAccount.getMaxSourceUiForTokenSwap( + // group, + // group.banksMapByName.get(src)![0].mint, + // group.banksMapByName.get(tgt)![0].mint, + // 1, + // )!, + // ); + // } + // for (const srcToken of Array.from(group.banksMapByName.keys())) { + // for (const tgtToken of Array.from(group.banksMapByName.keys())) { + // getMaxSourceForTokenSwapWrapper(srcToken, tgtToken); + // } + // } - const maxBaseForSerum3AskUi = mangoAccount.getMaxBaseForSerum3AskUi( - group, - DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, - ); - console.log( - "...mangoAccount.getMaxBaseForSerum3AskUi(group, 'BTC/USDC') " + - maxBaseForSerum3AskUi, - ); + // const maxQuoteForSerum3BidUi = mangoAccount.getMaxQuoteForSerum3BidUi( + // group, + // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + // ); + // console.log( + // "...mangoAccount.getMaxQuoteForSerum3BidUi(group, 'BTC/USDC') " + + // maxQuoteForSerum3BidUi, + // ); - 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')!, - )}`, - ); - } + // const maxBaseForSerum3AskUi = mangoAccount.getMaxBaseForSerum3AskUi( + // group, + // DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + // ); + // console.log( + // "...mangoAccount.getMaxBaseForSerum3AskUi(group, 'BTC/USDC') " + + // maxBaseForSerum3AskUi, + // ); - // perps - // eslint-disable-next-line no-constant-condition - if (true) { - let sig; - const perpMarket = group.getPerpMarketByName('BTC-PERP'); - const orders = await mangoAccount.loadPerpOpenOrdersForMarket( - client, - group, - perpMarket.perpMarketIndex, - ); - for (const order of orders) { - console.log( - `Current order - ${order.uiPrice} ${order.uiSize} ${order.side}`, - ); - } - console.log(`...cancelling all perp orders`); - sig = await client.perpCancelAllOrders( - group, - mangoAccount, - perpMarket.perpMarketIndex, - 10, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // 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')!, + // )}`, + // ); + // } - // oracle pegged - try { - const clientId = Math.floor(Math.random() * 99999); - const price = group.banksMapByName.get('BTC')![0].uiPrice!; - console.log( - `...placing perp pegged bid ${clientId} at oracle price ${perpMarket.uiPrice}`, - ); - const sig = await client.perpPlaceOrderPegged( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.bid, - -5, - perpMarket.uiPrice + 5, - 0.01, - price * 0.011, - clientId, - PerpOrderType.limit, - false, - 0, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - try { - const clientId = Math.floor(Math.random() * 99999); - const price = group.banksMapByName.get('BTC')![0].uiPrice!; - console.log( - `...placing perp pegged bid ${clientId} at oracle price ${perpMarket.uiPrice}`, - ); - const sig = await client.perpPlaceOrderPegged( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.ask, - 5, - perpMarket.uiPrice - 5, - 0.01, - price * 0.011, - clientId, - PerpOrderType.limit, - false, - 0, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - - await logBidsAndAsks(client, group); - - sig = await client.perpCancelAllOrders( - group, - mangoAccount, - perpMarket.perpMarketIndex, - 10, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // // perps + // // eslint-disable-next-line no-constant-condition + // if (true) { + // let sig; + // const perpMarket = group.getPerpMarketByName('BTC-PERP'); + // const orders = await mangoAccount.loadPerpOpenOrdersForMarket( + // client, + // group, + // perpMarket.perpMarketIndex, + // ); + // for (const order of orders) { + // console.log( + // `Current order - ${order.uiPrice} ${order.uiSize} ${order.side}`, + // ); + // } + // console.log(`...cancelling all perp orders`); + // sig = await client.perpCancelAllOrders( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // 10, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - // scenario 1 - // bid max perp - try { - const clientId = Math.floor(Math.random() * 99999); - await mangoAccount.reload(client); - await group.reloadAll(client); - const price = - group.banksMapByName.get('BTC')![0].uiPrice! - - Math.floor(Math.random() * 100); - const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi( - group, - perpMarket.perpMarketIndex, - ); - const baseQty = quoteQty / price; - console.log( - ` simHealthRatioWithPerpBidUiChanges - ${mangoAccount.simHealthRatioWithPerpBidUiChanges( - group, - perpMarket.perpMarketIndex, - baseQty, - )}`, - ); - console.log( - `...placing max qty perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, - ); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.bid, - price, - baseQty, - quoteQty, - clientId, - PerpOrderType.limit, - false, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - console.log(`...cancelling all perp orders`); - sig = await client.perpCancelAllOrders( - group, - mangoAccount, - perpMarket.perpMarketIndex, - 10, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // // oracle pegged + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = group.banksMapByName.get('BTC')![0].uiPrice!; + // console.log( + // `...placing perp pegged bid ${clientId} at oracle price ${perpMarket.uiPrice}`, + // ); + // const sig = await client.perpPlaceOrderPegged( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.bid, + // -5, + // perpMarket.uiPrice + 5, + // 0.01, + // price * 0.011, + // clientId, + // PerpOrderType.limit, + // false, + // 0, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = group.banksMapByName.get('BTC')![0].uiPrice!; + // console.log( + // `...placing perp pegged bid ${clientId} at oracle price ${perpMarket.uiPrice}`, + // ); + // const sig = await client.perpPlaceOrderPegged( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.ask, + // 5, + // perpMarket.uiPrice - 5, + // 0.01, + // price * 0.011, + // clientId, + // PerpOrderType.limit, + // false, + // 0, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } - // bid max perp + some - try { - const clientId = Math.floor(Math.random() * 99999); - const price = - group.banksMapByName.get('BTC')![0].uiPrice! - - Math.floor(Math.random() * 100); - const quoteQty = - mangoAccount.getMaxQuoteForPerpBidUi( - group, - perpMarket.perpMarketIndex, - ) * 1.02; - - const baseQty = quoteQty / price; - console.log( - `...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, - ); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.bid, - price, - baseQty, - quoteQty, - clientId, - PerpOrderType.limit, - false, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - console.log('Errored out as expected'); - } - - // bid max ask - try { - const clientId = Math.floor(Math.random() * 99999); - const price = - group.banksMapByName.get('BTC')![0].uiPrice! + - Math.floor(Math.random() * 100); - const baseQty = mangoAccount.getMaxBaseForPerpAskUi( - group, - perpMarket.perpMarketIndex, - ); - console.log( - ` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges( - group, - perpMarket.perpMarketIndex, - baseQty, - )}`, - ); - const quoteQty = baseQty * price; - console.log( - `...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, - ); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.ask, - price, - baseQty, - quoteQty, - clientId, - PerpOrderType.limit, - false, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - - // bid max ask + some - try { - const clientId = Math.floor(Math.random() * 99999); - const price = - group.banksMapByName.get('BTC')![0].uiPrice! + - Math.floor(Math.random() * 100); - const baseQty = - mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) * - 1.02; - const quoteQty = baseQty * price; - console.log( - `...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, - ); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.ask, - price, - baseQty, - quoteQty, - clientId, - PerpOrderType.limit, - false, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - console.log('Errored out as expected'); - } - - console.log(`...cancelling all perp orders`); - sig = await client.perpCancelAllOrders( - group, - mangoAccount, - perpMarket.perpMarketIndex, - 10, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // await logBidsAndAsks(client, group); - // scenario 2 - // make + take orders - try { - const clientId = Math.floor(Math.random() * 99999); - const price = group.banksMapByName.get('BTC')![0].uiPrice!; - console.log(`...placing perp bid ${clientId} at ${price}`); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.bid, - price, - 0.01, - price * 0.01, - clientId, - PerpOrderType.limit, - false, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - try { - const clientId = Math.floor(Math.random() * 99999); - const price = group.banksMapByName.get('BTC')![0].uiPrice!; - console.log(`...placing perp ask ${clientId} at ${price}`); - const sig = await client.perpPlaceOrder( - group, - mangoAccount, - perpMarket.perpMarketIndex, - PerpOrderSide.ask, - price, - 0.01, - price * 0.011, - clientId, - PerpOrderType.limit, - false, - 0, //Date.now() + 200, - 1, - ); - console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); - } catch (error) { - console.log(error); - } - // // should be able to cancel them : know bug - // console.log(`...cancelling all perp orders`); - // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10); - // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // sig = await client.perpCancelAllOrders( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // 10, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + + // // scenario 1 + // // bid max perp + // try { + // const clientId = Math.floor(Math.random() * 99999); + // await mangoAccount.reload(client); + // await group.reloadAll(client); + // const price = + // group.banksMapByName.get('BTC')![0].uiPrice! - + // Math.floor(Math.random() * 100); + // const quoteQty = mangoAccount.getMaxQuoteForPerpBidUi( + // group, + // perpMarket.perpMarketIndex, + // ); + // const baseQty = quoteQty / price; + // console.log( + // ` simHealthRatioWithPerpBidUiChanges - ${mangoAccount.simHealthRatioWithPerpBidUiChanges( + // group, + // perpMarket.perpMarketIndex, + // baseQty, + // )}`, + // ); + // console.log( + // `...placing max qty perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + // ); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.bid, + // price, + // baseQty, + // quoteQty, + // clientId, + // PerpOrderType.limit, + // false, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } + // console.log(`...cancelling all perp orders`); + // sig = await client.perpCancelAllOrders( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // 10, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + + // // bid max perp + some + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = + // group.banksMapByName.get('BTC')![0].uiPrice! - + // Math.floor(Math.random() * 100); + // const quoteQty = + // mangoAccount.getMaxQuoteForPerpBidUi( + // group, + // perpMarket.perpMarketIndex, + // ) * 1.02; + + // const baseQty = quoteQty / price; + // console.log( + // `...placing max qty * 1.02 perp bid clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + // ); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.bid, + // price, + // baseQty, + // quoteQty, + // clientId, + // PerpOrderType.limit, + // false, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // console.log('Errored out as expected'); + // } - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - await perpMarket?.loadEventQueue(client)!; - const fr = perpMarket?.getInstantaneousFundingRateUi( - await perpMarket.loadBids(client), - await perpMarket.loadAsks(client), - ); - console.log(`current funding rate per hour is ${fr}`); + // // bid max ask + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = + // group.banksMapByName.get('BTC')![0].uiPrice! + + // Math.floor(Math.random() * 100); + // const baseQty = mangoAccount.getMaxBaseForPerpAskUi( + // group, + // perpMarket.perpMarketIndex, + // ); + // console.log( + // ` simHealthRatioWithPerpAskUiChanges - ${mangoAccount.simHealthRatioWithPerpAskUiChanges( + // group, + // perpMarket.perpMarketIndex, + // baseQty, + // )}`, + // ); + // const quoteQty = baseQty * price; + // console.log( + // `...placing max qty perp ask clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + // ); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.ask, + // price, + // baseQty, + // quoteQty, + // clientId, + // PerpOrderType.limit, + // false, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - const eq = await perpMarket?.loadEventQueue(client)!; - console.log( - `raw events - ${JSON.stringify(eq.eventsSince(new BN(0)), null, 2)}`, - ); + // // bid max ask + some + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = + // group.banksMapByName.get('BTC')![0].uiPrice! + + // Math.floor(Math.random() * 100); + // const baseQty = + // mangoAccount.getMaxBaseForPerpAskUi(group, perpMarket.perpMarketIndex) * + // 1.02; + // const quoteQty = baseQty * price; + // console.log( + // `...placing max qty perp ask * 1.02 clientId ${clientId} at price ${price}, base ${baseQty}, quote ${quoteQty}`, + // ); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.ask, + // price, + // baseQty, + // quoteQty, + // clientId, + // PerpOrderType.limit, + // false, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // console.log('Errored out as expected'); + // } - // sleep so that keeper can catch up - await new Promise((r) => setTimeout(r, 2000)); + // console.log(`...cancelling all perp orders`); + // sig = await client.perpCancelAllOrders( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // 10, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + + // // scenario 2 + // // make + take orders + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = group.banksMapByName.get('BTC')![0].uiPrice!; + // console.log(`...placing perp bid ${clientId} at ${price}`); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.bid, + // price, + // 0.01, + // price * 0.01, + // clientId, + // PerpOrderType.limit, + // false, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } + // try { + // const clientId = Math.floor(Math.random() * 99999); + // const price = group.banksMapByName.get('BTC')![0].uiPrice!; + // console.log(`...placing perp ask ${clientId} at ${price}`); + // const sig = await client.perpPlaceOrder( + // group, + // mangoAccount, + // perpMarket.perpMarketIndex, + // PerpOrderSide.ask, + // price, + // 0.01, + // price * 0.011, + // clientId, + // PerpOrderType.limit, + // false, + // 0, //Date.now() + 200, + // 1, + // ); + // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + // } catch (error) { + // console.log(error); + // } + // // // should be able to cancel them : know bug + // // console.log(`...cancelling all perp orders`); + // // sig = await client.perpCancelAllOrders(group, mangoAccount, perpMarket.perpMarketIndex, 10); + // // console.log(`sig https://explorer.solana.com/tx/${sig}?cluster=devnet`); + + // // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + // await perpMarket?.loadEventQueue(client)!; + // const fr = perpMarket?.getInstantaneousFundingRateUi( + // await perpMarket.loadBids(client), + // await perpMarket.loadAsks(client), + // ); + // console.log(`current funding rate per hour is ${fr}`); - // make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position or we see a small quotePositionNative - await group.reloadAll(client); - await mangoAccount.reload(client); - console.log(`${mangoAccount.toString(group)}`); + // // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain + // const eq = await perpMarket?.loadEventQueue(client)!; + // console.log( + // `raw events - ${JSON.stringify(eq.eventsSince(new BN(0)), null, 2)}`, + // ); + + // // sleep so that keeper can catch up + // await new Promise((r) => setTimeout(r, 2000)); + + // // make+take orders should have cancelled each other, and if keeper has already cranked, then should not appear in position or we see a small quotePositionNative + // await group.reloadAll(client); + // await mangoAccount.reload(client); + // console.log(`${mangoAccount.toString(group)}`); + // } + + // eslint-disable-next-line no-constant-condition + if (true) { + const market = Array.from( + group.openbookV2MarketsMapByMarketIndex.values(), + )[0]; + + // console.log(mangoAccount.openbookV2); + + // const sig = await client.openbookV2CreateOpenOrders( + // group, + // mangoAccount, + // market.openbookMarketExternal, + // ); + // console.log(sig.signature); + + // const sig1 = await client.openbookV2CloseOpenOrders( + // group, + // mangoAccount, + // market.openbookMarketExternal, + // ); + // console.log(sig1.signature); + + // const sig2 = await client.openbookV2PlaceOrder( + // group, + // mangoAccount, + // market.openbookMarketExternal, + // OpenbookV2Side.bid, + // 10, + // 1, + // OpenbookV2SelfTradeBehavior.decrementTake, + // OpenbookV2OrderType.limit, + // 0, + // 10, + // ); + // console.log(sig2.signature); + + // await mangoAccount.reload(client); + // console.log(mangoAccount.getOpenbookV2OoAccount(market.marketIndex)); + + console.log( + mangoAccount.getMaxBaseForSerum3AskUi( + group, + market.openbookMarketExternal, + ), + ); + console.log( + mangoAccount.getMaxQuoteForSerum3BidUi( + group, + market.openbookMarketExternal, + ), + ); } process.exit(); diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index e8e50f5e92..93151cfad1 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -1,10 +1,10 @@ -import { AnchorProvider, BorshAccountsCoder, Wallet } from '@coral-xyz/anchor'; -import { Market, Orderbook } from '@project-serum/serum'; +import { AnchorProvider, BorshAccountsCoder } from '@coral-xyz/anchor'; import { - MarketAccount, BookSideAccount, + MarketAccount, OpenBookV2Client, } from '@openbook-dex/openbook-v2'; +import { Market, Orderbook } from '@project-serum/serum'; import { parsePriceData } from '@pythnetwork/client'; import { TOKEN_PROGRAM_ID, unpackAccount } from '@solana/spl-token'; import { @@ -29,6 +29,7 @@ import { toUiDecimals, } from '../utils'; import { Bank, MintInfo, TokenIndex } from './bank'; +import { OpenbookV2Market } from './openbookV2'; import { OracleProvider, isPythOracle, @@ -37,8 +38,6 @@ import { } from './oracle'; import { BookSide, PerpMarket, PerpMarketIndex } from './perp'; import { MarketIndex, Serum3Market } from './serum3'; -import { OpenbookV2MarketIndex, OpenbookV2Market } from './openbookV2'; -import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'; export class Group { static from( @@ -339,6 +338,7 @@ export class Group { openbookV2Market, ]), ); + this.openbookV2MarketsMapByMarketIndex = new Map( openbookV2Markets.map((openbookV2Market) => [ openbookV2Market.marketIndex, diff --git a/ts/client/src/accounts/healthCache.spec.ts b/ts/client/src/accounts/healthCache.spec.ts index 924146fc62..4c5bc5110f 100644 --- a/ts/client/src/accounts/healthCache.spec.ts +++ b/ts/client/src/accounts/healthCache.spec.ts @@ -5,12 +5,12 @@ import range from 'lodash/range'; import { PublicKey } from '@solana/web3.js'; import { I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; +import { deepClone } from '../utils'; import { BankForHealth, StablePriceModel, TokenIndex } from './bank'; -import { HealthCache, PerpInfo, Serum3Info, TokenInfo } from './healthCache'; +import { HealthCache, PerpInfo, SpotInfo, TokenInfo } from './healthCache'; import { HealthType, PerpPosition, Serum3Orders } from './mangoAccount'; import { PerpMarket, PerpOrderSide } from './perp'; import { MarketIndex } from './serum3'; -import { deepClone } from '../utils'; function mockBankAndOracle( tokenIndex: TokenIndex, @@ -112,7 +112,7 @@ describe('Health Cache', () => { const ti1 = TokenInfo.fromBank(sourceBank, I80F48.fromNumber(100)); const ti2 = TokenInfo.fromBank(targetBank, I80F48.fromNumber(-10)); - const si1 = Serum3Info.fromOoModifyingTokenInfos( + const si1 = SpotInfo.fromSerum3OoModifyingTokenInfos( new Serum3Orders( PublicKey.default, 2 as MarketIndex, @@ -242,7 +242,7 @@ describe('Health Cache', () => { const ti2 = TokenInfo.fromBank(bank2, I80F48.fromNumber(fixture.token2)); const ti3 = TokenInfo.fromBank(bank3, I80F48.fromNumber(fixture.token3)); - const si1 = Serum3Info.fromOoModifyingTokenInfos( + const si1 = SpotInfo.fromSerum3OoModifyingTokenInfos( new Serum3Orders( PublicKey.default, 2 as MarketIndex, @@ -265,7 +265,7 @@ describe('Health Cache', () => { } as any as OpenOrders, ); - const si2 = Serum3Info.fromOoModifyingTokenInfos( + const si2 = SpotInfo.fromSerum3OoModifyingTokenInfos( new Serum3Orders( PublicKey.default, 3 as MarketIndex, @@ -962,15 +962,15 @@ describe('Health Cache', () => { { console.log(' - test 6'); const clonedHc: HealthCache = deepClone(hc); - clonedHc.serum3Infos = [ - new Serum3Info( + clonedHc.spotInfos = [ + new SpotInfo( I80F48.fromNumber(30 / 3), I80F48.fromNumber(30 / 2), ZERO_I80F48(), ZERO_I80F48(), 1, 0, - 0 as MarketIndex, + { marketIndex: 0 as MarketIndex, type: 'Serum3' }, ), ]; diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index f242b4498a..4711762c41 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -14,11 +14,12 @@ import { Group } from './group'; import { HealthType, MangoAccount, + OpenbookV2Orders, PerpPosition, Serum3Orders, } from './mangoAccount'; -import { PerpMarket, PerpMarketIndex, PerpOrder, PerpOrderSide } from './perp'; -import { MarketIndex, Serum3Market, Serum3Side } from './serum3'; +import { PerpMarket, PerpMarketIndex, PerpOrderSide } from './perp'; +import { MarketIndex, Serum3Side } from './serum3'; // ░░░░ // @@ -91,7 +92,7 @@ function spotAmountGivenForHealthZero( export class HealthCache { constructor( public tokenInfos: TokenInfo[], - public serum3Infos: Serum3Info[], + public spotInfos: SpotInfo[], public perpInfos: PerpInfo[], ) {} @@ -145,7 +146,7 @@ export class HealthCache { ); } - return Serum3Info.fromOoModifyingTokenInfos( + return SpotInfo.fromSerum3OoModifyingTokenInfos( serum3, baseInfoIndex, baseInfo, @@ -156,6 +157,40 @@ export class HealthCache { ); }); + const obv2Infos = mangoAccount.openbookV2Active().map((obv2) => { + const oo = mangoAccount.getOpenbookV2OoAccount(obv2.marketIndex); + + // find the TokenInfos for the market's base and quote tokens + const baseInfoIndex = tokenInfos.findIndex( + (tokenInfo) => tokenInfo.tokenIndex === obv2.baseTokenIndex, + ); + const baseInfo = tokenInfos[baseInfoIndex]; + if (!baseInfo) { + throw new Error( + `BaseInfo not found for market with marketIndex ${obv2.marketIndex}!`, + ); + } + const quoteInfoIndex = tokenInfos.findIndex( + (tokenInfo) => tokenInfo.tokenIndex === obv2.quoteTokenIndex, + ); + const quoteInfo = tokenInfos[quoteInfoIndex]; + if (!quoteInfo) { + throw new Error( + `QuoteInfo not found for market with marketIndex ${obv2.marketIndex}!`, + ); + } + + return SpotInfo.fromObv2OoModifyingTokenInfos( + obv2, + baseInfoIndex, + baseInfo, + quoteInfoIndex, + quoteInfo, + obv2.marketIndex, + oo, + ); + }); + // health contribution from perp accounts const perpInfos = mangoAccount.perpActive().map((perpPosition) => { const perpMarket = group.getPerpMarketByMarketIndex( @@ -164,7 +199,11 @@ export class HealthCache { return PerpInfo.fromPerpPosition(perpMarket, perpPosition); }); - return new HealthCache(tokenInfos, serum3Infos, perpInfos); + return new HealthCache( + tokenInfos, + [...serum3Infos, ...obv2Infos], + perpInfos, + ); } computeSerum3Reservations(healthType: HealthType | undefined): { @@ -180,7 +219,7 @@ export class HealthCache { // or reserved_quote was converted to base. const serum3Reserved: Serum3Reserved[] = []; - for (const info of this.serum3Infos) { + for (const info of this.spotInfos) { const quote = this.tokenInfos[info.quoteInfoIndex]; const base = this.tokenInfos[info.baseInfoIndex]; @@ -318,7 +357,7 @@ export class HealthCache { health.iadd(contrib); } const res = this.computeSerum3Reservations(healthType); - for (const [index, serum3Info] of this.serum3Infos.entries()) { + for (const [index, serum3Info] of this.spotInfos.entries()) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, @@ -378,7 +417,7 @@ export class HealthCache { }); } const res = this.computeSerum3Reservations(healthType); - for (const [index, serum3Info] of this.serum3Infos.entries()) { + for (const [index, serum3Info] of this.spotInfos.entries()) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, @@ -387,7 +426,11 @@ export class HealthCache { res.serum3Reserved[index], ); ret.push({ - asset: group.getSerum3MarketByMarketIndex(serum3Info.marketIndex).name, + asset: + serum3Info.market.type + + ' ' + + group.getSerum3MarketByMarketIndex(serum3Info.market.marketIndex) + .name, contribution: toUiDecimalsForQuote(contrib), contributionDetails: undefined, }); @@ -489,7 +532,7 @@ export class HealthCache { const tokenBalances = this.effectiveTokenBalances(healthType); const res = this.computeSerum3Reservations(healthType); - for (const [index, serum3Info] of this.serum3Infos.entries()) { + for (const [index, serum3Info] of this.spotInfos.entries()) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, @@ -554,36 +597,44 @@ export class HealthCache { return adjustedCache.healthRatio(healthType); } - findSerum3InfoIndex(marketIndex: MarketIndex): number { - return this.serum3Infos.findIndex( - (serum3Info) => serum3Info.marketIndex === marketIndex, + findSerum3InfoIndex( + marketIndex: MarketIndex, + type: 'Serum3' | 'OpenbookV2', + ): number { + return this.spotInfos.findIndex( + (serum3Info) => + serum3Info.market.marketIndex === marketIndex && + serum3Info.market.type == type, ); } getOrCreateSerum3InfoIndex( baseBank: BankForHealth, quoteBank: BankForHealth, - serum3Market: Serum3Market, + marketIndex: MarketIndex, + type: 'Serum3' | 'OpenbookV2', ): number { - const index = this.findSerum3InfoIndex(serum3Market.marketIndex); + const index = this.findSerum3InfoIndex(marketIndex, type); const baseEntryIndex = this.getOrCreateTokenInfoIndex(baseBank); const quoteEntryIndex = this.getOrCreateTokenInfoIndex(quoteBank); if (index == -1) { - this.serum3Infos.push( - Serum3Info.emptyFromSerum3Market( - serum3Market, + this.spotInfos.push( + SpotInfo.emptyFromSerum3Market( + marketIndex, + type, baseEntryIndex, quoteEntryIndex, ), ); } - return this.findSerum3InfoIndex(serum3Market.marketIndex); + return this.findSerum3InfoIndex(marketIndex, type); } adjustSerum3Reserved( baseBank: BankForHealth, quoteBank: BankForHealth, - serum3Market: Serum3Market, + marketIndex: MarketIndex, + type: 'Serum3' | 'OpenbookV2', reservedBaseChange: I80F48, freeBaseChange: I80F48, reservedQuoteChange: I80F48, @@ -603,9 +654,10 @@ export class HealthCache { const index = this.getOrCreateSerum3InfoIndex( baseBank, quoteBank, - serum3Market, + marketIndex, + type, ); - const serum3Info = this.serum3Infos[index]; + const serum3Info = this.spotInfos[index]; serum3Info.reservedBase.iadd(reservedBaseChange); serum3Info.reservedQuote.iadd(reservedQuoteChange); } @@ -614,7 +666,8 @@ export class HealthCache { baseBank: BankForHealth, quoteBank: BankForHealth, bidNativeQuoteAmount: I80F48, - serum3Market: Serum3Market, + marketIndex: MarketIndex, + type: 'Serum3' | 'OpenbookV2', healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = deepClone(this); @@ -630,7 +683,8 @@ export class HealthCache { adjustedCache.adjustSerum3Reserved( baseBank, quoteBank, - serum3Market, + marketIndex, + type, ZERO_I80F48(), ZERO_I80F48(), bidNativeQuoteAmount, @@ -643,7 +697,8 @@ export class HealthCache { baseBank: BankForHealth, quoteBank: BankForHealth, askNativeBaseAmount: I80F48, - serum3Market: Serum3Market, + marketIndex: MarketIndex, + type: 'Serum3' | 'OpenbookV2', healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = deepClone(this); @@ -659,7 +714,8 @@ export class HealthCache { adjustedCache.adjustSerum3Reserved( baseBank, quoteBank, - serum3Market, + marketIndex, + type, askNativeBaseAmount, ZERO_I80F48(), ZERO_I80F48(), @@ -1085,7 +1141,8 @@ export class HealthCache { getMaxSerum3OrderForHealthRatio( baseBank: BankForHealth, quoteBank: BankForHealth, - serum3Market: Serum3Market, + marketIndex: MarketIndex, + type: 'Serum3' | 'OpenbookV2', side: Serum3Side, minRatio: I80F48, ): I80F48 { @@ -1182,7 +1239,8 @@ export class HealthCache { adjustedCache.adjustSerum3Reserved( baseBank, quoteBank, - serum3Market, + marketIndex, + type, side === Serum3Side.ask ? amount.div(base.prices.oracle) : ZERO_I80F48(), @@ -1541,7 +1599,7 @@ export class Serum3Reserved { ) {} } -export class Serum3Info { +export class SpotInfo { constructor( public reservedBase: I80F48, public reservedQuote: I80F48, @@ -1549,26 +1607,27 @@ export class Serum3Info { public reservedQuoteAsBaseHighestBid: I80F48, public baseInfoIndex: number, public quoteInfoIndex: number, - public marketIndex: MarketIndex, + public market: { marketIndex: MarketIndex; type: 'OpenbookV2' | 'Serum3' }, ) {} static emptyFromSerum3Market( - serum3Market: Serum3Market, + marketIndex: MarketIndex, + type: 'OpenbookV2' | 'Serum3', baseEntryIndex: number, quoteEntryIndex: number, - ): Serum3Info { - return new Serum3Info( + ): SpotInfo { + return new SpotInfo( ZERO_I80F48(), ZERO_I80F48(), ZERO_I80F48(), ZERO_I80F48(), baseEntryIndex, quoteEntryIndex, - serum3Market.marketIndex, + { marketIndex, type }, ); } - static fromOoModifyingTokenInfos( + static fromSerum3OoModifyingTokenInfos( serumAccount: Serum3Orders, baseInfoIndex: number, baseInfo: TokenInfo, @@ -1576,7 +1635,7 @@ export class Serum3Info { quoteInfo: TokenInfo, marketIndex: MarketIndex, oo: OpenOrders, - ): Serum3Info { + ): SpotInfo { // add the amounts that are freely settleable immediately to token balances const baseFree = I80F48.fromI64(oo.baseTokenFree); const quoteFree = I80F48.fromI64(oo.quoteTokenFree); @@ -1598,14 +1657,62 @@ export class Serum3Info { I80F48.fromNumber(serumAccount.highestPlacedBidInv), ); - return new Serum3Info( + return new SpotInfo( reservedBase, reservedQuote, reservedBaseAsQuoteLowestAsk, reservedQuoteAsBaseHighestBid, baseInfoIndex, quoteInfoIndex, - marketIndex, + { marketIndex, type: 'Serum3' }, + ); + } + + static fromObv2OoModifyingTokenInfos( + openOrders: OpenbookV2Orders, + baseInfoIndex: number, + baseInfo: TokenInfo, + quoteInfoIndex: number, + quoteInfo: TokenInfo, + marketIndex: MarketIndex, + ooAccount: { + position: { + baseFreeNative: BN; + quoteFreeNative: BN; + bidsQuoteLots: BN; + asksBaseLots: BN; + }; + }, + ): SpotInfo { + // add the amounts that are freely settleable immediately to token balances + const baseFree = I80F48.fromU64(ooAccount.position.baseFreeNative); + const quoteFree = I80F48.fromU64(ooAccount.position.quoteFreeNative); + baseInfo.balanceSpot.iadd(baseFree); + quoteInfo.balanceSpot.iadd(quoteFree); + + // track the reserved amounts + const reservedBase = I80F48.fromU64( + ooAccount.position.asksBaseLots.mul(new BN(openOrders.baseLotSize)), + ); + const reservedQuote = I80F48.fromU64( + ooAccount.position.bidsQuoteLots.mul(new BN(openOrders.quoteLotSize)), + ); + + const reservedBaseAsQuoteLowestAsk = reservedBase.mul( + I80F48.fromNumber(openOrders.lowestPlacedAsk), + ); + const reservedQuoteAsBaseHighestBid = reservedQuote.mul( + I80F48.fromNumber(openOrders.highestPlacedBidInv), + ); + + return new SpotInfo( + reservedBase, + reservedQuote, + reservedBaseAsQuoteLowestAsk, + reservedQuoteAsBaseHighestBid, + baseInfoIndex, + quoteInfoIndex, + { marketIndex, type: 'OpenbookV2' }, ); } @@ -1697,7 +1804,7 @@ export class Serum3Info { tokenMaxReserved: TokenMaxReserved[], marketReserved: Serum3Reserved, ): string { - return ` marketIndex: ${this.marketIndex}, baseInfoIndex: ${ + return ` marketIndex: ${this.market.marketIndex}, baseInfoIndex: ${ this.baseInfoIndex }, quoteInfoIndex: ${this.quoteInfoIndex}, reservedBase: ${ this.reservedBase diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index a4bd60899f..4070ab62bc 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -1,7 +1,7 @@ -import { AnchorProvider, BN, Wallet } from '@coral-xyz/anchor'; +import { AnchorProvider, BN } from '@coral-xyz/anchor'; import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; +import { OpenBookV2Client, OpenOrdersAccount } from '@openbook-dex/openbook-v2'; import { OpenOrders, Order, Orderbook } from '@project-serum/serum/lib/market'; -import { OpenOrdersAccount, OpenBookV2Client } from '@openbook-dex/openbook-v2'; import { AccountInfo, Keypair, PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; import { OPENBOOK_PROGRAM_ID, RUST_I64_MAX, RUST_I64_MIN } from '../constants'; @@ -956,14 +956,36 @@ export class MangoAccount { group: Group, externalMarketPk: PublicKey, ): number { - const serum3Market = - group.getSerum3MarketByExternalMarket(externalMarketPk); - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); + let marketIndex: MarketIndex; + let type: 'Serum3' | 'OpenbookV2'; + let baseBank: Bank; + let quoteBank: Bank; + + if (group.serum3ExternalMarketsMap.get(externalMarketPk.toString())) { + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + marketIndex = serum3Market.marketIndex; + type = 'Serum3'; + baseBank = group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex); + quoteBank = group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex); + } else if ( + group.openbookV2ExternalMarketsMap.get(externalMarketPk.toString()) + ) { + const openbookV2Market = + group.getOpenbookV2MarketByExternalMarket(externalMarketPk); + marketIndex = openbookV2Market.marketIndex; + type = 'OpenbookV2'; + baseBank = group.getFirstBankByTokenIndex( + openbookV2Market.baseTokenIndex, + ); + quoteBank = group.getFirstBankByTokenIndex( + openbookV2Market.quoteTokenIndex, + ); + } else { + throw new Error( + `No market found for external pubkey ${externalMarketPk}`, + ); + } const targetRemainingDepositLimit = baseBank.getRemainingDepositLimit(); @@ -971,7 +993,8 @@ export class MangoAccount { const nativeAmount = hc.getMaxSerum3OrderForHealthRatio( baseBank, quoteBank, - serum3Market, + marketIndex, + type, Serum3Side.bid, I80F48.fromNumber(2), ); @@ -994,9 +1017,13 @@ export class MangoAccount { quoteAmount = quoteAmount.min(equivalentSourceAmount); } - quoteAmount = quoteAmount.div( - ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))), - ); + if (group.serum3ExternalMarketsMap.get(externalMarketPk.toString())) { + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + quoteAmount = quoteAmount.div( + ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))), + ); + } return toUiDecimals(quoteAmount, quoteBank.mintDecimals); } @@ -1011,14 +1038,36 @@ export class MangoAccount { group: Group, externalMarketPk: PublicKey, ): number { - const serum3Market = - group.getSerum3MarketByExternalMarket(externalMarketPk); - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); + let marketIndex: MarketIndex; + let type: 'Serum3' | 'OpenbookV2'; + let baseBank: Bank; + let quoteBank: Bank; + + if (group.serum3ExternalMarketsMap.get(externalMarketPk.toString())) { + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + marketIndex = serum3Market.marketIndex; + type = 'Serum3'; + baseBank = group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex); + quoteBank = group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex); + } else if ( + group.openbookV2ExternalMarketsMap.get(externalMarketPk.toString()) + ) { + const openbookV2Market = + group.getOpenbookV2MarketByExternalMarket(externalMarketPk); + marketIndex = openbookV2Market.marketIndex; + type = 'OpenbookV2'; + baseBank = group.getFirstBankByTokenIndex( + openbookV2Market.baseTokenIndex, + ); + quoteBank = group.getFirstBankByTokenIndex( + openbookV2Market.quoteTokenIndex, + ); + } else { + throw new Error( + `No market found for external pubkey ${externalMarketPk}`, + ); + } const targetRemainingDepositLimit = quoteBank.getRemainingDepositLimit(); @@ -1026,7 +1075,8 @@ export class MangoAccount { const nativeAmount = hc.getMaxSerum3OrderForHealthRatio( baseBank, quoteBank, - serum3Market, + marketIndex, + type, Serum3Side.ask, I80F48.fromNumber(2), ); @@ -1049,9 +1099,13 @@ export class MangoAccount { baseAmount = baseAmount.min(equivalentSourceAmount); } - baseAmount = baseAmount.div( - ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))), - ); + if (group.serum3ExternalMarketsMap.get(externalMarketPk.toString())) { + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + baseAmount = baseAmount.div( + ONE_I80F48().add(I80F48.fromNumber(serum3Market.getFeeRates(true))), + ); + } return toUiDecimals(baseAmount, baseBank.mintDecimals); } @@ -1070,14 +1124,37 @@ export class MangoAccount { externalMarketPk: PublicKey, healthType: HealthType = HealthType.init, ): number { - const serum3Market = - group.getSerum3MarketByExternalMarket(externalMarketPk); - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); + let marketIndex: MarketIndex; + let type: 'Serum3' | 'OpenbookV2'; + let baseBank: Bank; + let quoteBank: Bank; + + if (group.serum3ExternalMarketsMap.get(externalMarketPk.toString())) { + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + marketIndex = serum3Market.marketIndex; + type = 'Serum3'; + baseBank = group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex); + quoteBank = group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex); + } else if ( + group.openbookV2ExternalMarketsMap.get(externalMarketPk.toString()) + ) { + const openbookV2Market = + group.getOpenbookV2MarketByExternalMarket(externalMarketPk); + marketIndex = openbookV2Market.marketIndex; + type = 'OpenbookV2'; + baseBank = group.getFirstBankByTokenIndex( + openbookV2Market.baseTokenIndex, + ); + quoteBank = group.getFirstBankByTokenIndex( + openbookV2Market.quoteTokenIndex, + ); + } else { + throw new Error( + `No market found for external pubkey ${externalMarketPk}`, + ); + } + const hc = HealthCache.fromMangoAccount(group, this); return hc .simHealthRatioWithSerum3BidChanges( @@ -1085,10 +1162,10 @@ export class MangoAccount { quoteBank, toNativeI80F48( uiQuoteAmount, - group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) - .mintDecimals, + group.getFirstBankByTokenIndex(quoteBank.tokenIndex).mintDecimals, ), - serum3Market, + marketIndex, + type, healthType, ) .toNumber(); @@ -1108,14 +1185,37 @@ export class MangoAccount { externalMarketPk: PublicKey, healthType: HealthType = HealthType.init, ): number { - const serum3Market = - group.getSerum3MarketByExternalMarket(externalMarketPk); - const baseBank = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ); - const quoteBank = group.getFirstBankByTokenIndex( - serum3Market.quoteTokenIndex, - ); + let marketIndex: MarketIndex; + let type: 'Serum3' | 'OpenbookV2'; + let baseBank: Bank; + let quoteBank: Bank; + + if (group.serum3ExternalMarketsMap.get(externalMarketPk.toString())) { + const serum3Market = + group.getSerum3MarketByExternalMarket(externalMarketPk); + marketIndex = serum3Market.marketIndex; + type = 'Serum3'; + baseBank = group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex); + quoteBank = group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex); + } else if ( + group.openbookV2ExternalMarketsMap.get(externalMarketPk.toString()) + ) { + const openbookV2Market = + group.getOpenbookV2MarketByExternalMarket(externalMarketPk); + marketIndex = openbookV2Market.marketIndex; + type = 'OpenbookV2'; + baseBank = group.getFirstBankByTokenIndex( + openbookV2Market.baseTokenIndex, + ); + quoteBank = group.getFirstBankByTokenIndex( + openbookV2Market.quoteTokenIndex, + ); + } else { + throw new Error( + `No market found for external pubkey ${externalMarketPk}`, + ); + } + const hc = HealthCache.fromMangoAccount(group, this); return hc .simHealthRatioWithSerum3AskChanges( @@ -1123,10 +1223,10 @@ export class MangoAccount { quoteBank, toNativeI80F48( uiBaseAmount, - group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) - .mintDecimals, + group.getFirstBankByTokenIndex(baseBank.tokenIndex).mintDecimals, ), - serum3Market, + marketIndex, + type, healthType, ) .toNumber(); @@ -1520,28 +1620,48 @@ export class Serum3Orders { export class OpenbookV2Orders { static OpenbookV2MarketIndexUnset = 65535; - static from(dto: OpenbookV2PositionDto): Serum3Orders { + static from(dto: OpenbookV2PositionDto): OpenbookV2Orders { return new OpenbookV2Orders( dto.openOrders, + dto.baseBorrowsWithoutFee, + dto.quoteBorrowsWithoutFee, + dto.highestPlacedBidInv, + dto.lowestPlacedAsk, + dto.potentialBaseTokens, + dto.potentialQuoteTokens, + dto.lowestPlacedBidInv, + dto.highestPlacedAsk, + dto.quoteLotSize, + dto.baseLotSize, dto.marketIndex as MarketIndex, dto.baseTokenIndex as TokenIndex, dto.quoteTokenIndex as TokenIndex, - dto.highestPlacedBidInv, - dto.lowestPlacedAsk, ); } constructor( public openOrders: PublicKey, + public baseBorrowsWithoutFee: BN, + public quoteBorrowsWithoutFee: BN, + public highestPlacedBidInv: number, + public lowestPlacedAsk: number, + public potentialBaseTokens: BN, + public potentialQuoteTokens: BN, + public lowestPlacedBidInv: number, + public highestPlacedAsk: number, + public quoteLotSize: number, + public baseLotSize: number, public marketIndex: MarketIndex, public baseTokenIndex: TokenIndex, public quoteTokenIndex: TokenIndex, - public highestPlacedBidInv: number, - public lowestPlacedAsk: number, ) {} public isActive(): boolean { - return this.marketIndex !== OpenbookV2Orders.OpenbookV2MarketIndexUnset; + console.log(`isActive - ${this.marketIndex} ${this.openOrders}`); + return ( + this.marketIndex !== OpenbookV2Orders.OpenbookV2MarketIndexUnset && + !this.openOrders.equals(PublicKey.default) + ); } } @@ -1564,13 +1684,19 @@ export class Serum3PositionDto { export class OpenbookV2PositionDto { constructor( public openOrders: PublicKey, - public marketIndex: number, public baseBorrowsWithoutFee: BN, public quoteBorrowsWithoutFee: BN, - public baseTokenIndex: number, - public quoteTokenIndex: number, public highestPlacedBidInv: number, public lowestPlacedAsk: number, + public potentialBaseTokens: BN, + public potentialQuoteTokens: BN, + public lowestPlacedBidInv: number, + public highestPlacedAsk: number, + public quoteLotSize: number, + public baseLotSize: number, + public marketIndex: number, + public baseTokenIndex: number, + public quoteTokenIndex: number, public reserved: number[], ) {} } diff --git a/ts/client/src/accounts/openbookV2.ts b/ts/client/src/accounts/openbookV2.ts index 5198ce28d3..dc3e1c26c4 100644 --- a/ts/client/src/accounts/openbookV2.ts +++ b/ts/client/src/accounts/openbookV2.ts @@ -1,20 +1,19 @@ +import { AnchorProvider } from '@coral-xyz/anchor'; import { utf8 } from '@coral-xyz/anchor/dist/cjs/utils/bytes'; import { - OpenBookV2Client, BookSideAccount, MarketAccount, + OpenBookV2Client, baseLotsToUi, priceLotsToUi, } from '@openbook-dex/openbook-v2'; -import { Cluster, Keypair, PublicKey } from '@solana/web3.js'; +import { Keypair, PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; import { MangoClient } from '../client'; -import { OPENBOOK_V2_PROGRAM_ID } from '../constants'; import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from '../numbers/I80F48'; import { As, EmptyWallet } from '../utils'; import { TokenIndex } from './bank'; import { Group } from './group'; -import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; export type OpenbookV2MarketIndex = number & As<'market-index'>; @@ -118,7 +117,6 @@ export class OpenbookV2Market { [Buffer.from('OpenOrders'), mangoAccount.toBuffer(), indexBuf], programId, ); - console.log('nextoo', nextIndex, openOrderPublicKey.toBase58()); return openOrderPublicKey; } diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index f793629fff..88c8b15d47 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -2972,7 +2972,7 @@ export class MangoClient { openbookV2Program: openbookV2Market.openbookProgram, openbookV2MarketExternal: openbookV2Market.openbookMarketExternal, openOrdersIndexer: openbookV2Market.findOoIndexerPda( - this.programId, + new PublicKey('opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb'), // TODO replace with OPENBOOK_PROGRAM_ID mangoAccount.publicKey, ), openOrdersAccount: await openbookV2Market.getNextOoPda( @@ -3052,6 +3052,12 @@ export class MangoClient { mangoAccount.publicKey, ), openOrdersAccount: openOrders, + baseBank: group.getFirstBankByTokenIndex( + openbookV2Market.baseTokenIndex, + ).publicKey, + quoteBank: group.getFirstBankByTokenIndex( + openbookV2Market.quoteTokenIndex, + ).publicKey, solDestination: (this.program.provider as AnchorProvider).wallet .publicKey, }) @@ -3201,10 +3207,7 @@ export class MangoClient { [], openOrdersForMarket, ); - console.log('healthremaining') - healthRemainingAccounts.forEach(pk => { - console.log(pk.toBase58()) - }); + const openbookV2MarketExternal = group.openbookV2ExternalMarketsMap.get( externalMarketPk.toBase58(), )!; @@ -3242,7 +3245,7 @@ export class MangoClient { const payerBank = group.getFirstBankByTokenIndex(payerTokenIndex); const receiverBank = group.getFirstBankByTokenIndex(receiverTokenIndex); - console.log('openOrderPk', openOrderPk?.toBase58()) + // console.log('openOrderPk', openOrderPk?.toBase58()); const ix = await this.program.methods .openbookV2PlaceOrder( side, @@ -6129,28 +6132,28 @@ export class MangoClient { })), ) .flat(); - console.log('indices') - openbookPositionMarketIndices.forEach((p) => { - console.log(p.marketIndex, p.openOrders.toBase58()) - }) - console.log('oos for market') - openbookOpenOrdersForMarket.forEach((p) => { - console.log(p[0].baseTokenIndex, p[1].toBase58()) - }) + // console.log('indices'); + // openbookPositionMarketIndices.forEach((p) => { + // console.log(p.marketIndex, p.openOrders.toBase58()); + // }); + // // console.log('oos for market'); + // openbookOpenOrdersForMarket.forEach((p) => { + // console.log(p[0].baseTokenIndex, p[1].toBase58()); + // }); for (const [openbookV2Market, openOrderPk] of openbookOpenOrdersForMarket) { const ooPositionExists = serumPositionMarketIndices.findIndex( (i) => i.marketIndex === openbookV2Market.marketIndex, ) > -1; if (!ooPositionExists) { - console.log('postion does not exist') + // console.log('postion does not exist'); const inactiveOpenbookPosition = openbookPositionMarketIndices.findIndex( (serumPos) => serumPos.marketIndex === OpenbookV2Orders.OpenbookV2MarketIndexUnset, ); - console.log('new pos index', inactiveOpenbookPosition) + // console.log('new pos index', inactiveOpenbookPosition); if (inactiveOpenbookPosition != -1) { openbookPositionMarketIndices[inactiveOpenbookPosition].marketIndex = openbookV2Market.marketIndex; @@ -6169,10 +6172,10 @@ export class MangoClient { .map((serumPosition) => serumPosition.openOrders), ); - console.log('pushing') - openbookPositionMarketIndices.forEach((p) => { - console.log(p.marketIndex, p.openOrders.toBase58()) - }) + // console.log('pushing'); + // openbookPositionMarketIndices.forEach((p) => { + // console.log(p.marketIndex, p.openOrders.toBase58()); + // }); healthRemainingAccounts.push( ...openbookPositionMarketIndices .filter( diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 0d48f2e66f..3868018edc 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -10330,9 +10330,13 @@ export type MangoV4 = { "type": { "array": [ "u8", - 119 + 111 ] } + }, + { + "name": "forceAlign", + "type": "u64" } ] } @@ -25291,9 +25295,13 @@ export const IDL: MangoV4 = { "type": { "array": [ "u8", - 119 + 111 ] } + }, + { + "name": "forceAlign", + "type": "u64" } ] }