2021-10-17 21:50:48 -07:00
|
|
|
const bs58 = require('bs58');
|
|
|
|
import {
|
|
|
|
MangoInstructionLayout,
|
|
|
|
IDS,
|
|
|
|
Config,
|
|
|
|
GroupConfig,
|
|
|
|
PerpMarketConfig,
|
|
|
|
} from '@blockworks-foundation/mango-client';
|
|
|
|
|
|
|
|
// 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',
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
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 === '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 getLatestObjPerCombination(arr, combinationFields) {
|
|
|
|
// Utility function - iterates over arr and return the element with the highest index per set of combinationFields
|
|
|
|
|
|
|
|
let latestCombinations = {};
|
|
|
|
for (let values of arr) {
|
|
|
|
let combination = combinationFields.map((e) => values[e]);
|
|
|
|
latestCombinations[combination] = values;
|
|
|
|
}
|
|
|
|
|
|
|
|
return Object.values(latestCombinations);
|
|
|
|
}
|
|
|
|
|
|
|
|
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 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'],
|
|
|
|
|
2021-10-17 23:21:27 -07:00
|
|
|
// Storing both price and quantity in UI terms to be consistent with db
|
|
|
|
price: eventDetails['price'] / perpMarket.quoteLotSize,
|
|
|
|
quantity: eventDetails['quantity'] * perpMarket.baseLotSize / Math.pow(10, perpMarket.quoteDecimals),
|
2021-10-17 21:50:48 -07:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
},
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
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 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
|
|
|
|
}
|
|
|
|
|