import { Config, GroupConfig, I80F48, IDL, IDS, PerpEventLayout, PerpMarketConfig, } from "@blockworks-foundation/mango-client"; import { Coder } from "@project-serum/anchor"; import { getLatestObjPerCombination } from './utils'; // Unfortunately ids.json does not correspond to the token indexes in the log - so keep a map here for reference // mango group -> token index -> mint key // TODO: is there a better way? var tokenIndexesMap = { '98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue': { 0: 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac', 1: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E', 2: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk', 3: 'So11111111111111111111111111111111111111112', 4: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 5: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', 6: '4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R', 7: '8HGyAAB1yoM1ttS7pXjHMa3dukTFGQggnFFH3hJZgzQh', 8: 'AGFEad2et2ZJif9jaGpdMixQqvW5i81aBdvKe7PHNfz3', 10:'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So', 11: '9gP2kCy3wA1ctvYWQk75guqXuHfrEomqydHLtcTCqiLa', 12: 'KgV1GvrHQmRBY8sHQQeUKwTm2r2h8t4C8qt12Cw1HVE', 13: 'F6v4wfAdJB8D8p77bMXZgYt8TDKsYxLYxH5AFhUkYx9W', 15: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', }, '4yJ2Vx3kZnmHTNCrHzdoj5nCwriF2kVhfKNvqC6gU8tr': { 0: 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac', 1: '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E', 2: '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk', 3: 'So11111111111111111111111111111111111111112', 4: 'SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt', 5: 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB', 15: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', }, }; // mango group -> token index -> mint key var oracleIndexesMap = { '98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue': { 0: '49cnp1ejyvQi3CJw3kKXNCDGnNbWDuZd3UG3Y2zGvQkX', 1: 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU', 2: 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB', 3: 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG', 4: '3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL', 5: '3NBReDRTLKMQEKiLD5tGcx4kXbTf88b7f2xLS9UuGjym', 6: 'AnLf8tVYCM816gmBjiy8n53eXKKEDydT5piYjjQDPgTB', 7: '9xYBiDWYsh2fHzpsz3aaCnNHCKWBNtfEDLtU6kS4aFD9', 8: '8JPJJkmDScpcNmBRKGZuPuG2GYAveQgP3t5gFuMymwvF', 9: '3pyn4svBbxJ9Wnn3RVeafyLWfzie6yC5eTig2S62v9SC', 10:'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9', 11: '4CkQJBxhU8EZ2UjhigbtdaPbpTe6mqf811fipYBFbSYN', 12: 'C2GXZT21UUm6G3J4N26h6d3tpgfUhh6ok5aNi24K88Wu', 13: '5bmWuR1dgP4avtGYMNKLuxumZTVKGgoN2BCMXWDNL9nY' }, '4yJ2Vx3kZnmHTNCrHzdoj5nCwriF2kVhfKNvqC6gU8tr': { 0: '49cnp1ejyvQi3CJw3kKXNCDGnNbWDuZd3UG3Y2zGvQkX', 1: 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU', 2: 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB', 3: 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG', 4: '3NBReDRTLKMQEKiLD5tGcx4kXbTf88b7f2xLS9UuGjym', 5: '3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL', }, }; // Lot sizes also not available on ids.json // mango group -> perp name -> lot sizes var perpLotSizes = { '98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue': { 'MNGO-PERP': { 'pk': '4nfmQP3KmUqEJ6qJLsS3offKgE96YUB4Rp7UQvm2Fbi9', 'baseLotSize': 1000000, 'quoteLotSize': 100 }, 'BTC-PERP': { 'pk': 'DtEcjPLyD4YtTBB4q8xwFZ9q49W89xZCZtJyrGebi5t8', 'baseLotSize': 100, 'quoteLotSize': 10 }, 'ETH-PERP': { 'pk': 'DVXWg6mfwFvHQbGyaHke4h3LE9pSkgbooDSDgA4JBC8d', 'baseLotSize': 1000, 'quoteLotSize': 100 }, 'SOL-PERP': { 'pk': '2TgaaVoHgnSeEtXvWTx13zQeTf4hYWAMEiMQdcG6EwHi', 'baseLotSize': 10000000, 'quoteLotSize': 100 }, 'SRM-PERP': { 'pk': '4GkJj2znAr2pE2PBbak66E12zjCs2jkmeafiJwDVM9Au', 'baseLotSize': 100000, 'quoteLotSize': 100 }, 'RAY-PERP': { 'pk': '6WGoQr5mJAEpYCdX6qjju2vEnJuD7e8ZeYes7X7Shi7E', 'baseLotSize': 100000, 'quoteLotSize': 100 }, 'FTT-PERP': { 'pk': 'AhgEayEGNw46ALHuC5ASsKyfsJzAm5JY8DWqpGMQhcGC', 'baseLotSize': 100000, 'quoteLotSize': 100 }, 'ADA-PERP': { 'pk': 'Bh9UENAncoTEwE7NDim8CdeM1GPvw6xAT4Sih2rKVmWB', 'baseLotSize': 1000000, 'quoteLotSize': 100 }, 'BNB-PERP': { 'pk': 'CqxX2QupYiYafBSbA519j4vRVxxecidbh2zwX66Lmqem', 'baseLotSize': 100000, 'quoteLotSize': 100 }, 'AVAX-PERP': { 'pk': 'EAC7jtzsoQwCbXj1M3DapWrNLnc3MBwXAarvWDPr2ZV9', 'baseLotSize': 1000000, 'quoteLotSize': 100 }, 'LUNA-PERP': { 'pk': 'BCJrpvsB2BJtqiDgKVC4N6gyX1y24Jz96C6wMraYmXss', 'baseLotSize': 10000, 'quoteLotSize': 100 } } } var ids = IDS; export function anchorParser(parsedTransactions, result, signature, blockTime, slot, blockDatetime) { let serializedLogMessages: any = []; const coder = new Coder(IDL); if (result.meta.logMessages.includes('Log truncated')) { throw new Error("Log truncated"); } for (let i = 0; i < result.meta.logMessages.length; i++) { const logMessage = result.meta.logMessages[i]; const jsonStartStr = "Program log: mango-log"; if (logMessage.startsWith(jsonStartStr)) { const serializedMangoLog = result.meta.logMessages[i + 1].slice( "Program log: ".length ); serializedLogMessages.push(serializedMangoLog); } } // Can have multiple inserts per signature so add instructionNum column to allow a primary key let eventNum = 1; let allNetBalances: any = []; for (const log of serializedLogMessages) { const decodedEvent = coder.events.decode(log); const eventName = decodedEvent?.name; const eventData = decodedEvent?.data as any; if (eventName === "CachePricesLog") { parsedTransactions.cache_prices.push( ...parseCachePrices( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "CacheRootBanksLog") { parsedTransactions.cache_indexes.push( ...parseCacheRootBanks( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "DepositLog" || eventName === "WithdrawLog") { parsedTransactions.deposits_withdraws.push( parseDepositWithDraw( eventNum, eventData, eventName, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "TokenBalanceLog") { // Net balances is a special case - we only want to keep the latest change to net balance for each // (mangoAccount, symbol) pair in each transaction allNetBalances.push( parseTokenBalance( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "RedeemMngoLog") { parsedTransactions.redeem_mngo.push( parseRedeemMngo( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "FillLog") { parsedTransactions.fill_events.push( parseFillLog( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "LiquidateTokenAndTokenLog") { parsedTransactions.liquidate_token_and_token.push( parseLiquidateTokenAndToken( eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "LiquidateTokenAndPerpLog") { parsedTransactions.liquidate_token_and_perp.push( parseLiquidateTokenAndPerp( eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "LiquidatePerpMarketLog") { parsedTransactions.liquidate_perp_market.push( parseLiquidatePerpMarket( eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "TokenBankruptcyLog") { parsedTransactions.token_bankruptcy.push( parseTokenBankruptcy( eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "PerpBankruptcyLog") { const [perpBankruptcyRow, updateFundingRow] = parsePerpBankruptcy( eventNum, eventData, signature, blockTime, slot, blockDatetime ); parsedTransactions.perp_bankruptcy.push(perpBankruptcyRow); parsedTransactions.funding.push(updateFundingRow); } else if (eventName === "SettlePnlLog") { parsedTransactions.settle_pnl.push( ...parseSettlePnl( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "SettleFeesLog") { parsedTransactions.settle_fees.push( parseSettleFees( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "UpdateFundingLog") { parsedTransactions.funding.push( parseUpdateFunding( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else if (eventName === "UpdateRootBankLog") { parsedTransactions.cache_indexes.push( parseUpdateRootBank( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ) } else if (eventName === "CachePerpMarketsLog") { // pass - logging from CachePerpMarkets is redundant - can just log from updateFunding // parsedTransactions.funding.push( // ...parseCachePerpMarkets( // eventNum, // eventData, // signature, // blockTime, // slot, // blockDatetime // ) // ) } else if (eventName === "OpenOrdersBalanceLog") { parsedTransactions.open_orders_balances.push( parseOpenOrdersBalance( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ) } else if (eventName === "MngoAccrualLog") { parsedTransactions.mango_accrual.push( parseMngoAccrual( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ) } else if (eventName === "CancelAllPerpOrdersLog") { parsedTransactions.cancel_all_perp_orders.push( ...parseCancelAllPerpOrders( eventNum, eventData, signature, blockTime, slot, blockDatetime ) ); } else { throw new Error("Unknown anchor event: " + eventName); } eventNum++; } // Extract the latest (mangoAccount, symbol) pair from net balances changes in the transaction parsedTransactions.net_balances.push(...getLatestObjPerCombination(allNetBalances, ['mango_account','symbol'])) } function parseMngoAccrual( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { const config = new Config(IDS); const groupConfig = config.groups.find((g) => g.publicKey.equals(eventData.mangoGroup) ) as GroupConfig; let perpMarketConfig = groupConfig.perpMarkets.find( (p) => p.marketIndex === eventData.marketIndex.toNumber() ) as PerpMarketConfig; let mangoDecimals = groupConfig.tokens.find((e) => e["symbol"] === 'MNGO')!.decimals let mangoAccrualUi = eventData.mngoAccrual.toNumber() / Math.pow(10, mangoDecimals) return { margin_account: eventData.mangoAccount.toString(), perp_market: perpMarketConfig.name, base_symbol: perpMarketConfig.baseSymbol, mango_accrual: mangoAccrualUi, instruction_num: instructionNum, mango_group: eventData.mangoGroup.toString(), block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, } } function parseOpenOrdersBalance( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroup = eventData.mangoGroup.toString() let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroup)[ "tokens" ]; let tokenIndex = eventData.marketIndex.toNumber() let tokenPk = tokenIndexesMap[mangoGroup][tokenIndex]; let token = tokens.find((e) => e["mintKey"] === tokenPk); let symbol = token.symbol; let baseDecimals = token.decimals // Assuming that quote currency is always USDC let quoteDecimals = tokens.find((e) => e.symbol === 'USDC').decimals; let baseFree = eventData.baseFree.toNumber() / Math.pow(10, baseDecimals) let baseTotal = eventData.baseTotal.toNumber() / Math.pow(10, baseDecimals) let quoteFree = eventData.quoteFree.toNumber() / Math.pow(10, quoteDecimals) let quoteTotal = eventData.quoteTotal.toNumber() / Math.pow(10, quoteDecimals) let referrerRebatesAccrued = eventData.referrerRebatesAccrued.toNumber() / Math.pow(10, quoteDecimals) let marginAccount = eventData.mangoAccount.toString() return { margin_account: marginAccount, symbol: symbol, base_free: baseFree, base_total: baseTotal, quote_free: quoteFree, quote_total: quoteTotal, referrer_rebates_accrued: referrerRebatesAccrued, instruction_num: instructionNum, mango_group: mangoGroup, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, } } function parseUpdateRootBank( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "tokens" ]; let tokenIndex = eventData.tokenIndex.toNumber(); let depositIndex = new I80F48(eventData.depositIndex).toNumber() let borrowIndex = new I80F48(eventData.borrowIndex).toNumber() let tokenPk = tokenIndexesMap[mangoGroupPk][tokenIndex]; let token = tokens.find((e) => e["mintKey"] === tokenPk); let symbol = token.symbol; return { symbol: symbol, deposit_index: depositIndex, borrow_index: borrowIndex, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, } } function parseCachePerpMarkets( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { const config = new Config(IDS); const groupConfig = config.groups.find((g) => g.publicKey.equals(eventData.mangoGroup) ) as GroupConfig; let out: any = [] for (let i=0; i p.marketIndex === marketIndex ) as PerpMarketConfig; out.push( { symbol: perpMarketConfig.baseSymbol, long_funding: new I80F48(eventData.longFundings[i]).toNumber(), short_funding: new I80F48(eventData.shortFundings[i]).toNumber(), instruction_num: instructionNum, mango_group: eventData.mangoGroup.toString(), block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, } ) } return out } function parseFillLog( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { // instructionNum is used here to form a primary key on the db table (with signature) let mangoGroupPk = eventData.mangoGroup.toString(); let perpMarkets = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "perpMarkets" ]; let marketIndex = eventData.marketIndex.toNumber(); let perpMarket = perpMarkets.find((e) => e["marketIndex"] === marketIndex); let lotSizes = perpLotSizes[mangoGroupPk][perpMarket.name] const nativeToUi = Math.pow(10, perpMarket.baseDecimals - perpMarket.quoteDecimals); const lotsToNative = lotSizes.quoteLotSize / lotSizes.baseLotSize const fill = { event_num: instructionNum, maker: eventData.maker.toString(), maker_fee: new I80F48(eventData.makerFee).toNumber(), maker_order_id: eventData.makerOrderId.toString(), maker_client_order_id: eventData.makerClientOrderId.toString(), price: eventData.price.toNumber() * lotsToNative * nativeToUi, // Storing both price and quantity in UI terms to be consistent with db quantity: eventData.quantity.toNumber() * lotSizes.baseLotSize / Math.pow(10, perpMarket.baseDecimals), seq_num: eventData.seqNum.toNumber(), taker: eventData.taker.toString(), taker_fee: new I80F48(eventData.takerFee).toNumber(), taker_order_id: eventData.takerOrderId.toString(), taker_client_order_id: eventData.takerClientOrderId.toString(), taker_side: eventData.takerSide === 0 ? 'bid' : 'ask', perp_market: perpMarket.name, base_symbol: perpMarket.baseSymbol, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; return fill; } function parseSettleFees( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); let mangoAccount = eventData.mangoAccount.toString(); let perpMarkets = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "perpMarkets" ]; let marketIndex = eventData.marketIndex.toNumber(); let perpMarket = perpMarkets.find((e) => e["marketIndex"] === marketIndex); let perpMarketName = perpMarket.name; // TODO: confirm correct conversion from i80f48 let settlement = new I80F48(eventData.settlement).toNumber() / Math.pow(10, perpMarket.quoteDecimals); return { margin_account: mangoAccount, settlement: settlement, perp_market_name: perpMarketName, base_symbol: perpMarket.baseSymbol, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseSettlePnl( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); let perpMarkets = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "perpMarkets" ]; let mangoAccountA = eventData.mangoAccountA.toString(); let mangoAccountB = eventData.mangoAccountB.toString(); let marketIndex = eventData.marketIndex.toNumber(); let perpMarket = perpMarkets.find((e) => e["marketIndex"] === marketIndex); let perpMarketName = perpMarket.name; let settlement = new I80F48(eventData.settlement).toNumber(); // A's quote position is reduced by settlement and B's quote position is increased by settlement let settlementA = (-1 * settlement) / Math.pow(10, perpMarket.quoteDecimals); let settlementB = settlement / Math.pow(10, perpMarket.quoteDecimals); return [ { margin_account: mangoAccountA, settlement: settlementA, perp_market_name: perpMarketName, base_symbol: perpMarket.baseSymbol, counterparty: mangoAccountB, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, { margin_account: mangoAccountB, settlement: settlementB, perp_market_name: perpMarketName, base_symbol: perpMarket.baseSymbol, counterparty: mangoAccountA, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]; } function parsePerpBankruptcy( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { const config = new Config(IDS); const groupConfig = config.groups.find((g) => g.publicKey.equals(eventData.mangoGroup) ) as GroupConfig; const perpMarketConfig = groupConfig.perpMarkets.find( (p) => p.marketIndex === eventData.liabIndex.toNumber() ) as PerpMarketConfig; let mangoGroupPk = eventData.mangoGroup.toString(); let liqee = eventData.liqee.toString(); let liqor = eventData.liqor.toString(); let perpMarkets = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "perpMarkets" ]; let quoteSymbol = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "quoteSymbol" ]; let quoteDecimals = ids["groups"] .find((e) => e["publicKey"] === mangoGroupPk) ["tokens"].find((e) => e.symbol === quoteSymbol).decimals; let liabIndex = eventData.liabIndex.toNumber(); let perpMarket = perpMarkets.find((e) => e["marketIndex"] === liabIndex); let perpMarketName = perpMarket.name; let perpBaseSymbol = perpMarket.baseSymbol; let insuranceFundTransfer = eventData.insuranceTransfer.toNumber() / Math.pow(10, quoteDecimals); // loss is on quote position let loss = new I80F48(eventData.socializedLoss).toNumber() / Math.pow(10, quoteDecimals); // TODO: are these needed? db columns don't exist const cacheLongFunding = new I80F48(eventData.cacheLongFunding).toNumber(); const cacheShortFunding = new I80F48(eventData.cacheShortFunding).toNumber(); return [ { liqor: liqor, liqee: liqee, perp_market_name: perpMarketName, perp_base_symbol: perpBaseSymbol, insurance_fund_transfer: insuranceFundTransfer, loss: loss, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, { symbol: perpMarketConfig.baseSymbol, long_funding: cacheLongFunding, short_funding: cacheShortFunding, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]; } function parseTokenBankruptcy( eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); let liqee = eventData.liqee.toString(); let liqor = eventData.liqor.toString(); let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "tokens" ]; let quoteSymbol = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "quoteSymbol" ]; let quoteDecimals = ids["groups"] .find((e) => e["publicKey"] === mangoGroupPk) ["tokens"].find((e) => e.symbol === quoteSymbol).decimals; // Either bankruptcy or socialized loss (or both) will be logged // So initialize variables as null - nulls can be outputted to json if variables are not set let symbol; let insuranceFundTransfer: number | null = null; let loss: number | null = null; let percentageLoss: number | null = null; let depositIndex: number | null = null; // TODO: validate this when an example comes along let tokenIndex = eventData.liabIndex.toNumber(); let tokenPk = tokenIndexesMap[mangoGroupPk][tokenIndex]; let token = tokens.find((e) => e["mintKey"] === tokenPk); symbol = token.symbol; insuranceFundTransfer = eventData.insuranceTransfer.toNumber() / Math.pow(10, quoteDecimals); loss = eventData.socializedLoss.toNumber() / Math.pow(10, token.decimals); percentageLoss = eventData.percentageLoss.toNumber(); symbol = token.symbol; // TODO: is this needed? didn't see it in the logs // depositIndex = socializedLossDetails["deposit_index"] return { liqor: liqor, liqee: liqee, symbol: symbol, insurance_fund_transfer: insuranceFundTransfer, loss: loss, percentage_loss: percentageLoss, deposit_index: depositIndex, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseLiquidatePerpMarket( eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); let liqee = eventData.liqee.toString(); let liqor = eventData.liqor.toString(); let marketIndex = eventData.marketIndex.toNumber(); let perpMarkets = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "perpMarkets" ]; let quoteSymbol = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "quoteSymbol" ]; let perpMarket = perpMarkets.find((e) => e["marketIndex"] === marketIndex); let perpMarketName = perpMarket.name; let liabDecimals = perpMarket.baseDecimals; let assetDecimals = perpMarket.quoteDecimals; let liabSymbol = perpMarket.baseSymbol; let assetSymbol = quoteSymbol; let baseTransfer = eventData.baseTransfer.toNumber() / Math.pow(10, liabDecimals); // TODO: quoteTransfer is -base_transfer * pmi.base_lot_size - but I don't really know what this means let quoteTransfer = new I80F48(eventData.quoteTransfer).toNumber() / Math.pow(10, assetDecimals); let bankruptcy = eventData.bankruptcy; return { liqor: liqor, liqee: liqee, perp_market: perpMarketName, liab_symbol: liabSymbol, liab_amount: baseTransfer, asset_symbol: assetSymbol, asset_amount: quoteTransfer, bankruptcy: bankruptcy, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseLiquidateTokenAndPerp( eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); let liqee = eventData.liqee.toString(); let liqor = eventData.liqor.toString(); let assetIndex = eventData.assetIndex.toNumber(); let liabIndex = eventData.liabIndex.toNumber(); let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "tokens" ]; let perpMarkets = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "perpMarkets" ]; let quoteSymbol = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "quoteSymbol" ]; let quoteDecimals = ids["groups"] .find((e) => e["publicKey"] === mangoGroupPk) ["tokens"].find((e) => e.symbol === quoteSymbol).decimals; let perpMarket; let perpBaseSymbol; let assetSymbol; let liabSymbol; let assetType = eventData.assetType === 0 ? 'Token': 'Perp'; let liabType = eventData.liabType === 0 ? 'Token': 'Perp'; let assetDecimals; let liabDecimals; let assetTokenPk = tokenIndexesMap[mangoGroupPk][assetIndex]; let assetToken = tokens.find((e) => e["mintKey"] === assetTokenPk); let liabTokenPk = tokenIndexesMap[mangoGroupPk][liabIndex]; let liabToken = tokens.find((e) => e["mintKey"] === liabTokenPk); if (assetType === "Token") { // asset is token and liab is perp assetSymbol = assetToken.symbol; assetDecimals = assetToken.decimals; let liabPerpMarket = perpMarkets.find( (e) => e["marketIndex"] === liabIndex ); // Liquidation can only occur on quote position on perp side // So I'll set the asset symbol to the quote symbol (as that is what is transferred) liabSymbol = quoteSymbol; liabDecimals = liabPerpMarket.quoteDecimals; perpMarket = liabPerpMarket.name; perpBaseSymbol = liabPerpMarket.baseSymbol; } else { // asset is perp and liab is token let assetPerpMarket = perpMarkets.find( (e) => e["marketIndex"] === assetIndex ); // Liquidation can only occur on quote position on perp side // So I'll set the asset symbol to the quote symbol (as that is what is transferred) assetSymbol = quoteSymbol; assetDecimals = assetPerpMarket.quoteDecimals; perpMarket = assetPerpMarket.name; perpBaseSymbol = assetPerpMarket.baseSymbol; liabSymbol = liabToken.symbol; liabDecimals = liabToken.decimals; } let assetPrice = new I80F48(eventData.assetPrice).toNumber() * Math.pow(10, assetDecimals - quoteDecimals); let liabPrice = new I80F48(eventData.liabPrice).toNumber() * Math.pow(10, liabDecimals - quoteDecimals); let assetTransfer = new I80F48(eventData.assetTransfer).toNumber() / Math.pow(10, assetDecimals); let liabTransfer = new I80F48(eventData.liabTransfer).toNumber() / Math.pow(10, liabDecimals); return { liqor: liqor, liqee: liqee, perp_market: perpMarket, liab_symbol: liabSymbol, liab_amount: liabTransfer, liab_price: liabPrice, liab_type: liabType, asset_symbol: assetSymbol, asset_amount: assetTransfer, asset_price: assetPrice, asset_type: assetType, perp_base_symbol: perpBaseSymbol, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseLiquidateTokenAndToken( eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroup = eventData.mangoGroup.toString(); let liqee = eventData.liqee.toString(); let liqor = eventData.liqor.toString(); let assetIndex = eventData.assetIndex.toNumber(); let liabIndex = eventData.liabIndex.toNumber(); let assetToken = ids["groups"] .find((e) => e["publicKey"] === mangoGroup) ["tokens"].find( (e) => e.mintKey === tokenIndexesMap[mangoGroup][assetIndex] ); let liabToken = ids["groups"] .find((e) => e["publicKey"] === mangoGroup) ["tokens"].find( (e) => e.mintKey === tokenIndexesMap[mangoGroup][liabIndex] ); let quoteSymbol = ids["groups"].find((e) => e["publicKey"] === mangoGroup)[ "quoteSymbol" ]; let quoteDecimals = ids["groups"] .find((e) => e["publicKey"] === mangoGroup) ["tokens"].find((e) => e.symbol === quoteSymbol).decimals; let assetPrice = new I80F48(eventData.assetPrice).toNumber() * Math.pow(10, assetToken.decimals - quoteDecimals); let liabPrice = new I80F48(eventData.liabPrice).toNumber() * Math.pow(10, liabToken.decimals - quoteDecimals); // TODO: confirm that this is correct let assetTransfer = new I80F48(eventData.assetTransfer).toNumber() / Math.pow(10, assetToken.decimals); let liabTransfer = new I80F48(eventData.liabTransfer).toNumber() / Math.pow(10, liabToken.decimals); let bankruptcy = eventData.bankruptcy; return { liqor: liqor, liqee: liqee, liab_symbol: liabToken.symbol, liab_amount: liabTransfer, liab_price: liabPrice, asset_symbol: assetToken.symbol, asset_amount: assetTransfer, asset_price: assetPrice, bankruptcy: bankruptcy, mango_group: mangoGroup, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseRedeemMngo( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroup = eventData.mangoGroup.toString(); let marginAccount = eventData.mangoAccount.toString(); const config = new Config(IDS); const groupConfig = config.groups.find((g) => g.publicKey.equals(eventData.mangoGroup) ) as GroupConfig; let perpMarketConfig = groupConfig.perpMarkets.find( (p) => p.marketIndex === eventData.marketIndex.toNumber() ) as PerpMarketConfig; let decimals = ids["groups"] .find((e) => e["publicKey"] === mangoGroup) ["tokens"].find((e) => e.symbol === "MNGO").decimals; let perpMarketName = perpMarketConfig.name let baseSymbol = perpMarketConfig.baseSymbol let quantity = eventData.redeemedMngo.toNumber() / Math.pow(10, decimals); return { margin_account: marginAccount, quantity: quantity, instruction_num: instructionNum, mango_group: mangoGroup, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, perp_market: perpMarketName, base_symbol: baseSymbol }; } function parseDepositWithDraw( instructionNum, eventData, eventName, signature, blockTime, slot, blockDatetime ) { let mangoGroup = eventData.mangoGroup.toString(); let marginAccount = eventData.mangoAccount.toString(); let owner = eventData.owner.toString(); let tokenIndex = eventData.tokenIndex.toNumber(); let nativeQuantity = eventData.quantity.toNumber(); let side; if (eventName === "DepositLog") { side = "Deposit"; } else if (eventName === "WithdrawLog") { side = "Withdraw"; } let tokenPk = tokenIndexesMap[mangoGroup][tokenIndex]; let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroup)[ "tokens" ]; let token = tokens.find((e) => e["mintKey"] === tokenPk); let mintDecimals = token.decimals; let symbol = token.symbol; let quantity = nativeQuantity / Math.pow(10, mintDecimals); return { margin_account: marginAccount, owner: owner, symbol: symbol, side: side, quantity: quantity, instruction_num: instructionNum, mango_group: mangoGroup, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseTokenBalance( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { const mangoGroupPk = eventData.mangoGroup.toString(); let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "tokens" ]; let quoteSymbol = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'quoteSymbol' ]; let quoteDecimals = ids['groups'] .find((e) => e['publicKey'] === mangoGroupPk) ['tokens'].find((e) => e.symbol === quoteSymbol).decimals; let mangoAccountPk = eventData.mangoAccount.toString(); let tokenIndex = eventData.tokenIndex.toNumber(); let tokenPk = tokenIndexesMap[mangoGroupPk][tokenIndex]; let token = tokens.find((e) => e["mintKey"] === tokenPk); let symbol = token.symbol; let deposit = new I80F48(eventData.deposit) .div(I80F48.fromNumber(Math.pow(10, token.decimals - quoteDecimals))) .toNumber(); let borrow = new I80F48(eventData.borrow) .div(I80F48.fromNumber(Math.pow(10, token.decimals - quoteDecimals))) .toNumber(); return { mango_account: mangoAccountPk, symbol: symbol, deposit: deposit, borrow: borrow, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseCacheRootBanks( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "tokens" ]; let cacheIndexes: any = []; let tokenIndexes = eventData.tokenIndexes.map((i) => i.toNumber()); let depositIndexes = eventData.depositIndexes.map((i) => new I80F48(i).toNumber() ); let borrowIndexes = eventData.borrowIndexes.map((i) => new I80F48(i).toNumber() ); for (let i = 0; i < tokenIndexes.length; i++) { let tokenIndex = tokenIndexes[i]; let tokenPk = tokenIndexesMap[mangoGroupPk][tokenIndex]; let token = tokens.find((e) => e["mintKey"] === tokenPk); let symbol = token.symbol; let depositIndex = depositIndexes[i]; let borrowIndex = borrowIndexes[i]; cacheIndexes.push({ symbol: symbol, deposit_index: depositIndex, borrow_index: borrowIndex, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }); } return cacheIndexes; } function parseCachePrices( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { let mangoGroupPk = eventData.mangoGroup.toString(); // Some symbols may only exist in tokens or perpMarkets but not both (such as ADA) let tokens = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "tokens" ]; let perpMarkets = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "perpMarkets" ] let symbolDecimalsMap = Object.fromEntries(tokens.map(e => [e.symbol, e.decimals]).concat(perpMarkets.map(e => [e.baseSymbol, e.baseDecimals]))) let oracles = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "oracles" ]; let quoteSymbol = ids["groups"].find((e) => e["publicKey"] === mangoGroupPk)[ "quoteSymbol" ]; let quoteDecimals = ids["groups"] .find((e) => e["publicKey"] === mangoGroupPk) ["tokens"].find((e) => e.symbol === quoteSymbol).decimals; let cachePrices: any = []; let oracleIndexes = eventData.oracleIndexes.map((i) => i.toNumber()); let oraclePrices = eventData.oraclePrices.map((p) => new I80F48(p)); for (let [i, oracleIndex] of oracleIndexes.entries()) { let oraclePk = oracleIndexesMap[mangoGroupPk][oracleIndex]; let oracle = oracles.find((e) => e["publicKey"] === oraclePk); let symbol = oracle.symbol; let baseDecimals = symbolDecimalsMap[symbol] let rawPrice = oraclePrices[i]; let price = rawPrice .mul(I80F48.fromNumber(Math.pow(10, baseDecimals - quoteDecimals))) .toNumber(); cachePrices.push({ symbol: symbol, price: price, oracle_pk: oraclePk, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }); } return cachePrices; } function parseUpdateFunding( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { const config = new Config(IDS); const groupConfig = config.groups.find((g) => g.publicKey.equals(eventData.mangoGroup) ) as GroupConfig; const perpMarketConfig = groupConfig.perpMarkets.find( (p) => p.marketIndex === eventData.marketIndex.toNumber() ) as PerpMarketConfig; return { symbol: perpMarketConfig.baseSymbol, long_funding: new I80F48(eventData.longFunding).toNumber(), short_funding: new I80F48(eventData.shortFunding).toNumber(), instruction_num: instructionNum, mango_group: eventData.mangoGroup.toString(), block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }; } function parseCancelAllPerpOrders ( instructionNum, eventData, signature, blockTime, slot, blockDatetime ) { const config = new Config(IDS); const groupConfig = config.groups.find((g) => g.publicKey.equals(eventData.mangoGroup) ) as GroupConfig; let perpMarketConfig = groupConfig.perpMarkets.find( (p) => p.marketIndex === eventData.marketIndex.toNumber() ) as PerpMarketConfig; let mangoGroupPk = eventData.mangoGroup.toString(); let mangoAccount = eventData.mangoAccount.toString(); let allOrderIds = eventData.allOrderIds.map(e => e.toString()) let cancelledOrderIds = eventData.canceledOrderIds.map(e => e.toString()) let cancelled; let result: any = []; for (let orderId of allOrderIds) { if (cancelledOrderIds.includes(orderId)) { cancelled = true } else { cancelled = false } result.push({ order_id: orderId, cancelled: cancelled, perp_market: perpMarketConfig.name, base_symbol: perpMarketConfig.baseSymbol, mango_account: mangoAccount, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }); } return result; }