import { MangoClient, MangoGroup, MarginAccount } from '../src/client'; import { findLargestTokenAccountForOwner } from '../src/utils'; import IDS from '../src/ids.json'; import { Account, Connection, PublicKey } from '@solana/web3.js'; import { Market, OpenOrders } from '@project-serum/serum'; import { expect } from 'chai'; import { spawn } from 'child_process'; import { blob, struct, u8, nu64 } from 'buffer-layout'; import { sleep } from '../src/utils'; import dotenv from 'dotenv'; dotenv.config() if (!process.env.AGGREGATOR_PATH) { console.info("You have not set the AGGREGATOR_PATH in .env, some tests will fail"); } import { _sendTransaction, createWalletAndRequestAirdrop, getSpotMarketDetails, createMangoGroupSymbolMappings, createTokenAccountWithBalance, getMinSizeAndPriceForMarket, placeOrderUsingSerumDex, cancelOrdersUsingSerumDex, getAndDecodeBidsAndAsksForOwner, performSingleDepositOrWithdrawal, getAndDecodeBidsAndAsks, getOrderSizeAndPrice, extractInfoFromLogs, prettyPrintOwnerKeys } from './test_utils'; console.log = function () {}; // NOTE: Disable all unnecessary logging let cluster = "devnet"; const client = new MangoClient(); const clusterIds = IDS[cluster]; const mangoProgramId = new PublicKey(clusterIds.mango_program_id); const dexProgramId = new PublicKey(clusterIds.dex_program_id); let connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip'); let mainnetCluster = "mainnet-beta"; const mainnetClusterIds = IDS[mainnetCluster]; const mainnetMangoProgramId = new PublicKey(mainnetClusterIds.mango_program_id); const mainnetDexProgramId = new PublicKey(mainnetClusterIds.dex_program_id); let mainnetConnection = new Connection(IDS.cluster_urls[mainnetCluster], 'singleGossip'); function chunkOrders(orders: any[], chunkSize: number) { return orders.reduce((resultArray: any[], item, index) => { const chunkIndex = Math.floor(index/chunkSize) if(!resultArray[chunkIndex]) { resultArray[chunkIndex] = [] } resultArray[chunkIndex].push(item) return resultArray; }, []); } async function initAccountsWithBalances(neededBalances: number[], wrappedSol: boolean) { const owner = await createWalletAndRequestAirdrop(connection, 5); const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); await Promise.all(neededBalances.map(async (x, i) => { if (x > 0) { const baseSymbol = mangoGroupSymbols[i]; await createTokenAccountWithBalance(connection, owner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, x, wrappedSol); } })); prettyPrintOwnerKeys(owner, "Account"); return owner; } async function requestPriceChange(mangoGroup: MangoGroup, requiredPrice: number, baseSymbol: string) { let prices = await mangoGroup.getPrices(connection); while (prices[0].toFixed(2) !== requiredPrice.toFixed(2)) { console.info("Running oracle to change price"); await performPriceChange(Math.round( requiredPrice * 1e2 ) / 1e2, baseSymbol.toLowerCase()); console.info("Finished running oracle to change price"); try { prices = await mangoGroup.getPrices(connection); } catch (e) { console.info("Error, trying to reset connection"); connection = new Connection(IDS.cluster_urls[cluster], 'singleGossip'); prices = await mangoGroup.getPrices(connection); } } return prices; } function performPriceChange(requiredPrice: number, baseSymbol: string): Promise { return new Promise(function(resolve, _){ const priceChangerOracle = spawn('yarn', ['solink', 'oracle', (requiredPrice * 100).toString()], {cwd: process.env.AGGREGATOR_PATH}); priceChangerOracle.stdout.on("data", data => { if (data.includes(`Submit OK {"aggregator":"${baseSymbol}:usd"`)) { priceChangerOracle.kill(); resolve(); } }); }) } async function cleanOrderBook(mangoGroupSpotMarket: any) { console.info("Cleaning order book, this will take a while..."); const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); const { spotMarket, baseSymbol, quoteSymbol } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); let bna = await getAndDecodeBidsAndAsks(connection, spotMarket); let allAsks: any[] = [...bna.askOrderBook].map(x => ({ price: x.price, size: x.size })).reverse(); let allBids: any[] = [...bna.bidOrderBook].map(x => ({ price: x.price, size: x.size })).reverse(); if (allAsks.length || allBids.length) { const owner = await createWalletAndRequestAirdrop(connection, 5); try { const marginAccountPk = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, owner); let marginAccount = await client.getMarginAccount(connection, marginAccountPk, dexProgramId); const amountNeededToClearAsks: number = Math.ceil(allAsks.reduce((acc, ask) => acc + (ask.price * ask.size), 0) + 10); await createTokenAccountWithBalance(connection, owner, quoteSymbol, mangoGroupTokenMappings, clusterIds.faucets, amountNeededToClearAsks); await performSingleDepositOrWithdrawal(connection, owner, client, mangoGroup, mangoProgramId, quoteSymbol, mangoGroupTokenMappings, marginAccount, 'deposit', amountNeededToClearAsks); const chunkedAsks = chunkOrders(allAsks, 15); for (let ask of chunkedAsks) { marginAccount = await client.getMarginAccount(connection, marginAccountPk, dexProgramId); const price: number = Math.max(...ask.map((x: any) => x.price)); const size: number = ask.reduce(( a: any, b: any ) => a + b.size, 0); const roundedSize = Math.round( size * 1e6 ) / 1e6; console.info(`Buying ${roundedSize} for ${price}`) await client.placeAndSettle(connection, mangoProgramId, mangoGroup, marginAccount, spotMarket, owner, 'buy', price, roundedSize); } const amountNeededToClearBids: number = Math.ceil(allBids.reduce((acc, bid) => acc + (bid.size), 0) + 10); await createTokenAccountWithBalance(connection, owner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, amountNeededToClearBids); await performSingleDepositOrWithdrawal(connection, owner, client, mangoGroup, mangoProgramId, baseSymbol, mangoGroupTokenMappings, marginAccount, 'deposit', amountNeededToClearBids); const chunkedBids = chunkOrders(allBids, 15); for (let bid of chunkedBids) { marginAccount = await client.getMarginAccount(connection, marginAccountPk, dexProgramId); const price: number = Math.min(...bid.map((x: any) => x.price)); const size: number = bid.reduce(( a: any, b: any ) => a + b.size, 0); const roundedSize = Math.round( size * 1e6 ) / 1e6; console.info(`Selling ${roundedSize} for ${price}`) await client.placeAndSettle(connection, mangoProgramId, mangoGroup, marginAccount, spotMarket, owner, 'sell', price, roundedSize); } bna = await getAndDecodeBidsAndAsks(connection, spotMarket); allAsks = [...bna.askOrderBook].map(x => ({ price: x.price, size: x.size })); allBids = [...bna.bidOrderBook].map(x => ({ price: x.price, size: x.size })); expect(allAsks).to.be.empty; expect(allBids).to.be.empty; prettyPrintOwnerKeys(owner, "Cleaner"); } catch (error) { console.info(error); throw new Error(` Test Error: ${error.message}, ${prettyPrintOwnerKeys(owner, "Cleaner")} `); } } } async function placeNOrdersAfterLimit(mangoGroupSpotMarket: any, marketIndex: number, orderQuantityAfter: number) { let openOrdersForOwner: any[]; const orderQuantity = 128; // Max orders const buyerOwner = await createWalletAndRequestAirdrop(connection, 5); prettyPrintOwnerKeys(buyerOwner, "Buyer"); const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); const buyerMarginAccountPk = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, buyerOwner); let buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccountPk, dexProgramId); const { spotMarket, baseSymbol, quoteSymbol } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); const [orderSize, orderPrice, _] = await getOrderSizeAndPrice(connection, spotMarket, mangoGroupTokenMappings, baseSymbol, quoteSymbol, 'buy'); const neededQuoteAmount = orderPrice * orderSize; const neededQuoteAmountForAllTrades = neededQuoteAmount * orderQuantity; await createTokenAccountWithBalance(connection, buyerOwner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, neededQuoteAmountForAllTrades); await performSingleDepositOrWithdrawal(connection, buyerOwner, client, mangoGroup, mangoProgramId, baseSymbol, mangoGroupTokenMappings, buyerMarginAccount, 'deposit', neededQuoteAmountForAllTrades); await requestPriceChange(mangoGroup, orderPrice, baseSymbol); for (let i = 0; i < orderQuantity; i++) { console.info(`Placing a buy order of ${orderSize} ${baseSymbol} for ${orderPrice} ${quoteSymbol} = ~${neededQuoteAmount} ${quoteSymbol} - ${i + 1}/${orderQuantity}`); buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccount.publicKey, dexProgramId); await client.placeAndSettle(connection, mangoProgramId, mangoGroup, buyerMarginAccount, spotMarket, buyerOwner, 'buy', orderPrice, orderSize); } buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccount.publicKey, dexProgramId); openOrdersForOwner = await getAndDecodeBidsAndAsksForOwner(connection, spotMarket, buyerMarginAccount.openOrdersAccounts[marketIndex]); // TODO: this should be a for loop of cancellations expect(openOrdersForOwner).to.be.an('array').and.to.have.lengthOf(128); await client.cancelOrder(connection, mangoProgramId, mangoGroup, buyerMarginAccount, buyerOwner, spotMarket, openOrdersForOwner[0]); buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccount.publicKey, dexProgramId); openOrdersForOwner = await getAndDecodeBidsAndAsksForOwner(connection, spotMarket, buyerMarginAccount.openOrdersAccounts[marketIndex]); expect(openOrdersForOwner).to.be.an('array').and.to.have.lengthOf(127); for (let i = 0; i < orderQuantityAfter; i++) { console.info(`Placing a buy order of ${orderSize} ${baseSymbol} for ${orderPrice} ${quoteSymbol} = ~${neededQuoteAmount} ${quoteSymbol} - ${i + 1}/${orderQuantityAfter}`); buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccount.publicKey, dexProgramId); await client.placeAndSettle(connection, mangoProgramId, mangoGroup, buyerMarginAccount, spotMarket, buyerOwner, 'buy', orderPrice, orderSize); } expect(openOrdersForOwner).to.be.an('array').and.to.have.lengthOf(128); } async function stressTestMatchOrder(mangoGroupSpotMarket: any, orderQuantity: number): Promise { let bna: any, allAsks: any[], allBids: any[]; const sellerOwner = await createWalletAndRequestAirdrop(connection, 5); prettyPrintOwnerKeys(sellerOwner, "Seller"); const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); const sellerMarginAccountPk = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, sellerOwner); let sellerMarginAccount = await client.getMarginAccount(connection, sellerMarginAccountPk, dexProgramId); const { spotMarket, baseSymbol, quoteSymbol } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); const [orderSize, orderPrice, _] = await getOrderSizeAndPrice(connection, spotMarket, mangoGroupTokenMappings, baseSymbol, quoteSymbol, 'sell'); const neededQuoteAmount = orderPrice * orderSize; const neededBaseAmountForAllTrades = orderSize * orderQuantity; const neededQuoteAmountForAllTrades = neededQuoteAmount * orderQuantity; await createTokenAccountWithBalance(connection, sellerOwner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, neededBaseAmountForAllTrades); await performSingleDepositOrWithdrawal(connection, sellerOwner, client, mangoGroup, mangoProgramId, baseSymbol, mangoGroupTokenMappings, sellerMarginAccount, 'deposit', neededBaseAmountForAllTrades); for (let i = 0; i < orderQuantity; i++) { console.info(`Placing a sell order of ${orderSize} ${baseSymbol} for ${orderPrice} ${quoteSymbol} = ~${neededQuoteAmount} USD - ${i + 1}/${orderQuantity}`); sellerMarginAccount = await client.getMarginAccount(connection, sellerMarginAccountPk, dexProgramId); await client.placeAndSettle(connection, mangoProgramId, mangoGroup, sellerMarginAccount, spotMarket, sellerOwner, 'sell', orderPrice, orderSize); } bna = await getAndDecodeBidsAndAsks(connection, spotMarket); allAsks = [...bna.askOrderBook].map(x => ({ price: x.price, size: x.size })); allBids = [...bna.bidOrderBook].map(x => ({ price: x.price, size: x.size })); const buyerOwner = await createWalletAndRequestAirdrop(connection, 5); prettyPrintOwnerKeys(buyerOwner, "Buyer"); const buyerMarginAccountPk = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, buyerOwner); const buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccountPk, dexProgramId); await createTokenAccountWithBalance(connection, buyerOwner, quoteSymbol, mangoGroupTokenMappings, clusterIds.faucets, neededQuoteAmountForAllTrades); await performSingleDepositOrWithdrawal(connection, buyerOwner, client, mangoGroup, mangoProgramId, quoteSymbol, mangoGroupTokenMappings, buyerMarginAccount, 'deposit', neededQuoteAmountForAllTrades); console.info(`Placing a buy order of ${neededBaseAmountForAllTrades} ${baseSymbol} for ${orderPrice} ${quoteSymbol} = ~${neededQuoteAmountForAllTrades} ${quoteSymbol}`); const buyTxHash = await client.placeAndSettle(connection, mangoProgramId, mangoGroup, buyerMarginAccount, spotMarket, buyerOwner, 'buy', orderPrice, neededBaseAmountForAllTrades); console.info("buyTxHash:", buyTxHash); await connection.confirmTransaction(buyTxHash, 'finalized'); const buyConfirmedTx: any = await connection.getConfirmedTransaction(buyTxHash); const buyTxLogInfo = extractInfoFromLogs(buyConfirmedTx); console.info("Buy txLogInfo:", buyTxLogInfo); bna = await getAndDecodeBidsAndAsks(connection, spotMarket); allAsks = [...bna.askOrderBook].map(x => ({ price: x.price, size: x.size })); allBids = [...bna.bidOrderBook].map(x => ({ price: x.price, size: x.size })); expect(allAsks).to.be.empty; expect(allBids).to.be.empty; } async function stressTestLiquidation(params: { mangoGroupSpotMarket: any, orderQuantity?: number, customLiqeeOwner?: Account, shouldPartialLiquidate?: boolean, shouldCreateNewLiqor?: boolean, shouldFinishLiquidationInTest?: boolean, customOrderPrice?: number, customOrderSize?: number, leverageCoefficient?: number, matchLeveragedOrder?: boolean, side?: 'buy' | 'sell' }) { const { mangoGroupSpotMarket, orderQuantity = 1, customLiqeeOwner = null, shouldPartialLiquidate = false, shouldCreateNewLiqor = true, shouldFinishLiquidationInTest = true, customOrderPrice = 0, customOrderSize = 0, leverageCoefficient = 15, matchLeveragedOrder = false, side = 'buy' } = params; console.info("shouldCreateNewLiqor:", shouldCreateNewLiqor) console.info("orderQuantity:", orderQuantity) let bna: any, allAsks: any[], allBids: any[], prices: number[]; const liqeeOwner = customLiqeeOwner || await createWalletAndRequestAirdrop(connection, 5); prettyPrintOwnerKeys(liqeeOwner, "Liqee"); const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); const liqeeMarginAccountPk = (customLiqeeOwner) ? (await client.getMarginAccountsForOwner(connection, mangoProgramId, mangoGroup, liqeeOwner))[0].publicKey : await client.initMarginAccount(connection, mangoProgramId, mangoGroup, liqeeOwner); let liqeeMarginAccount = await client.getMarginAccount(connection, liqeeMarginAccountPk, dexProgramId); const { spotMarket, baseSymbol, quoteSymbol } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); const baseSymbolIndex = mangoGroupSymbols.findIndex(x => x === baseSymbol); const quoteSymbolIndex = mangoGroupSymbols.findIndex(x => x === quoteSymbol); const [orderSize, orderPrice, _] = await getOrderSizeAndPrice(connection, spotMarket, mangoGroupTokenMappings, baseSymbol, quoteSymbol, side); const finalOrderPrice = customOrderPrice || orderPrice; const finalOrderSize = customOrderSize || orderSize; const neededQuoteAmount = finalOrderPrice * finalOrderSize; const neededBaseAmountForAllTrades = finalOrderSize * orderQuantity; const neededQuoteAmountForAllTrades = neededQuoteAmount * orderQuantity; const neededCollateralTokenSymbol = (side === 'buy') ? baseSymbol : quoteSymbol; const neededAmountForAllTrades = (side === 'buy') ? neededBaseAmountForAllTrades : neededQuoteAmountForAllTrades; await createTokenAccountWithBalance(connection, liqeeOwner, neededCollateralTokenSymbol, mangoGroupTokenMappings, clusterIds.faucets, neededAmountForAllTrades); await performSingleDepositOrWithdrawal(connection, liqeeOwner, client, mangoGroup, mangoProgramId, neededCollateralTokenSymbol, mangoGroupTokenMappings, liqeeMarginAccount, 'deposit', neededAmountForAllTrades); prices = await requestPriceChange(mangoGroup, finalOrderPrice, baseSymbol); for (let i = 0; i < orderQuantity; i++) { console.info(`Placing a ${side} order of ${finalOrderSize} ${baseSymbol} for ${finalOrderPrice} ${quoteSymbol} = ~${neededQuoteAmount} ${quoteSymbol} - ${i + 1}/${orderQuantity}`); liqeeMarginAccount = await client.getMarginAccount(connection, liqeeMarginAccountPk, dexProgramId); console.info("Deposits init:", liqeeMarginAccount.getAssets(mangoGroup)); console.info("Assets init:", liqeeMarginAccount.getAssets(mangoGroup)); console.info("Liabs init:", liqeeMarginAccount.getLiabs(mangoGroup)); await client.placeAndSettle(connection, mangoProgramId, mangoGroup, liqeeMarginAccount, spotMarket, liqeeOwner, side, finalOrderPrice, finalOrderSize); console.info("Sleeep!!!") await sleep(5000); liqeeMarginAccount = await client.getMarginAccount(connection, liqeeMarginAccountPk, dexProgramId); console.info("Deposits:", liqeeMarginAccount.getAssets(mangoGroup)); console.info("Assets:", liqeeMarginAccount.getAssets(mangoGroup)); console.info("Assets Val:", liqeeMarginAccount.getAssetsVal(mangoGroup, prices)); console.info("Liabs:", liqeeMarginAccount.getLiabs(mangoGroup)); } if (matchLeveragedOrder) await cleanOrderBook(mangoGroupSpotMarket); console.info("Sleeep!!!") await sleep(10000); liqeeMarginAccount = await client.getMarginAccount(connection, liqeeMarginAccountPk, dexProgramId); console.info("collRatio before price change:", liqeeMarginAccount.getCollateralRatio(mangoGroup, prices)); console.info("Assets before:", liqeeMarginAccount.getAssets(mangoGroup)); console.info("Assets Val before:", liqeeMarginAccount.getAssetsVal(mangoGroup, prices)); console.info("Deposits before:", liqeeMarginAccount.getDeposits(mangoGroup)); console.info("Liabs before:", liqeeMarginAccount.getLiabs(mangoGroup)); console.info(prices); const adjustedPrice = (side === 'buy') ? finalOrderPrice / leverageCoefficient : finalOrderPrice * leverageCoefficient; prices = await requestPriceChange(mangoGroup, adjustedPrice, baseSymbol); liqeeMarginAccount = await client.getMarginAccount(connection, liqeeMarginAccountPk, dexProgramId); console.info("collRatio after price change:", liqeeMarginAccount.getCollateralRatio(mangoGroup, prices)); console.info("Assets after:", liqeeMarginAccount.getAssets(mangoGroup)); console.info("Assets Val after:", liqeeMarginAccount.getAssetsVal(mangoGroup, prices)); console.info("Deposits after:", liqeeMarginAccount.getDeposits(mangoGroup)); console.info("Liabs after:", liqeeMarginAccount.getLiabs(mangoGroup)); console.info(prices); let liqorOwner = new Account(); if (shouldCreateNewLiqor) { liqorOwner = await createWalletAndRequestAirdrop(connection, 5); prettyPrintOwnerKeys(liqorOwner, "Liqor"); for (let mangoGroupSymbol of mangoGroupSymbols) { const requiredBalance = (mangoGroupSymbol === quoteSymbol) ? neededQuoteAmountForAllTrades : 0; await createTokenAccountWithBalance(connection, liqorOwner, mangoGroupSymbol, mangoGroupTokenMappings, clusterIds.faucets, requiredBalance); } if (shouldFinishLiquidationInTest) { const tokenWallets = (await Promise.all( mangoGroup.tokens.map( (mint) => findLargestTokenAccountForOwner(connection, liqorOwner.publicKey, mint).then( (response) => response.publicKey ) ) )); let liquidationTxHash: string; if (shouldPartialLiquidate) { const forceCancelTx = await client.forceCancelOrders(connection, mangoProgramId, mangoGroup, liqeeMarginAccount, liqorOwner, spotMarket, 5) console.log('forceCancelTxHash', forceCancelTx) liquidationTxHash = await client.partialLiquidate(connection, mangoProgramId, mangoGroup, liqeeMarginAccount, liqorOwner, tokenWallets[quoteSymbolIndex], tokenWallets[baseSymbolIndex], quoteSymbolIndex, baseSymbolIndex, neededQuoteAmountForAllTrades); } else { const depositQuantities = new Array(tokenWallets.length).fill(0); depositQuantities[quoteSymbolIndex] = neededQuoteAmountForAllTrades; liquidationTxHash = await client.liquidate(connection, mangoProgramId, mangoGroup, liqeeMarginAccount, liqorOwner, tokenWallets, depositQuantities); } console.info("liquidationTxHash:", liquidationTxHash); await connection.confirmTransaction(liquidationTxHash, 'finalized'); const liquidationConfirmedTx: any = await connection.getConfirmedTransaction(liquidationTxHash); const liquidationTxLogInfo = extractInfoFromLogs(liquidationConfirmedTx); console.info("Liquidation txLogInfo:", liquidationTxLogInfo); } } return {liqeeOwner, liqorOwner}; } const mangoGroupName = 'BTC_ETH_SOL_SRM_USDC'; const mangoGroupSymbols = mangoGroupName.split('_'); const mangoGroupIds = clusterIds.mango_groups[mangoGroupName]; const mangoGroupSpotMarkets: [string, string][] = Object.entries(mangoGroupIds.spot_market_symbols); const mangoGroupPk = new PublicKey(mangoGroupIds.mango_group_pk); const mangoGroupSpotMarket = mangoGroupSpotMarkets[0]; //BTC/USDC // const mangoGroupSpotMarket = mangoGroupSpotMarkets[1]; //ETH/USDC // const mangoGroupSpotMarket = mangoGroupSpotMarkets[2]; //SOL/USDC // const mangoGroupSpotMarket = mangoGroupSpotMarkets[3]; //SRM/USDC describe('stress test order limits', async() => { before(async () => { await cleanOrderBook(mangoGroupSpotMarket); }); it('should be able to place 129th order after cancelling one', async() => { await placeNOrdersAfterLimit(mangoGroupSpotMarket, 0, 1); }); }); describe('stress testing matching orders', async() => { before(async () => { await cleanOrderBook(mangoGroupSpotMarket); }); it('should match 1 order at a single price', async() => { await stressTestMatchOrder(mangoGroupSpotMarket, 1); }); it('should match 10 orders at a single price', async() => { await stressTestMatchOrder(mangoGroupSpotMarket, 10); }); it('should match 20 orders at a single price', async() => { await stressTestMatchOrder(mangoGroupSpotMarket, 20); }); it('should match 25 orders at a single price', async() => { await stressTestMatchOrder(mangoGroupSpotMarket, 25); }); }); describe('stress testing liquidation', async() => { before(async () => { await cleanOrderBook(mangoGroupSpotMarket); }); it('should liquidate an account with 1 open order', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 1}); }); it('should liquidate an account with 10 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 10}); }); it('should liquidate an account with 20 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 20}); }); it('should liquidate an account with 25 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 25}); }); it('should liquidate an account with 128 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 128}); }); }); describe('stress testing partial liquidation', async() => { // before(async () => { // await cleanOrderBook(mangoGroupSpotMarket); // }); it('should partially liquidate an account with 1 open order', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 1, shouldPartialLiquidate: true}); }); it('should partially liquidate an account with 10 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 10, shouldPartialLiquidate: true}); }); it('should partially liquidate an account with 20 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 20, shouldPartialLiquidate: true}); }); it('should partially liquidate an account with 25 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 25, shouldPartialLiquidate: true}); }); it('should partially liquidate an account with 128 open orders', async() => { await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 128, shouldPartialLiquidate: true}); }); it ('should test socialized loss with 1 borrows', async() => { const mangoGroupSpotMarketBTC = mangoGroupSpotMarkets[0]; //BTC/USDC await stressTestLiquidation({ mangoGroupSpotMarket: mangoGroupSpotMarketBTC, shouldPartialLiquidate: true, shouldFinishLiquidationInTest: true, customOrderPrice: 20, customOrderSize: 1, matchLeveragedOrder: false, shouldCreateNewLiqor: true, leverageCoefficient: 30 }); }) it ('should test socialized loss with 4 borrows', async() => { const mangoGroupSpotMarketETH = mangoGroupSpotMarkets[1]; //ETH/USDC const { liqeeOwner } = await stressTestLiquidation({ mangoGroupSpotMarket: mangoGroupSpotMarketETH, shouldPartialLiquidate: true, shouldFinishLiquidationInTest: false, customOrderPrice: 10, customOrderSize: 1, shouldCreateNewLiqor: false }); const mangoGroupSpotMarketSOL = mangoGroupSpotMarkets[2]; //SOL/USDC await stressTestLiquidation({ mangoGroupSpotMarket: mangoGroupSpotMarketSOL, customLiqeeOwner: liqeeOwner, shouldPartialLiquidate: true, shouldFinishLiquidationInTest: false, customOrderPrice: 10, customOrderSize: 1, shouldCreateNewLiqor: false }); const mangoGroupSpotMarketSRM = mangoGroupSpotMarkets[3]; //SRM/USDC await stressTestLiquidation({ mangoGroupSpotMarket: mangoGroupSpotMarketSRM, customLiqeeOwner: liqeeOwner, shouldPartialLiquidate: true, shouldFinishLiquidationInTest: false, customOrderPrice: 10, customOrderSize: 1, shouldCreateNewLiqor: false }); const mangoGroupSpotMarketBTC = mangoGroupSpotMarkets[0]; //BTC/USDC await stressTestLiquidation({ mangoGroupSpotMarket: mangoGroupSpotMarketBTC, customLiqeeOwner: liqeeOwner, shouldPartialLiquidate: true, shouldFinishLiquidationInTest: true, customOrderPrice: 10, customOrderSize: 1, matchLeveragedOrder: true, shouldCreateNewLiqor: true }); }) }); describe('create various liquidation opportunities', async() => { it('should create a liquidation opportunity', async() => { const {liqeeOwner, liqorOwner} = await stressTestLiquidation({ mangoGroupSpotMarket: mangoGroupSpotMarket, shouldPartialLiquidate: true, shouldFinishLiquidationInTest: false, customOrderPrice: 20, customOrderSize: 1, side: 'sell' }); // const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); // const quoteCurrencyAccountPk = await createTokenAccountWithBalance(connection, liqorOwner, 'USDC', mangoGroupTokenMappings, clusterIds.faucets, 0); // const baseCurrencyAmounts: number[] = [1, 2, 10, 100]; // await Promise.all(mangoGroupSpotMarkets.map(async (mangoGroupSpotMarket: any, index: number) => { // const { spotMarket, baseSymbol, minSize, minPrice } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); // console.info(baseSymbol); // const baseCurrencyAccountPk = await createTokenAccountWithBalance(connection, liqorOwner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, baseCurrencyAmounts[index]); // const side = 'sell'; // if (!baseCurrencyAccountPk || !quoteCurrencyAccountPk) throw Error('Missing the necessary token accounts'); // await placeOrderUsingSerumDex(connection, liqorOwner, spotMarket, baseCurrencyAccountPk, quoteCurrencyAccountPk, { side, size: minSize, price: minPrice }); // // const openOrdersAccounts = await spotMarket.findOpenOrdersAccountsForOwner(connection, owner.publicKey); // // const ordersForOwner = await getAndDecodeBidsAndAsksForOwner(connection, spotMarket, openOrdersAccounts[0]); // // await cancelOrdersUsingSerumDex(connection, owner, spotMarket, ordersForOwner); // const allOpenOrdersAccounts = await OpenOrders.findForOwner(connection, liqorOwner.publicKey, dexProgramId) // console.info("All oo accs:", allOpenOrdersAccounts.length); // })); }); }) describe('create accounts for testing', async() => { it('should create an account with test money', async() => { const buyerOwner = await initAccountsWithBalances([100, 100, 500, 1000, 100000], false); }); it('should fund mango group', async() => { const buyerOwner = await initAccountsWithBalances([100, 100, 500, 1000, 100000], false); const amounts = [100, 100, 500, 1000, 100000]; const symbols = ['BTC', 'ETH', 'SOL', 'SRM', 'USDC']; const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); const buyerMarginAccountPk = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, buyerOwner); const buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccountPk, dexProgramId); for (let i = 0; i < symbols.length; i++) { await createTokenAccountWithBalance(connection, buyerOwner, symbols[i], mangoGroupTokenMappings, clusterIds.faucets, amounts[i]); await performSingleDepositOrWithdrawal(connection, buyerOwner, client, mangoGroup, mangoProgramId, symbols[i], mangoGroupTokenMappings, buyerMarginAccount, 'deposit', amounts[i]); } }); it('should create an account with initialised openOrders', async() => { const owner: Account = await createWalletAndRequestAirdrop(connection, 5); const mangoGroupTokenMappings = await createMangoGroupSymbolMappings(connection, mangoGroupIds); const quoteCurrencyAccountPk = await createTokenAccountWithBalance(connection, owner, 'USDC', mangoGroupTokenMappings, clusterIds.faucets, 0); await Promise.all(mangoGroupSpotMarkets.map(async (mangoGroupSpotMarket: any) => { const { spotMarket, baseSymbol, minSize, minPrice } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); const baseCurrencyAccountPk = await createTokenAccountWithBalance(connection, owner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, minSize); const side = 'sell'; if (!baseCurrencyAccountPk || !quoteCurrencyAccountPk) throw Error('Missing the necessary token accounts'); await placeOrderUsingSerumDex(connection, owner, spotMarket, baseCurrencyAccountPk, quoteCurrencyAccountPk, { side, size: minSize, price: minPrice }); // const openOrdersAccounts = await spotMarket.findOpenOrdersAccountsForOwner(connection, owner.publicKey); // const ordersForOwner = await getAndDecodeBidsAndAsksForOwner(connection, spotMarket, openOrdersAccounts[0]); // await cancelOrdersUsingSerumDex(connection, owner, spotMarket, ordersForOwner); })); }) }); describe('log stuff', async() => { // NOTE: This part of tests is used to test and log random things it('should log token decimals', async() => { const MINT_LAYOUT = struct([blob(44), u8('decimals'), blob(37)]); const mainnetTokensToTest = [ ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], ['ETH', '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk'], ['SOL', 'So11111111111111111111111111111111111111112'], ['SRM', 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt'], ['USDT', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'], ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], ]; const devnetTokensToTest = [ ['BTC', 'bypQzRBaSDWiKhoAw3hNkf35eF3z3AZCU8Sxks6mTPP'], ['ETH', 'ErWGBLBQMwdyC4H3MR8ef6pFK6gyHAyBxy4o1mHoqKzm'], ['SOL', 'So11111111111111111111111111111111111111112'], ['SRM', '9FbAMDvXqNjPqZSYt4EWTguJuDrGkfvwr3gSFpiSbX9S'], ['USDT', '7KBVenLz5WNH4PA5MdGkJNpDDyNKnBQTwnz1UqJv9GUm'], ['USDC', 'H6hy7Ykzc43EuGivv7VVuUKNpKgUoFAfUY3wdPr4UyRX'], ]; for (let [tokenName, tokenMint] of mainnetTokensToTest) { const data: any = await mainnetConnection.getAccountInfo(new PublicKey(tokenMint)); const info = MINT_LAYOUT.decode(data.data); console.info(`Mainnet ${tokenName} decimals: ${info.decimals}`); } for (let [tokenName, tokenMint] of devnetTokensToTest) { const data: any = await connection.getAccountInfo(new PublicKey(tokenMint)); const info = MINT_LAYOUT.decode(data.data); console.info(`Devnet ${tokenName} decimals: ${info.decimals}`); }; }); it('should log lotSizes', async() => { const mainnetSpotMarketsToTest = [ ['BTC/USDC', 'A8YFbxQYFVqKZaoYJLLUVcQiWP7G2MeEgW5wsAQgMvFw'], ['BTC/USDT', 'C1EuT9VokAKLiW7i2ASnZUvxDoKuKkCpDDeNxAptuNe4'], ['ETH/USDC', '4tSvZvnbyzHXLMTiFonMyxZoHmFqau1XArcRCVHLZ5gX'], ['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'], ['SRM/USDC', 'ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA'], ]; const devnetSpotMarketsToTest = [ ['BTC/USDC', 'BCqDfFd119UyNEC2HavKdy3F4qhy6EMGirSurNWKgioW'], ['ETH/USDC', 'AfB75DQs1E2VoUAMRorUxAz68b18kWZ1uqQuRibGk212'], ['SOL/USDC', '6vZd6Ghwkuzpbp7qNzBuRkhcfA9H3S7BJ2LCWSYrjfzo'], ['SRM/USDC', '6rRnXBLGzcD5v1q4NfWWZQdgBfqzEuD3g4GqDWVU8yhH'], ]; for (let [spotMarketName, spotMarketAddress] of mainnetSpotMarketsToTest) { const spotMarket = await Market.load(mainnetConnection, new PublicKey(spotMarketAddress), { skipPreflight: true, commitment: 'singleGossip'}, mainnetDexProgramId); console.info(`Mainnet ${spotMarketName} base/quote lotSizes: ${spotMarket['_decoded'].baseLotSize.toString()}/${spotMarket['_decoded'].quoteLotSize.toString()}`); console.info(`Mainnet ${spotMarketName} baseSizeNumberToLots: ${spotMarket.baseSizeNumberToLots(1)}`); console.info(`Mainnet ${spotMarketName} priceNumberToLots: ${spotMarket.priceNumberToLots(1)}`); } for (let [spotMarketName, spotMarketAddress] of devnetSpotMarketsToTest) { const spotMarket = await Market.load(connection, new PublicKey(spotMarketAddress), { skipPreflight: true, commitment: 'singleGossip'}, dexProgramId); console.info(`Devnet ${spotMarketName} base/quote lotSizes: ${spotMarket['_decoded'].baseLotSize.toString()}/${spotMarket['_decoded'].quoteLotSize.toString()}`); console.info(`Devnet ${spotMarketName} baseSizeNumberToLots: ${spotMarket.baseSizeNumberToLots(1)}`); console.info(`Devnet ${spotMarketName} priceNumberToLots: ${spotMarket.priceNumberToLots(1)}`); } }); it ('should log orderbook for spotmarket', async() => { const { spotMarket } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); let bna = await getAndDecodeBidsAndAsks(connection, spotMarket); let allAsks: any[] = [...bna.askOrderBook].map(x => ({ price: x.price, size: x.size })).reverse(); let allBids: any[] = [...bna.bidOrderBook].map(x => ({ price: x.price, size: x.size })).reverse(); console.info("=== allAsks ==="); console.info(allAsks); console.info("=== allBids ==="); console.info(allBids); }); it ('should log margin account of private key', async() => { const ownerKey = [252,0,132,118,116,9,142,85,38,150,113,82,117,172,107,37,200,103,20,206,153,172,239,151,251,175,208,119,89,164,50,4,85,244,218,137,21,123,226,241,53,80,95,8,194,128,195,133,108,79,71,175,75,177,35,99,181,251,84,107,1,154,104,105]; const owner = new Account(ownerKey); const mangoGroup = await client.getMangoGroup(connection, mangoGroupPk); const marginAccount = await client.getMarginAccountsForOwner(connection, mangoProgramId, mangoGroup, owner); console.info(marginAccount[0].publicKey.toString()); }); })