174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
|
import {
|
||
|
getMultipleAccounts,
|
||
|
IDS,
|
||
|
PerpMarket,
|
||
|
PerpMarketLayout,
|
||
|
ZERO_BN,
|
||
|
} from '@blockworks-foundation/mango-client';
|
||
|
import BN from 'bn.js';
|
||
|
import { Connection as DbConnection } from 'typeorm';
|
||
|
import { Connection, PublicKey } from '@solana/web3.js';
|
||
|
import { wait } from './utils';
|
||
|
import { PerpEvent, PerpLiquidationEvent, PerpSequenceNumber } from './entity';
|
||
|
|
||
|
export async function getAllPerpMarkets(connection: Connection) {
|
||
|
const DEFAULT_MANGO_GROUP_NAME = process.env.GROUP || 'mainnet.1';
|
||
|
const defaultMangoGroupIds = IDS['groups'].find(
|
||
|
(group) => group.name === DEFAULT_MANGO_GROUP_NAME
|
||
|
);
|
||
|
const perpMarketInfos = defaultMangoGroupIds.perpMarkets;
|
||
|
const perpMarketPks = perpMarketInfos.map((mkt) => new PublicKey(mkt.publicKey));
|
||
|
const allMarketAccountInfos = await getMultipleAccounts(connection, perpMarketPks);
|
||
|
|
||
|
return perpMarketInfos.map((perpInfo, i) => {
|
||
|
const decoded = PerpMarketLayout.decode(allMarketAccountInfos[i].accountInfo.data);
|
||
|
return new PerpMarket(
|
||
|
new PublicKey(perpInfo.publicKey),
|
||
|
perpInfo.baseDecimals,
|
||
|
perpInfo.quoteDecimals,
|
||
|
decoded
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function formatFillEvents(address, fillEvents) {
|
||
|
return fillEvents.map((fillEvent) => {
|
||
|
return {
|
||
|
loadTimestamp: new Date(fillEvent.timestamp.toNumber() * 1000).toISOString(),
|
||
|
address,
|
||
|
takerSide: fillEvent.takerSide,
|
||
|
seqNum: fillEvent.seqNum.toNumber(),
|
||
|
makerFee: fillEvent.makerFee.toNumber(),
|
||
|
takerFee: fillEvent.takerFee.toNumber(),
|
||
|
maker: fillEvent.maker.toString(),
|
||
|
makerOrderId: fillEvent.makerOrderId.toString(),
|
||
|
taker: fillEvent.taker.toString(),
|
||
|
takerOrderId: fillEvent.takerOrderId.toString(),
|
||
|
price: fillEvent.price,
|
||
|
quantity: fillEvent.quantity,
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function formatLiquidateEvent(address, liquidationEvents) {
|
||
|
return liquidationEvents.map((event) => {
|
||
|
return {
|
||
|
seqNum: event.seqNum.toNumber(),
|
||
|
loadTimestamp: new Date(event.timestamp.toNumber() * 1000).toISOString(),
|
||
|
address,
|
||
|
liqee: event.liqee.toString(),
|
||
|
liqor: event.liqor.toString(),
|
||
|
price: event.price,
|
||
|
quantity: event.quantity,
|
||
|
liquidationFee: event.liquidationFee.toNumber(),
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const getLastSeqNumForMarket = async (address, db) => {
|
||
|
try {
|
||
|
return await db
|
||
|
.getRepository(PerpSequenceNumber)
|
||
|
.createQueryBuilder('perp_sequence_number')
|
||
|
.where('perp_sequence_number.address = :address', { address })
|
||
|
.addOrderBy('perp_sequence_number.seqNum', 'DESC')
|
||
|
.select('"seqNum"')
|
||
|
.limit(1)
|
||
|
.execute();
|
||
|
} catch (error) {
|
||
|
console.error('Unable to fetch perp_sequence_number', error);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
async function insertPerpEvents(perpMarketPk, allEvents, fillEvents, liquidateEvents, db) {
|
||
|
if (allEvents.length === 0) return;
|
||
|
|
||
|
if (fillEvents.length) {
|
||
|
const newRecords = formatFillEvents(perpMarketPk, fillEvents);
|
||
|
await db.getRepository(PerpEvent).createQueryBuilder().insert().values(newRecords).execute();
|
||
|
}
|
||
|
|
||
|
if (liquidateEvents.length) {
|
||
|
const newRecords = formatLiquidateEvent(perpMarketPk, liquidateEvents);
|
||
|
|
||
|
await db
|
||
|
.getRepository(PerpLiquidationEvent)
|
||
|
.createQueryBuilder()
|
||
|
.insert()
|
||
|
.values(newRecords)
|
||
|
.execute();
|
||
|
}
|
||
|
|
||
|
const sequenceNumbers = allEvents
|
||
|
.map((e) => e.fill || e.liquidate || e.out)
|
||
|
.map((event) => ({
|
||
|
address: perpMarketPk,
|
||
|
seqNum: event.seqNum.toNumber(),
|
||
|
loadTimestamp: new Date().toISOString(),
|
||
|
}));
|
||
|
|
||
|
await db
|
||
|
.getRepository(PerpSequenceNumber)
|
||
|
.createQueryBuilder()
|
||
|
.insert()
|
||
|
.values(sequenceNumbers)
|
||
|
.execute();
|
||
|
}
|
||
|
|
||
|
async function captureEventsForPerpMarket(
|
||
|
db: DbConnection,
|
||
|
connection: Connection,
|
||
|
perpMarket: PerpMarket
|
||
|
) {
|
||
|
let lastSeqNum;
|
||
|
const perpMarketPk = perpMarket.publicKey.toString();
|
||
|
const lastSeqNumRecord = await getLastSeqNumForMarket(perpMarketPk, db);
|
||
|
if (lastSeqNumRecord.length) {
|
||
|
lastSeqNum = new BN(lastSeqNumRecord[0]?.seqNum);
|
||
|
}
|
||
|
const eventQueue = await perpMarket.loadEventQueue(connection);
|
||
|
const allEvents = eventQueue.eventsSince(lastSeqNum);
|
||
|
|
||
|
const fillEvents = allEvents
|
||
|
.map((e) => e.fill)
|
||
|
.filter((e) => !!e)
|
||
|
.map((e) => perpMarket.parseFillEvent(e));
|
||
|
const liquidateEvents = allEvents
|
||
|
.map((e) => e.liquidate)
|
||
|
.filter((e) => !!e)
|
||
|
.map((e) => ({
|
||
|
...e,
|
||
|
price: e.price.toNumber(),
|
||
|
quantity: perpMarket.baseLotsToNumber(e.quantity),
|
||
|
}));
|
||
|
|
||
|
console.log(
|
||
|
`market ${perpMarketPk} lastSeqNum: ${lastSeqNum?.toNumber()}, fill events: ${
|
||
|
fillEvents.length
|
||
|
}, all events: ${allEvents.length}`
|
||
|
);
|
||
|
|
||
|
try {
|
||
|
await insertPerpEvents(perpMarketPk, allEvents, fillEvents, liquidateEvents, db);
|
||
|
} catch (error) {
|
||
|
console.error('Error inserting event queue data:', error);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export const startPerpEventParsing = async (
|
||
|
db: DbConnection,
|
||
|
connection: Connection,
|
||
|
perpMarket,
|
||
|
waitTime
|
||
|
) => {
|
||
|
while (true) {
|
||
|
try {
|
||
|
await captureEventsForPerpMarket(db, connection, perpMarket);
|
||
|
} catch (e) {
|
||
|
console.log(`Error in startPerpEventParsing() ${e}`);
|
||
|
}
|
||
|
|
||
|
await wait(waitTime);
|
||
|
}
|
||
|
};
|