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

1389 lines
42 KiB
TypeScript

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