import { EvmBlock, EvmTransaction, ReceiptTransaction } from "../../entities"; import { EvmBlockRepository } from "../../repositories"; import { GetEvmOpts } from "./GetEvmLogs"; import winston from "winston"; export class GetEvmTransactions { private readonly blockRepo: EvmBlockRepository; protected readonly logger: winston.Logger; constructor(blockRepo: EvmBlockRepository) { this.logger = winston.child({ module: "GetEvmTransactions" }); this.blockRepo = blockRepo; } async execute(range: Range, opts: GetEvmOpts): Promise { const fromBlock = range.fromBlock; const toBlock = range.toBlock; const chain = opts.chain; if (fromBlock > toBlock) { this.logger.info( `[${chain}][exec] Invalid range [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` ); return []; } let populatedTransactions: EvmTransaction[] = []; const isTransactionsPresent = true; this.logger.info( `[${chain}][exec] Processing blocks [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` ); let currentBlock = fromBlock; const batchSize = 9; while (currentBlock <= toBlock) { const batchPromises = []; for (let i = 0; i < batchSize && currentBlock <= toBlock; i++, currentBlock++) { // Push each getBlock call as a promise into the batchPromises array batchPromises.push(this.blockRepo.getBlock(chain, currentBlock, isTransactionsPresent)); } const results = await Promise.allSettled(batchPromises); results.forEach(async (result, index) => { if (result.status === "fulfilled") { const evmBlock = result.value; const transactions = evmBlock.transactions ?? []; // Only process transactions to the contract address configured const transactionsByAddressConfigured = transactions.filter( (transaction) => opts.addresses?.includes(String(transaction.to).toLowerCase()) || opts.addresses?.includes(String(transaction.from).toLowerCase()) ); if (transactionsByAddressConfigured.length > 0) { const hashNumbers = new Set( transactionsByAddressConfigured.map((transaction) => transaction.hash) ); const receiptTransactions = await this.blockRepo.getTransactionReceipt( chain, hashNumbers ); const filterTransactions = this.filterTransactions( opts, transactionsByAddressConfigured, receiptTransactions ); await this.populateTransaction( opts, evmBlock, receiptTransactions, filterTransactions, populatedTransactions ); } } else if (result.status === "rejected") { this.logger.warn( `[${chain}][exec] Invalid range [fromBlock: ${fromBlock} - toBlock: ${toBlock}]` ); } }); } this.logger.info( `[${chain}][exec] Got ${ populatedTransactions?.length } transactions to process for ${this.populateLog(opts, fromBlock, toBlock)}` ); return populatedTransactions; } /** * This method filter the transactions in base your logs with the topic and address configured in the job * For example: Redeemed or MintAndWithdraw transactions */ private filterTransactions( opts: GetEvmOpts, transactionsByAddressConfigured: EvmTransaction[], receiptTransactions: Record ): EvmTransaction[] { return transactionsByAddressConfigured.filter((transaction) => { const optsTopics = opts.topics; const logs = receiptTransactions[transaction.hash]?.logs || []; return logs.some((log) => { return optsTopics?.find((topic) => log.topics?.includes(topic)); }); }); } private async populateTransaction( opts: GetEvmOpts, evmBlock: EvmBlock, receiptTransactions: Record, filterTransactions: EvmTransaction[], populatedTransactions: EvmTransaction[] ) { filterTransactions.forEach((transaction) => { transaction.status = receiptTransactions[transaction.hash].status; transaction.timestamp = evmBlock.timestamp; transaction.environment = opts.environment; transaction.chainId = opts.chainId; transaction.chain = opts.chain; transaction.logs = receiptTransactions[transaction.hash].logs; populatedTransactions.push(transaction); }); } private populateLog(opts: GetEvmOpts, fromBlock: bigint, toBlock: bigint): string { return `[addresses:${opts.addresses}][topics:${opts.topics}][blocks:${fromBlock} - ${toBlock}]`; } } type Range = { fromBlock: bigint; toBlock: bigint; };