mango-transaction-scraper-v3/src/jsonParsers.ts

1510 lines
46 KiB
TypeScript

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
}