const bs58 = require('bs58'); import { MangoInstructionLayout, IDS, Config, GroupConfig, PerpMarketConfig, orderTypeLayout, } from '@blockworks-foundation/mango-client'; 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', 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', }, '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': { 'BTC-PERP': { 'baseLotSize': 100, 'quoteLotSize': 10 }, 'SOL-PERP': { 'baseLotSize': 10000000, 'quoteLotSize': 100 } } } var ids = IDS; export function jsonParser(parsedTransactions, result, instructions, signature, blockTime, slot, blockDatetime) { // Go through all instructions and get the update funding stuff first parsedTransactions.funding.push( ...extractUpdateFundings( result, instructions, signature, blockTime, slot, blockDatetime, ) ); parsedTransactions.net_balances.push( ...extractNetBalances( result.transaction.message.accountKeys, result.meta.logMessages, signature, blockTime, slot, blockDatetime, ) ); parsedTransactions.settle_fees.push( ...extractSettleFees( result.transaction.message.accountKeys, result.meta.logMessages, signature, blockTime, slot, blockDatetime, ) ); parsedTransactions.settle_pnl.push( ...extractSettlePnl( result.transaction.message.accountKeys, result.meta.logMessages, signature, blockTime, slot, blockDatetime, ) ); // Can have multiple inserts per signature so add instructionNum column to allow a primary key for (let instruction of instructions) { const instructionName = instruction.instructionName; const instructionNum = instruction.instructionNum; if (instructionName === 'CachePrices') { parsedTransactions.cache_prices.push( ...parseCachePrices( instructionNum, result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'CacheRootBanks') { parsedTransactions.cache_indexes.push( ...parseCacheRootBanks( instructionNum, result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'Deposit') { parsedTransactions.deposits_withdraws.push( ...parseDepositWithDraw( instruction, instructionNum, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'Withdraw') { parsedTransactions.deposits_withdraws.push( ...parseDepositWithDraw( instruction, instructionNum, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'LiquidateTokenAndToken') { parsedTransactions.liquidate_token_and_token.push( ...parseLiquidateTokenAndToken( result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'LiquidateTokenAndPerp') { parsedTransactions.liquidate_token_and_perp.push( ...parseLiquidateTokenAndPerp( result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'LiquidatePerpMarket') { parsedTransactions.liquidate_perp_market.push( ...parseLiquidatePerpMarket( result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'ResolveTokenBankruptcy') { parsedTransactions.token_bankruptcy.push( ...parseResolveTokenBankruptcy( result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'ResolvePerpBankruptcy') { parsedTransactions.perp_bankruptcy.push( ...parseResolvePerpBankruptcy( result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'ConsumeEvents') { // if (instructionName === 'ConsumeEvents') { parsedTransactions.fill_events.push( ...parseConsumeEvents( result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); // } } else if (instructionName === 'RedeemMngo') { parsedTransactions.redeem_mngo.push( ...parseRedeemMngo( instruction, result.meta.innerInstructions, instructionNum, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'ForceSettleQuotePositions') { parsedTransactions.force_settle_quote_positions.push( ...parseForceSettleQuotePositions( result.meta.logMessages, instruction.accounts, signature, blockTime, slot, blockDatetime, ), ); } else if (instructionName === 'UpdateFunding') { // read through the program logs and filter for the one that starts with open curly bracket // TODO what about the case where it's part of a CPI? } } } function extractSettleFees( allAccounts, logMessages, signature, blockTime, slot, blockDatetime, ) { // Need to know the mango group pk of the transaction for information not tied to instructions // Transaction will only have one mango group - so check which one it is by iterating over mango group pks in IDS // TODO: add mango group to appropriate log messages and remove this workaround let ids = IDS; console.log(); let mangoGroupPk; let accountKeys = allAccounts.map( (e) => e.pubkey, ); for (let pk of ids.groups.map((e) => e.publicKey)) { if (accountKeys.includes(pk)) { mangoGroupPk = pk; break; } } let startDetailsStr = 'Program log: settle_fees details: ' const filteredLogs = logMessages.filter((line) => line.startsWith(startDetailsStr), ); let instructionNum = 1; let out: any = [] for (let log of filteredLogs) { let perpMarkets = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'perpMarkets' ]; // Log JSON is missing quotes around mango accounts // Also trailing comma at end of json // TODO: fix this let settlePnlDetails; try { settlePnlDetails = JSON.parse(log.slice(startDetailsStr.length)); } catch { let jsonString = log.slice(startDetailsStr.length); jsonString = insertQuotesAroundField(jsonString, 'mango_account'); jsonString = jsonString.replace(', }', ' }'); settlePnlDetails = JSON.parse(jsonString); } let mangoAccount = settlePnlDetails['mango_account']; let marketIndex = settlePnlDetails['market_index']; let perpMarket = perpMarkets.find( (e) => e['marketIndex'] === marketIndex, ); let perpMarketName = perpMarket.name; let settlement = settlePnlDetails['settlement'] / Math.pow(10, perpMarket.quoteDecimals); out.push(...[ { margin_account: mangoAccount, settlement: settlement, perp_market_name: perpMarketName, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]) instructionNum++; } return out } function extractSettlePnl( allAccounts, logMessages, signature, blockTime, slot, blockDatetime, ) { // Need to know the mango group pk of the transaction for information not tied to instructions // Transaction will only have one mango group - so check which one it is by iterating over mango group pks in IDS // TODO: add mango group to appropriate log messages and remove this workaround let ids = IDS; console.log(); let mangoGroupPk; let accountKeys = allAccounts.map( (e) => e.pubkey, ); for (let pk of ids.groups.map((e) => e.publicKey)) { if (accountKeys.includes(pk)) { mangoGroupPk = pk; break; } } let startDetailsStr = 'Program log: settle_pnl details: ' const filteredLogs = logMessages.filter((line) => line.startsWith(startDetailsStr), ); let instructionNum = 1; let out: any = [] for (let log of filteredLogs) { let perpMarkets = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'perpMarkets' ]; // Log JSON is missing quotes around mango accounts // TODO: fix this let settlePnlDetails; try { settlePnlDetails = JSON.parse(log.slice(startDetailsStr.length)); } catch { let jsonString = log.slice(startDetailsStr.length); jsonString = insertQuotesAroundField(jsonString, 'mango_account_a'); jsonString = insertQuotesAroundField(jsonString, 'mango_account_b'); settlePnlDetails = JSON.parse(jsonString); } let mangoAccountA = settlePnlDetails['mango_account_a']; let mangoAccountB = settlePnlDetails['mango_account_b']; let marketIndex = settlePnlDetails['market_index']; let perpMarket = perpMarkets.find( (e) => e['marketIndex'] === marketIndex, ); let perpMarketName = perpMarket.name; let settlement = settlePnlDetails['settlement']; // 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); out.push(...[ { margin_account: mangoAccountA, settlement: settlementA, perp_market_name: perpMarketName, 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, counterparty: mangoAccountA, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]); instructionNum++; } return out } function insertQuotesAroundField(jsonString, field) { // Utility function to fix malformed json (json with quotes around strings) // Assumes fields have a single space before the key let firstQuotePosition = jsonString.search('"' + field + '": ') + ('"' + field + '": ').length; let secondQuotePosition = firstQuotePosition + jsonString.slice(firstQuotePosition).search(','); return [ jsonString.slice(0, firstQuotePosition), '"', jsonString.slice(firstQuotePosition, secondQuotePosition), '"', jsonString.slice(secondQuotePosition), ].join(''); } function getJsonStringsFromArray(logMessages, jsonStartStr) { let jsonStrings: any = []; for (let logMessage of logMessages) { if (logMessage.startsWith(jsonStartStr)) { jsonStrings.push(logMessage.slice(jsonStartStr.length)); } } return jsonStrings; } function parseConsumeEvents( logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { // instructionNum is used here to form a primary key on the db table (with signature) let mangoGroupPk = accounts[0]; let perpMarkets = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'perpMarkets' ]; let perpMarketPk = accounts[2]; let perpMarket = perpMarkets.find( (e) => e['publicKey'] === perpMarketPk, ); let lotSizes = perpLotSizes[mangoGroupPk][perpMarket.name] let events: any = []; let startDetailsStr = 'Program log: FillEvent details: '; let eventNum = 1; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let eventDetails; try { eventDetails = JSON.parse(logMessage.slice(startDetailsStr.length)); } catch { let jsonString = logMessage.slice(startDetailsStr.length); jsonString = insertQuotesAroundField(jsonString, 'maker'); jsonString = insertQuotesAroundField(jsonString, 'taker'); jsonString = insertQuotesAroundField(jsonString, 'taker_side'); jsonString = insertQuotesAroundField(jsonString, 'maker_order_id'); jsonString = insertQuotesAroundField(jsonString, 'taker_order_id'); eventDetails = JSON.parse(jsonString); } events.push({ event_num: eventNum, maker: eventDetails['maker'], maker_fee: eventDetails['maker_fee'], maker_order_id: eventDetails['maker_order_id'], // Storing both price and quantity in UI terms to be consistent with db price: eventDetails['price'] / lotSizes.quoteLotSize, quantity: eventDetails['quantity'] * lotSizes.baseLotSize / Math.pow(10, perpMarket.baseDecimals), seq_num: eventDetails['seq_num'], taker: eventDetails['taker'], taker_fee: eventDetails['taker_fee'], taker_order_id: eventDetails['taker_order_id'], taker_side: eventDetails['taker_side'], mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, perp_market: perpMarket.name, base_symbol: perpMarket.baseSymbol }); eventNum++; } } return events; } function parseResolvePerpBankruptcy( logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { let mangoGroupPk = accounts[0]; let liqee = accounts[2]; let liqor = accounts[3]; 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; // 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 perpMarketName; let insuranceFundTransfer: number | null = null; let loss: number | null = null; // TODO: validate this when an example comes along let startDetailsStr = 'Program log: perp_bankruptcy details: '; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let bankruptcyDetails = JSON.parse( logMessage.slice(startDetailsStr.length), ); let perpMarket = perpMarkets.find( (e) => e['marketIndex'] === bankruptcyDetails['liab_index'], ); perpMarketName = perpMarket.name; insuranceFundTransfer = bankruptcyDetails['insurance_transfer'] / Math.pow(10, quoteDecimals); } } startDetailsStr = 'Program log: perp_socialized_loss details: '; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let socializedLossDetails = JSON.parse( logMessage.slice(startDetailsStr.length), ); let perpMarket = perpMarkets.find( (e) => e['marketIndex'] === socializedLossDetails['liab_index'], ); perpMarketName = perpMarket.name; // loss is on quote position loss = socializedLossDetails['socialized_loss'] / Math.pow(10, quoteDecimals); } } return [ { liqor: liqor, liqee: liqee, perp_market_name: perpMarketName, insurance_fund_transfer: insuranceFundTransfer, loss: loss, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]; } function parseResolveTokenBankruptcy( logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { let mangoGroupPk = accounts[0]; let liqee = accounts[2]; let liqor = accounts[3]; 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 startDetailsStr = 'Program log: token_bankruptcy details: '; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let bankruptcyDetails = JSON.parse( logMessage.slice(startDetailsStr.length), ); let tokenIndex = bankruptcyDetails['liab_index']; let tokenPk = tokenIndexesMap[mangoGroupPk][tokenIndex]; let token = tokens.find((e) => e['mintKey'] === tokenPk); symbol = token.symbol; insuranceFundTransfer = bankruptcyDetails['insurance_transfer'] / Math.pow(10, quoteDecimals); } } startDetailsStr = 'Program log: token_socialized_loss details: '; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let socializedLossDetails = JSON.parse( logMessage.slice(startDetailsStr.length), ); let tokenIndex = socializedLossDetails['liab_index']; let tokenPk = tokenIndexesMap[mangoGroupPk][tokenIndex]; let token = tokens.find((e) => e['mintKey'] === tokenPk); symbol = token.symbol; loss = socializedLossDetails['native_loss'] / Math.pow(10, token.decimals); percentageLoss = socializedLossDetails['percentage_loss']; // Deposit index was added to the logging after launch // TODO: remove when we've parsed the logs without deposit_index // TODO: does this need to be parsed from native units? try { depositIndex = socializedLossDetails['deposit_index']; } catch { // pass } } } 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( logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { if (logMessages.includes('Program log: Account init_health above zero.')) { return []; } let mangoGroupPk = accounts[0]; let liqee = accounts[4]; let liqor = accounts[5]; let perpMarkets = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'perpMarkets' ]; let quoteSymbol = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'quoteSymbol' ]; let perpMarketName; let liabSymbol; let assetSymbol; let baseTransfer; let quoteTransfer; let bankruptcy; let startDetailsStr = 'Program log: liquidate_perp_market details: '; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let liquidationDetails = JSON.parse( logMessage.slice(startDetailsStr.length), ); let perpMarket = perpMarkets.find( (e) => e['marketIndex'] === liquidationDetails['market_index'], ); perpMarketName = perpMarket.name; let liabDecimals = perpMarket.baseDecimals; let assetDecimals = perpMarket.quoteDecimals; liabSymbol = perpMarket.baseSymbol; assetSymbol = quoteSymbol; baseTransfer = liquidationDetails['base_transfer'] / Math.pow(10, liabDecimals); // TODO: quoteTransfer is -base_transfer * pmi.base_lot_size - but I don't really know what this means quoteTransfer = liquidationDetails['quote_transfer'] / Math.pow(10, assetDecimals); bankruptcy = liquidationDetails['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( logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { if (logMessages.includes('Program log: Account init_health above zero.')) { return []; } let mangoGroupPk = accounts[0]; let liqee = accounts[2]; let liqor = accounts[3]; 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 assetSymbol; let liabSymbol; let assetType; let liabType; let assetTransfer; let assetPrice; let liabTransfer; let liabPrice; let startDetailsStr = 'Program log: liquidate_token_and_perp details: '; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let liquidationDetails = JSON.parse( logMessage.slice(startDetailsStr.length), ); assetType = liquidationDetails['asset_type']; let assetIndex = liquidationDetails['asset_index']; liabType = liquidationDetails['liab_type']; let liabIndex = liquidationDetails['liab_index']; let assetDecimals; let liabDecimals; if (assetType === 'Token') { // asset is token and liab is perp let assetTokenPk = tokenIndexesMap[mangoGroupPk][assetIndex]; let assetToken = tokens.find((e) => e['mintKey'] === assetTokenPk); 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; } 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; let liabTokenPk = tokenIndexesMap[mangoGroupPk][liabIndex]; let liabToken = tokens.find((e) => e['mintKey'] === liabTokenPk); liabSymbol = liabToken.symbol; liabDecimals = liabToken.decimals; } assetTransfer = liquidationDetails['asset_transfer'] / Math.pow(10, assetDecimals); assetPrice = liquidationDetails['asset_price'] * Math.pow(10, assetDecimals - quoteDecimals); liabTransfer = liquidationDetails['actual_liab_transfer'] / Math.pow(10, liabDecimals); liabPrice = liquidationDetails['liab_price'] * Math.pow(10, liabDecimals - quoteDecimals); } } 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, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]; } function parseLiquidateTokenAndToken( logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { if (logMessages.includes('Program log: Account init_health above zero.')) { return []; } let mangoGroup = accounts[0]; let liqee = accounts[2]; let liqor = accounts[3]; let assetRootPk = accounts[5]; let liabRootPk = accounts[7]; let assetToken = ids['groups'] .find((e) => e['publicKey'] === mangoGroup) ['tokens'].find((e) => e.rootKey === assetRootPk); let liabToken = ids['groups'] .find((e) => e['publicKey'] === mangoGroup) ['tokens'].find((e) => e.rootKey === liabRootPk); 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; let liabPrice; let assetTransfer; let liabTransfer; let bankruptcy; let startDetailsStr = 'Program log: liquidate_token_and_token details: '; for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let liquidationDetails = JSON.parse( logMessage.slice(startDetailsStr.length), ); assetPrice = liquidationDetails['asset_price'] * Math.pow(10, assetToken.decimals - quoteDecimals); liabPrice = liquidationDetails['liab_price'] * Math.pow(10, liabToken.decimals - quoteDecimals); assetTransfer = liquidationDetails['asset_transfer'] / Math.pow(10, assetToken.decimals); liabTransfer = liquidationDetails['liab_transfer'] / Math.pow(10, liabToken.decimals); bankruptcy = liquidationDetails['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( instruction, innerInstructions, instructionNum, signature, blockTime, slot, blockDatetime, ) { let mangoGroup = instruction.accounts[0]; let marginAccount = instruction.accounts[2]; let decimals = ids['groups'] .find((e) => e['publicKey'] === mangoGroup) ['tokens'].find((e) => e.symbol === 'MNGO').decimals; let perpMarket = ids['groups'] .find((e) => e['publicKey'] === mangoGroup) ['perpMarkets'].find(e => e.publicKey === instruction.accounts[4]) let perpMarketName = perpMarket.name let baseSymbol = perpMarket.baseSymbol // TODO: This would be simpler to just parse logs let transferInstruction = innerInstructions.find( (e) => e.index === instructionNum - 1, ).instructions[0]; let quantity = parseInt(transferInstruction.parsed.info.amount) / 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( instruction, instructionNum, signature, blockTime, slot, blockDatetime, ) { let decodedInstruction = MangoInstructionLayout.decode( bs58.decode(instruction.data), 0 ); let instructionName = Object.keys(decodedInstruction)[0]; let mangoGroup = instruction.accounts[0]; let marginAccount = instruction.accounts[1]; let owner = instruction.accounts[2]; let rootPk = instruction.accounts[4]; let token = ids['groups'] .find((e) => e['publicKey'] === mangoGroup) ['tokens'].find((e) => e.rootKey === rootPk); let mintDecimals = token.decimals; let symbol = token.symbol; let quantity = decodedInstruction[instructionName].quantity.toNumber() / Math.pow(10, mintDecimals); return [ { margin_account: marginAccount, owner: owner, symbol: symbol, side: instructionName, quantity: quantity, instruction_num: instructionNum, mango_group: mangoGroup, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]; } function parseNetAmounts( logMessage, mangoGroupPk, signature, blockTime, slot, blockDatetime, ) { 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 startDetailsStr; if (logMessage.startsWith('Program log: checked_sub_net details: ')) { startDetailsStr = 'Program log: checked_sub_net details: '; } else if (logMessage.startsWith('Program log: checked_add_net details: ')) { startDetailsStr = 'Program log: checked_add_net details: '; } else { throw 'Unexpected startDetailsStr'; } // TODO: fix this in the rust code and remove this workaround // The json is missing quotes around the mango_account_pk let netAmountDetails; try { netAmountDetails = JSON.parse(logMessage.slice(startDetailsStr.length)); } catch { let jsonString = logMessage.slice(startDetailsStr.length); jsonString = insertQuotesAroundField(jsonString, 'mango_account_pk'); netAmountDetails = JSON.parse(jsonString); } let mangoAccountPk = netAmountDetails['mango_account_pk']; let tokenIndex = netAmountDetails['token_index']; let tokenPk = tokenIndexesMap[mangoGroupPk][tokenIndex]; let token = tokens.find((e) => e['mintKey'] === tokenPk); let symbol = token.symbol; let deposit = netAmountDetails['deposit'] / Math.pow(10, token.decimals - quoteDecimals); let borrow = netAmountDetails['borrow'] / Math.pow(10, token.decimals - quoteDecimals); 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, logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { let mangoGroupPk = accounts[0]; let tokens = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'tokens' ]; let cacheIndexes: any = []; let startDetailsStr = 'Program log: cache_root_banks details: '; let jsons = getJsonStringsFromArray(logMessages, startDetailsStr); // Nothing cached if (jsons.length === 0) { return []; } // TODO: fix this in the rust code and remove this workaround // The json is missing a comma before "borrow_indexes" let cacheIndexDetails; try { cacheIndexDetails = JSON.parse(jsons[0]); } catch { let jsonString = jsons[0]; let insertPosition = jsonString.search('"borrow_indexes"'); let fixedJsonString = [ jsonString.slice(0, insertPosition), ',', jsonString.slice(insertPosition), ].join(''); cacheIndexDetails = JSON.parse(fixedJsonString); } let tokenIndexes = cacheIndexDetails['token_indexes']; let depositIndexes = cacheIndexDetails['deposit_indexes']; let borrowIndexes = cacheIndexDetails['borrow_indexes']; 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, logMessages, accounts, signature, blockTime, slot, blockDatetime, ) { let mangoGroupPk = accounts[0]; let tokens = ids['groups'].find((e) => e['publicKey'] === mangoGroupPk)[ 'tokens' ]; 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 startDetailsStr = 'Program log: cache_prices details: '; let jsons = getJsonStringsFromArray(logMessages, startDetailsStr); let cachePriceDetails = JSON.parse(jsons[0]); let oracleIndexes = cachePriceDetails['oracle_indexes']; let oraclePrices = cachePriceDetails['oracle_prices']; 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 token = tokens.find((e) => e.symbol === symbol); let baseDecimals = token.decimals; let rawPrice = oraclePrices[i]; let price = (rawPrice * Math.pow(10, baseDecimals)) / Math.pow(10, quoteDecimals); 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 extractNetBalances( allAccounts, logMessages, signature, blockTime, slot, blockDatetime, ) { // Need to know the mango group pk of the transaction for information not tied to instructions // Transaction will only have one mango group - so check which one it is by iterating over mango group pks in IDS // TODO: add mango group to appropriate log messages and remove this workaround let ids = IDS; console.log(); let mangoGroupPk; let accountKeys = allAccounts.map( (e) => e.pubkey, ); for (let pk of ids.groups.map((e) => e.publicKey)) { if (accountKeys.includes(pk)) { mangoGroupPk = pk; break; } } let allNetBalances: any = []; // Some information is not tied to instructions specifically for (let logMessage of logMessages) { if ( logMessage.startsWith('Program log: checked_sub_net details: ') || logMessage.startsWith('Program log: checked_add_net details: ') ) { let parsedNetAmounts = parseNetAmounts( logMessage, mangoGroupPk, signature, blockTime, slot, blockDatetime, ); allNetBalances.push(...parsedNetAmounts); } } // Only want to store the latest deposit/borrow amounts per marginAccount/symbol pair for each instruction return getLatestObjPerCombination(allNetBalances, ['mango_account','symbol']) } function parseUpdateFunding( instructionNum: number, logMessage: string, accounts: string[], signature, blockTime, slot, blockDatetime, ) { const config = new Config(IDS); const mangoGroupPk = accounts[0]; const perpMarketPk = accounts[2]; const groupConfig = config.groups.find( (g) => g.publicKey.toString() === mangoGroupPk, ) as GroupConfig; const perpMarketConfig = groupConfig.perpMarkets.find( (p) => p.publicKey.toString() === perpMarketPk, ) as PerpMarketConfig; const parsed = JSON.parse(logMessage.slice("Program log: ".length)); if ('long_funding' in parsed && 'short_funding' in parsed) { return [ { symbol: perpMarketConfig.baseSymbol, long_funding: parsed.long_funding, short_funding: parsed.short_funding, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, }, ]; } else { return []; } } function parseForceSettleQuotePositions( logMessages: string, accounts: string[], signature, blockTime, slot, blockDatetime, ) { const config = new Config(IDS); const mangoGroupPk = accounts[0]; const groupConfig = config.groups.find( (g) => g.publicKey.toString() === mangoGroupPk, ) as GroupConfig; let liqee = accounts[2] let liqor = accounts[3] let startDetailsStr = 'Program log: force_settle_quote_positions details: '; let instructionNum = 1 let out: any = [] for (let logMessage of logMessages) { if (logMessage.startsWith(startDetailsStr)) { let forceSettleQuoteDetails; try { forceSettleQuoteDetails = JSON.parse(logMessage.slice(startDetailsStr.length)); } catch { let jsonString = logMessage.slice(startDetailsStr.length); jsonString = insertQuotesAroundField(jsonString, 'liqee'); jsonString = insertQuotesAroundField(jsonString, 'liqor'); jsonString = jsonString.replace(', }', ' }'); forceSettleQuoteDetails = JSON.parse(jsonString); } for (let [marketIndex, settlement] of forceSettleQuoteDetails['settlements'].entries()) { let perpMarketConfig = groupConfig.perpMarkets.find( (p) => p.marketIndex === marketIndex, ) as PerpMarketConfig; if (settlement !== 0) { console.log(perpMarketConfig.name) console.log(perpMarketConfig.baseSymbol) let settlementUi = settlement / Math.pow(10, perpMarketConfig.quoteDecimals) // console.log(perpMarketConfig.quoteDecimals) out.push(...[ { liqee: liqee, liqor: liqor, settlement: settlementUi, perp_market_name: perpMarketConfig.name, base_symbol: perpMarketConfig.baseSymbol, instruction_num: instructionNum, mango_group: mangoGroupPk, block_datetime: blockDatetime, slot: slot, signature: signature, blocktime: blockTime, } ]) instructionNum++ } } } } return out } function extractUpdateFundings( result, instructions, signature, blockTime, slot, blockDatetime, ) { const fundingLogs = result.meta.logMessages.filter((line) => line.startsWith('Program log: {"long_funding":'), ); const fundingInstructions = instructions.filter((ix) => { return ix.instructionName === 'UpdateFunding'; }); if (fundingLogs.length !== fundingInstructions.length) { throw new Error("funding logs don't match length of funding instructions"); } let out: any = [] for (let i = 0; i < fundingLogs.length; i++) { const ix = fundingInstructions[i]; const logMessage = fundingLogs[i]; out.push( ...parseUpdateFunding( ix.instructionNum, logMessage, ix.accounts, signature, blockTime, slot, blockDatetime, ), ); } return out }