diff --git a/src/utils.ts b/src/utils.ts index 2311e5b..236b08b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -27,6 +27,22 @@ export async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } +export function getDecimalCount(value: number): number { + if ( + !isNaN(value) && + Math.floor(value) !== value && + value.toString().includes('.') + ) + return value.toString().split('.')[1].length || 0; + if ( + !isNaN(value) && + Math.floor(value) !== value && + value.toString().includes('e') + ) + return parseInt(value.toString().split('e-')[1] || '0'); + return 0; +} + export async function simulateTransaction( connection: Connection, transaction: Transaction, @@ -368,4 +384,3 @@ export function decodeRecentEvents( return { header, nodes }; } - diff --git a/tests/Stateless.test.ts b/tests/Stateless.test.ts index 993feb8..2cb078c 100644 --- a/tests/Stateless.test.ts +++ b/tests/Stateless.test.ts @@ -2,7 +2,7 @@ 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 } from '@project-serum/serum'; +import { Market, OpenOrders } from '@project-serum/serum'; import { expect } from 'chai'; import { spawn } from 'child_process'; import { blob, struct, u8, nu64 } from 'buffer-layout'; @@ -18,9 +18,12 @@ if (!process.env.AGGREGATOR_PATH) { import { _sendTransaction, createWalletAndRequestAirdrop, + getSpotMarketDetails, createMangoGroupSymbolMappings, createTokenAccountWithBalance, - getOwnedTokenAccounts, + getMinSizeAndPriceForMarket, + placeOrderUsingSerumDex, + cancelOrdersUsingSerumDex, getAndDecodeBidsAndAsksForOwner, performSingleDepositOrWithdrawal, getAndDecodeBidsAndAsks, @@ -55,23 +58,17 @@ function chunkOrders(orders: any[], chunkSize: number) { }, []); } -async function initAccountsWithBalances(neededBalances: number[]) { +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); + await createTokenAccountWithBalance(connection, owner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, x, wrappedSol); } })); prettyPrintOwnerKeys(owner, "Account"); -} - -async function getSpotMarketDetails(mangoGroupSpotMarket: any): Promise { - const [spotMarketSymbol, spotMarketAddress] = mangoGroupSpotMarket; - const [baseSymbol, quoteSymbol] = spotMarketSymbol.split('/'); - const spotMarket = await Market.load(connection, new PublicKey(spotMarketAddress), { skipPreflight: true, commitment: 'singleGossip'}, dexProgramId); - return { spotMarket, baseSymbol, quoteSymbol }; + return owner; } async function requestPriceChange(mangoGroup: MangoGroup, requiredPrice: number, baseSymbol: string) { @@ -107,7 +104,7 @@ 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(mangoGroupSpotMarket); + 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(); @@ -124,7 +121,8 @@ async function cleanOrderBook(mangoGroupSpotMarket: any) { 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 * 1e2 ) / 1e2; + 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); @@ -135,7 +133,8 @@ async function cleanOrderBook(mangoGroupSpotMarket: any) { 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 * 1e2 ) / 1e2; + 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); @@ -145,6 +144,7 @@ async function cleanOrderBook(mangoGroupSpotMarket: any) { expect(allBids).to.be.empty; prettyPrintOwnerKeys(owner, "Cleaner"); } catch (error) { + console.info(error); throw new Error(` Test Error: ${error.message}, ${prettyPrintOwnerKeys(owner, "Cleaner")} @@ -163,19 +163,14 @@ async function placeNOrdersAfterLimit(mangoGroupSpotMarket: any, marketIndex: nu const buyerMarginAccountPk = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, buyerOwner); let buyerMarginAccount = await client.getMarginAccount(connection, buyerMarginAccountPk, dexProgramId); - const { spotMarket, baseSymbol, quoteSymbol } = await getSpotMarketDetails(mangoGroupSpotMarket); - const baseSymbolIndex = mangoGroupSymbols.findIndex(x => x === baseSymbol); - const quoteSymbolIndex = mangoGroupSymbols.findIndex(x => x === quoteSymbol); + 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 neededBaseAmountForAllTrades = orderSize * orderQuantity; const neededQuoteAmountForAllTrades = neededQuoteAmount * orderQuantity; - console.info("neededQuoteAmountForAllTrades:", neededQuoteAmountForAllTrades); 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++) { @@ -186,7 +181,6 @@ async function placeNOrdersAfterLimit(mangoGroupSpotMarket: any, marketIndex: nu 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 - // NOTE: Maybe trying cancelling last order not first 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); @@ -209,7 +203,7 @@ async function stressTestMatchOrder(mangoGroupSpotMarket: any, orderQuantity: nu const sellerMarginAccountPk = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, sellerOwner); let sellerMarginAccount = await client.getMarginAccount(connection, sellerMarginAccountPk, dexProgramId); - const { spotMarket, baseSymbol, quoteSymbol } = await getSpotMarketDetails(mangoGroupSpotMarket); + const { spotMarket, baseSymbol, quoteSymbol } = await getSpotMarketDetails(connection, mangoGroupSpotMarket, dexProgramId); const [orderSize, orderPrice, _] = await getOrderSizeAndPrice(connection, spotMarket, mangoGroupTokenMappings, baseSymbol, quoteSymbol, 'sell'); const neededQuoteAmount = orderPrice * orderSize; @@ -248,69 +242,106 @@ async function stressTestMatchOrder(mangoGroupSpotMarket: any, orderQuantity: nu expect(allBids).to.be.empty; } -async function stressTestLiquidation(mangoGroupSpotMarket: any, orderQuantity: number, shouldPartialLiquidate: boolean = false) { +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("orderQuantity:", orderQuantity) let bna: any, allAsks: any[], allBids: any[], prices: number[]; - let leverageCoefficient = 15; - const liqeeOwner = await createWalletAndRequestAirdrop(connection, 5); + 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 = await client.initMarginAccount(connection, mangoProgramId, mangoGroup, liqeeOwner); + 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(mangoGroupSpotMarket); + 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, 'buy'); - const neededQuoteAmount = orderPrice * orderSize; - const neededBaseAmountForAllTrades = orderSize * orderQuantity; + 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; - console.info("neededBaseAmountForAllTrades:", neededBaseAmountForAllTrades); - - await createTokenAccountWithBalance(connection, liqeeOwner, baseSymbol, mangoGroupTokenMappings, clusterIds.faucets, neededBaseAmountForAllTrades); - await performSingleDepositOrWithdrawal(connection, liqeeOwner, client, mangoGroup, mangoProgramId, baseSymbol, mangoGroupTokenMappings, liqeeMarginAccount, 'deposit', neededBaseAmountForAllTrades); - - prices = await requestPriceChange(mangoGroup, orderPrice, baseSymbol); + 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 buy order of ${orderSize} ${baseSymbol} for ${orderPrice} ${quoteSymbol} = ~${neededQuoteAmount} ${quoteSymbol} - ${i + 1}/${orderQuantity}`); + console.info(`Placing a ${side} order of ${finalOrderSize} ${baseSymbol} for ${finalOrderPrice} ${quoteSymbol} = ~${neededQuoteAmount} ${quoteSymbol} - ${i + 1}/${orderQuantity}`); liqeeMarginAccount = await client.getMarginAccount(connection, liqeeMarginAccountPk, dexProgramId); - await client.placeAndSettle(connection, mangoProgramId, mangoGroup, liqeeMarginAccount, spotMarket, liqeeOwner, 'buy', orderPrice * 2, orderSize); + console.info("Deposits:", liqeeMarginAccount.getAssets(mangoGroup)); + console.info("Assets:", liqeeMarginAccount.getAssets(mangoGroup)); + await client.placeAndSettle(connection, mangoProgramId, mangoGroup, liqeeMarginAccount, spotMarket, liqeeOwner, side, finalOrderPrice, finalOrderSize); } + if (matchLeveragedOrder) cleanOrderBook(mangoGroupSpotMarket); + liqeeMarginAccount = await client.getMarginAccount(connection, liqeeMarginAccountPk, dexProgramId); - console.info("collRatio before price change:", liqeeMarginAccount.getCollateralRatio(mangoGroup, prices)); - prices = await requestPriceChange(mangoGroup, orderPrice / leverageCoefficient, baseSymbol); + const adjustedPrice = (side === 'buy') ? finalOrderPrice / leverageCoefficient : finalOrderPrice * leverageCoefficient; + prices = await requestPriceChange(mangoGroup, adjustedPrice, baseSymbol); console.info("collRatio after price change:", liqeeMarginAccount.getCollateralRatio(mangoGroup, prices)); - const liqorOwner = await createWalletAndRequestAirdrop(connection, 5); - prettyPrintOwnerKeys(liqeeOwner, "Liqor"); - for (let mangoGroupSymbol of mangoGroupSymbols) { - const requiredBalance = (mangoGroupSymbol === quoteSymbol) ? neededQuoteAmountForAllTrades : 0; - await createTokenAccountWithBalance(connection, liqorOwner, mangoGroupSymbol, mangoGroupTokenMappings, clusterIds.faucets, requiredBalance); + 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) { + 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); + } } - const tokenWallets = (await Promise.all( - mangoGroup.tokens.map( - (mint) => findLargestTokenAccountForOwner(connection, liqorOwner.publicKey, mint).then( - (response) => response.publicKey - ) - ) - )); - let liquidationTxHash: string; - if (shouldPartialLiquidate) { - 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'; @@ -319,115 +350,57 @@ 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[0]; //BTC/USDC // const mangoGroupSpotMarket = mangoGroupSpotMarkets[1]; //ETH/USDC -// const mangoGroupSpotMarket = mangoGroupSpotMarkets[2]; //SOL/USDC +const mangoGroupSpotMarket = mangoGroupSpotMarkets[2]; //SOL/USDC // const mangoGroupSpotMarket = mangoGroupSpotMarkets[3]; //SRM/USDC -describe('Log stuff', async() => { - // 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'], - // ['ETH/USDC', '4tSvZvnbyzHXLMTiFonMyxZoHmFqau1XArcRCVHLZ5gX'], - // ['SOL/USDC', '9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT'], - // ['SRM/USDC', 'ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA'], - // ]; - // 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()}`); - // } - // }) - // it ('should log order', async() => { - // const { spotMarket } = await getSpotMarketDetails(mangoGroupSpotMarket); - // 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(allBids); - // }) -}) - -describe('create account with test money', async() => { - it('should create an account with test money', async() => { - await initAccountsWithBalances([50, 50, 50, 1000, 100000]); +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 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); - // }); + 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, 1); - // }); - // it('should liquidate an account with 10 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 10); - // }); - // it('should liquidate an account with 20 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 20); - // }); - // it('should liquidate an account with 25 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 25); - // }); - // it('should liquidate an account with 128 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 128); - // }); + 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() => { @@ -435,18 +408,161 @@ describe('stress testing partial liquidation', async() => { // await cleanOrderBook(mangoGroupSpotMarket); // }); it('should partially liquidate an account with 1 open order', async() => { - await stressTestLiquidation(mangoGroupSpotMarket, 1, true); + await stressTestLiquidation({mangoGroupSpotMarket, orderQuantity: 1, shouldPartialLiquidate: true}); }); - // it('should partially liquidate an account with 10 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 10, true); - // }); - // it('should partially liquidate an account with 20 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 20, true); - // }); - // it('should partially liquidate an account with 25 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 25, true); - // }); - // it('should partially liquidate an account with 128 open orders', async() => { - // await stressTestLiquidation(mangoGroupSpotMarket, 128, 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 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: false }); + }) }); + +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()); + }); +}) diff --git a/tests/test_utils.ts b/tests/test_utils.ts index 638dcfb..0662bc5 100644 --- a/tests/test_utils.ts +++ b/tests/test_utils.ts @@ -1,9 +1,10 @@ -import { MangoClient, MangoGroup, MarginAccount } from '../src/client'; -import { Account, Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionSignature, TransferParams } from '@solana/web3.js'; +import { MangoClient, MangoGroup } from '../src/client'; +import { Account, Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction, TransactionSignature } from '@solana/web3.js'; import { Market, TokenInstructions, OpenOrders, Orderbook } from '@project-serum/serum'; +import { Order } from '@project-serum/serum/lib/market'; import { token } from '@project-serum/common'; import { u64, NATIVE_MINT } from "@solana/spl-token"; -import { sleep } from '../src/utils'; +import { sleep, getDecimalCount } from '../src/utils'; console.log = function () {}; // NOTE: Disable all unnecessary logging @@ -20,6 +21,7 @@ export async function _sendTransaction ( transaction: Transaction, signers: Account[], ): Promise { + await sleep(1000) const signature = await connection.sendTransaction(transaction, signers); try { await connection.confirmTransaction(signature); @@ -115,6 +117,18 @@ export async function createWalletAndRequestAirdrop( return owner; } +export async function getSpotMarketDetails( + connection: Connection, + mangoGroupSpotMarket: any, + dexProgramId: PublicKey +): Promise<{spotMarket: Market, baseSymbol: string, quoteSymbol: string, minSize: number, minPrice: number}> { + const [spotMarketSymbol, spotMarketAddress] = mangoGroupSpotMarket; + const [baseSymbol, quoteSymbol] = spotMarketSymbol.split('/'); + const spotMarket = await Market.load(connection, new PublicKey(spotMarketAddress), { skipPreflight: true, commitment: 'singleGossip'}, dexProgramId); + const { minSize, minPrice } = getMinSizeAndPriceForMarket(spotMarket); + return { spotMarket, baseSymbol, quoteSymbol, minSize: minSize as number, minPrice: minPrice as number }; +} + export async function createMangoGroupSymbolMappings ( connection: Connection, mangoGroupIds: any, @@ -128,28 +142,6 @@ export async function createMangoGroupSymbolMappings ( return mangoGroupTokenMappings; } -export async function getOwnedTokenAccounts( - connection: Connection, - owner: Account, -): Promise { - const ownedTokenAccounts = await token.getOwnedTokenAccounts(connection, owner.publicKey); - return ownedTokenAccounts; -} - -export async function updateMarginTokenAccountsAndDeposits( - connection: Connection, - owner: Account, - client: MangoClient, - mangoGroup: MangoGroup, - marginAccountPk: PublicKey | null, - state: any, - dexProgramId: PublicKey, -): Promise{ - state.ownedTokenAccounts = await token.getOwnedTokenAccounts(connection, owner.publicKey); - state.marginAccount = (marginAccountPk) ? await client.getMarginAccount(connection, marginAccountPk, dexProgramId) : null; - state.deposits = (state.marginAccount) ? state.marginAccount.getDeposits(mangoGroup) : []; -} - export async function buildAirdropTokensIx( amount: u64, tokenMintPublicKey: PublicKey, @@ -188,24 +180,6 @@ export async function airdropTokens( return tokenDestinationPublicKey.toBase58(); }; -export async function airdropToken( - connection: Connection, - owner: Account, - tokenName: string, - mangoGroupTokenMappings: any, - faucetIds: any, - amount: number -): Promise { - if (tokenName !== 'SOL') throw new Error('This airdrop is function is not meant for SOL'); - const ownedTokenAccounts = await token.getOwnedTokenAccounts(connection, owner.publicKey); - const tokenMapping: any = Object.values(mangoGroupTokenMappings).find((x: any) => x.tokenName === tokenName); - const { tokenMint, decimals } = tokenMapping; - const ownedTokenAccount = ownedTokenAccounts.find((x: any) => x.account.mint.equals(tokenMint)); - if (!ownedTokenAccount) throw new Error(`Token account doesn't exist for ${tokenName}`); - const multiplier = Math.pow(10, decimals); - await airdropTokens(connection, owner, faucetIds[tokenName], ownedTokenAccount.publicKey, tokenMint, new u64(amount * multiplier)); -} - export async function airdropSol( connection: Connection, owner: Account, @@ -223,26 +197,56 @@ export async function airdropSol( await _sendTransaction(connection, tx, signers); } -export async function airdropMangoGroupTokens( +export function getMinSizeAndPriceForMarket( + spotMarket: Market +): { minSize : string | number, minPrice: string | number} { + const minSize = spotMarket?.minOrderSize?.toFixed(getDecimalCount(spotMarket.minOrderSize)) || spotMarket?.minOrderSize; + const minPrice = spotMarket?.tickSize?.toFixed(getDecimalCount(spotMarket.tickSize)) || spotMarket?.tickSize; + return { minSize, minPrice }; +} + +export async function placeOrderUsingSerumDex( connection: Connection, owner: Account, - mangoGroup: MangoGroup, - mangoGroupTokenMappings: any, - ownedTokenAccounts: any, - faucetIds: any -): Promise { - (await Promise.all( - mangoGroup.tokens.map(async (mint: PublicKey) => { - const {tokenName, decimals} = mangoGroupTokenMappings[mint.toString()]; - if (tokenName) { - const ownedTokenAccount = ownedTokenAccounts.find((x: any) => x.account.mint.equals(mint)); - if (tokenName !== 'SOL') { - const multiplier = Math.pow(10, decimals); - await airdropTokens(connection, owner, faucetIds[tokenName], ownedTokenAccount.publicKey, mint, new u64(100 * multiplier)); - } - } - }) - )); + spotMarket: Market, + baseCurrencyAccount: PublicKey, + quoteCurrencyAccount: PublicKey, + orderParams: any +): Promise { + const { side, price, size, orderType = 'limit', feeDiscountPubkey = undefined } = orderParams; + const { minSize, minPrice } = getMinSizeAndPriceForMarket(spotMarket); + const isIncrement = (num: number, step: number) => Math.abs((num / step) % 1) < 1e-5 || Math.abs(((num / step) % 1) - 1) < 1e-5; + if (isNaN(price)) throw Error('Invalid price'); + if (isNaN(size)) throw Error('Invalid size'); + if (!spotMarket) throw Error('Invalid market'); + if (!isIncrement(size, spotMarket.minOrderSize)) throw Error(`Size must be an increment of ${minSize}`); + if (size < spotMarket.minOrderSize) throw Error('Size too small'); + if (!isIncrement(price, spotMarket.tickSize)) throw Error(`Price must be an increment of ${minPrice}`); + if (price < spotMarket.tickSize) throw Error('Price too small'); + const transaction = new Transaction(); + const signers: Account[] = [owner]; + const payer = side === 'sell' ? baseCurrencyAccount : quoteCurrencyAccount; + if (!payer) throw Error('Associated account for spend currency is missing'); + const params = { owner: owner.publicKey, payer, side, price, size, orderType, feeDiscountPubkey: feeDiscountPubkey || null}; + let { transaction: placeOrderTx, signers: placeOrderSigners } = await spotMarket.makePlaceOrderTransaction( connection, params, 120_000, 120_000 ); + transaction.add(placeOrderTx); + signers.push(...placeOrderSigners); + return await _sendTransaction(connection, transaction, signers); +} + +export async function cancelOrdersUsingSerumDex( + connection: Connection, + owner: Account, + spotMarket: Market, + orders: Order[] +): Promise{ + const transaction = new Transaction(); + orders.forEach((order) => { + transaction.add( + spotMarket.makeCancelOrderInstruction(connection, owner.publicKey, order), + ); + }); + return await _sendTransaction(connection, transaction, [owner]); } export async function createTokenAccountWithBalance( @@ -251,44 +255,26 @@ export async function createTokenAccountWithBalance( tokenName: string, mangoGroupTokenMappings: any, faucetIds: any, - amount: number + amount: number, + wrappedSol: boolean = true, ) { const tokenMapping: any = Object.values(mangoGroupTokenMappings).find((x: any) => x.tokenName === tokenName); const { tokenMint, decimals } = tokenMapping; const multiplier = Math.pow(10, decimals); const processedAmount = amount * multiplier; + let ownedTokenAccountPk: PublicKey | null = null; if (tokenName === 'SOL') { await airdropSol(connection, owner, amount); - await createWrappedNativeAccount(connection, owner, processedAmount); + if (wrappedSol) { + ownedTokenAccountPk = await createWrappedNativeAccount(connection, owner, processedAmount); + } } else { - await createTokenAccount(connection, tokenMint, owner); + ownedTokenAccountPk = await createTokenAccount(connection, tokenMint, owner); if (amount > 0) { - const ownedTokenAccounts = await token.getOwnedTokenAccounts(connection, owner.publicKey); - const ownedTokenAccount = ownedTokenAccounts.find((x: any) => x.account.mint.equals(tokenMint)); - if (!ownedTokenAccount) throw new Error(`Token account doesn't exist for ${tokenName}`); - await airdropTokens(connection, owner, faucetIds[tokenName], ownedTokenAccount.publicKey, tokenMint, new u64(processedAmount)); + await airdropTokens(connection, owner, faucetIds[tokenName], ownedTokenAccountPk, tokenMint, new u64(processedAmount)); } } -} - -export async function createTokenAccountsForMangoGroupTokens ( - connection: Connection, - owner: Account, - mangoGroup: MangoGroup, - mangoGroupTokenMappings: any, -) { - (await Promise.all( - mangoGroup.tokens.map(async (mint: PublicKey) => { - const {tokenName} = mangoGroupTokenMappings[mint.toString()]; - if (tokenName) { - if (tokenName === 'SOL') { - await createWrappedNativeAccount(connection, owner, 100 * 1e9); - } else { - await createTokenAccount(connection, mint, owner); - } - } - }) - )); + return ownedTokenAccountPk; } export async function performSingleDepositOrWithdrawal ( @@ -309,34 +295,13 @@ export async function performSingleDepositOrWithdrawal ( const ownedTokenAccount = ownedTokenAccounts.find((x: any) => x.account.mint.equals(tokenMint)); if (!ownedTokenAccount) throw new Error(`Token account doesn't exist for ${tokenName}`); if (type === 'deposit') { + // TODO: Add wrapped SOL functionality here instead of creating an account for Wrapped SOL in default await client.deposit(connection, mangoProgramId, mangoGroup, marginAccount, owner, tokenMint, ownedTokenAccount.publicKey, Number(amount)); } else if (type === 'withdraw') { await client.withdraw(connection, mangoProgramId, mangoGroup, marginAccount, owner, tokenMint, ownedTokenAccount.publicKey, Number(amount)); } } -export async function performDepositOrWithdrawal ( - connection: Connection, - owner: Account, - client: MangoClient, - mangoGroup: MangoGroup, - mangoProgramId: PublicKey, - state: any, - type: string, - amount: number -) { - (await Promise.all( - mangoGroup.tokens.map(async (mint: PublicKey) => { - const ownedTokenAccount = state.ownedTokenAccounts.find((x: any) => x.account.mint.equals(mint)); - if (type === 'deposit') { - await client.deposit(connection, mangoProgramId, mangoGroup, state.marginAccount, owner, mint, ownedTokenAccount.publicKey, Number(amount)); - } else if (type === 'withdraw') { - await client.withdraw(connection, mangoProgramId, mangoGroup, state.marginAccount, owner, mint, ownedTokenAccount.publicKey, Number(amount)); - } - }) - )); -} - export async function getAndDecodeBidsAndAsks ( connection: Connection, spotMarket: Market @@ -367,6 +332,7 @@ export async function getBidOrAskPriceEdge( bidOrAsk: string, maxOrMin: string ): Promise{ + // TODO: Refactor this function to use minSize and minPrice prices const { bidOrderBook, askOrderBook } = await getAndDecodeBidsAndAsks(connection, spotMarket); const [orderBookSide, orderBookOtherSide] = (bidOrAsk === 'bid' ? [bidOrderBook, askOrderBook] : [askOrderBook, bidOrderBook]); const orderBookSidePrices: number[] = [...orderBookSide].map(x => x.price); @@ -397,7 +363,7 @@ export async function getOrderSizeAndPrice( // NOTE: Always use minOrderSize const tokenMapping: any = Object.values(mangoGroupTokenMappings).find((x: any) => x.tokenName === baseSymbol); const { decimals } = tokenMapping; - const [stepSize, orderSize] = (decimals === 6) ? [0.01, 1] : [10, 0.01]; + const [stepSize, orderSize] = (decimals === 6) ? [0.1, 1] : [1, 0.1]; const edge = (side === 'buy') ? ['bid', 'max'] : ['ask', 'min']; const orderPrice: number = Math.max(await getBidOrAskPriceEdge(connection, spotMarket, edge[0], edge[1]), stepSize); return [orderSize, orderPrice, stepSize];