diff --git a/packages/indexer-database/src/utils/BlockchainEventRepository.ts b/packages/indexer-database/src/utils/BlockchainEventRepository.ts index b1c4332f..e9a40ea5 100644 --- a/packages/indexer-database/src/utils/BlockchainEventRepository.ts +++ b/packages/indexer-database/src/utils/BlockchainEventRepository.ts @@ -27,7 +27,7 @@ export class BlockchainEventRepository { * The unique keys to check for. It is recommended these keys to be indexed columns, so that the query is faster. * @param comparisonKeys - The keys to compare for changes. */ - protected async saveAndHandleFinalisationBatch( + public async saveAndHandleFinalisationBatch( entity: EntityTarget, data: Partial[], uniqueKeys: (keyof Entity)[], diff --git a/packages/indexer/src/data-indexing/adapter/oft/model.ts b/packages/indexer/src/data-indexing/adapter/oft/model.ts index 945b6c1e..38e8742e 100644 --- a/packages/indexer/src/data-indexing/adapter/oft/model.ts +++ b/packages/indexer/src/data-indexing/adapter/oft/model.ts @@ -32,3 +32,12 @@ export interface SponsoredOFTSendLog extends ethers.providers.Log { sig: string; }; } + +export interface ComposeDeliveredEvent extends ethers.Event { + args: [] & { + from: string; + to: string; + guid: string; + index: number; + }; +} diff --git a/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts b/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts index 5428f715..5752b705 100644 --- a/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts +++ b/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts @@ -1,5 +1,4 @@ import { BigNumber, providers } from "ethers"; -import { CHAIN_IDs } from "@across-protocol/constants"; export interface SimpleTransferFlowCompletedLog extends providers.Log { args: { diff --git a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts index 27e978e1..90319613 100644 --- a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts @@ -39,6 +39,10 @@ import { isHypercoreWithdraw, } from "../adapter/cctp-v2/service"; import { entities, SaveQueryResult } from "@repo/indexer-database"; +import { + formatAndSaveSimpleTransferFlowCompletedEvents, + getSimpleTransferFlowCompletedEventsFromTransactionReceipts, +} from "./hyperEvmExecutor"; export type EvmBurnEventsPair = { depositForBurn: DepositForBurnEvent; @@ -286,7 +290,7 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { []; if (sponsoredCCTPDstPeripheryAddress) { simpleTransferFlowCompletedEvents = - this.getSimpleTransferFlowCompletedEventsFromTransactionReceipts( + getSimpleTransferFlowCompletedEventsFromTransactionReceipts( filteredMessageReceivedTxReceipts, sponsoredCCTPDstPeripheryAddress, ); @@ -508,28 +512,6 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { return events; } - private getSimpleTransferFlowCompletedEventsFromTransactionReceipts( - transactionReceipts: Record, - hyperEvmExecutorAddress: string, - ) { - const events: SimpleTransferFlowCompletedLog[] = []; - for (const txHash of Object.keys(transactionReceipts)) { - const transactionReceipt = transactionReceipts[ - txHash - ] as providers.TransactionReceipt; - const simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = - EventDecoder.decodeSimpleTransferFlowCompletedEvents( - transactionReceipt, - hyperEvmExecutorAddress, - ); - if (simpleTransferFlowCompletedEvents.length > 0) { - events.push(...simpleTransferFlowCompletedEvents); - } - } - - return events; - } - private getArbitraryActionsExecutedEventsFromTransactionReceipts( transactionReceipts: Record, arbitraryEvmFlowExecutorAddress: string, @@ -675,7 +657,8 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), - this.cctpRepository.formatAndSaveSimpleTransferFlowCompletedEvents( + formatAndSaveSimpleTransferFlowCompletedEvents( + this.cctpRepository, simpleTransferFlowCompletedEvents, lastFinalisedBlock, this.chainId, diff --git a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts index d5ae44a4..d74e7aca 100644 --- a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts @@ -2,9 +2,9 @@ import { Logger } from "winston"; import { ethers, providers, Transaction } from "ethers"; import * as across from "@across-protocol/sdk"; -import { DataSource, entities, SaveQueryResult } from "@repo/indexer-database"; +import { entities, SaveQueryResult } from "@repo/indexer-database"; -import { BlockRange } from "../model"; +import { BlockRange, SimpleTransferFlowCompletedLog } from "../model"; import { IndexerDataHandler } from "./IndexerDataHandler"; import { O_ADAPTER_UPGRADEABLE_ABI } from "../adapter/oft/abis"; import { @@ -18,26 +18,41 @@ import { isEndpointIdSupported, SPONSORED_OFT_SRC_PERIPHERY_ADDRESS, } from "../adapter/oft/service"; -import { OftTransferAggregator } from "./OftTransferAggregator"; import { EventDecoder } from "../../web3/EventDecoder"; +import { fetchEvents } from "../../utils/contractUtils"; +import { + formatAndSaveSimpleTransferFlowCompletedEvents, + getSimpleTransferFlowCompletedEventsFromTransactionReceipts, +} from "./hyperEvmExecutor"; +import { CHAIN_IDs } from "@across-protocol/constants"; export type FetchEventsResult = { oftSentEvents: OFTSentEvent[]; oftReceivedEvents: OFTReceivedEvent[]; sponsoredOFTSendEvents: SponsoredOFTSendLog[]; + simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[]; blocks: Record; }; export type StoreEventsResult = { oftSentEvents: SaveQueryResult[]; oftReceivedEvents: SaveQueryResult[]; sponsoredOFTSendEvents: SaveQueryResult[]; + simpleTransferFlowCompletedEvents: SaveQueryResult[]; +}; + +// Taken from https://hyperevmscan.io/tx/0xf72cfb2c0a9f781057cd4f7beca6fc6bd9290f1d73adef1142b8ac1b0ed7186c#eventlog#37 +// TODO: Add testnet endpoint v2 address when applicable +export const ENDPOINT_V2_ADDRESS = "0x3a73033c0b1407574c76bdbac67f126f6b4a9aa9"; + +const DST_OFT_HANDLER_ADDRESS: { [key: number]: string } = { + // Taken from https://hyperevmscan.io/address/0x2beF20D17a17f6903017d27D1A35CC9Dc72b0888#code + [CHAIN_IDs.HYPEREVM]: "0x2beF20D17a17f6903017d27D1A35CC9Dc72b0888", }; const SWAP_API_CALLDATA_MARKER = "73c0de"; export class OFTIndexerDataHandler implements IndexerDataHandler { private isInitialized: boolean; - private oftTransferAggregator: OftTransferAggregator; constructor( private logger: Logger, @@ -117,8 +132,10 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { O_ADAPTER_UPGRADEABLE_ABI, this.provider, ); + const dstOftHandlerAddress = DST_OFT_HANDLER_ADDRESS[this.chainId]; const sponsoredOFTSrcPeripheryAddress = SPONSORED_OFT_SRC_PERIPHERY_ADDRESS[this.chainId]; + const [oftSentEvents, oftReceivedEvents] = await Promise.all([ oftAdapterContract.queryFilter( "OFTSent", @@ -131,6 +148,7 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { blockRange.to, ) as Promise, ]); + let blockHashes = []; const oftSentTransactions = await this.getTransactions([ ...new Set(oftSentEvents.map((event) => event.transactionHash)), ]); @@ -138,12 +156,17 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftSentTransactions, oftSentEvents, ); + blockHashes.push(...filteredOftSentEvents.map((event) => event.blockHash)); + const filteredOftReceivedEvents = await this.filterTransactionsForSupportedEndpointIds(oftReceivedEvents); const filteredOftSentTransactionReceipts = await this.getTransactionsReceipts([ ...new Set(filteredOftSentEvents.map((event) => event.transactionHash)), ]); + blockHashes.push( + ...filteredOftReceivedEvents.map((event) => event.blockHash), + ); let sponsoredOFTSendEvents: SponsoredOFTSendLog[] = []; if (sponsoredOFTSrcPeripheryAddress) { @@ -154,12 +177,31 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { ); } - const blocks = await this.getBlocks([ - ...new Set([ - ...filteredOftSentEvents.map((event) => event.blockHash), - ...filteredOftReceivedEvents.map((event) => event.blockHash), - ]), - ]); + const simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = + []; + if (dstOftHandlerAddress) { + const composeDeliveredEvents = await fetchEvents( + this.provider, + ENDPOINT_V2_ADDRESS, + "event ComposeDelivered(address from, address to, bytes32 guid, uint16 index)", + blockRange.from, + blockRange.to, + ); + if (composeDeliveredEvents.length > 0) { + const transactionReceipts = await this.getTransactionsReceipts( + composeDeliveredEvents.map((event) => event.transactionHash), + ); + const events = + getSimpleTransferFlowCompletedEventsFromTransactionReceipts( + transactionReceipts, + dstOftHandlerAddress, + ); + simpleTransferFlowCompletedEvents.push(...events); + blockHashes.push(...events.map((event) => event.blockHash)); + } + } + + const blocks = await this.getBlocks([...new Set(blockHashes)]); if (oftSentEvents.length > 0) { this.logger.debug({ at: "Indexer#OFTIndexerDataHandler#fetchEventsByRange", @@ -176,6 +218,7 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftSentEvents: filteredOftSentEvents, oftReceivedEvents: filteredOftReceivedEvents, sponsoredOFTSendEvents, + simpleTransferFlowCompletedEvents, blocks, }; } @@ -185,13 +228,19 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { lastFinalisedBlock: number, tokenAddress: string, ): Promise { - const { blocks, oftReceivedEvents, oftSentEvents, sponsoredOFTSendEvents } = - events; + const { + blocks, + oftReceivedEvents, + oftSentEvents, + sponsoredOFTSendEvents, + simpleTransferFlowCompletedEvents, + } = events; const blocksTimestamps = this.getBlocksTimestamps(blocks); const [ savedOftSentEvents, savedOftReceivedEvents, savedSponsoredOFTSendEvents, + savedSimpleTransferFlowCompletedEvents, ] = await Promise.all([ this.oftRepository.formatAndSaveOftSentEvents( oftSentEvents, @@ -213,12 +262,20 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), + formatAndSaveSimpleTransferFlowCompletedEvents( + this.oftRepository, + simpleTransferFlowCompletedEvents, + lastFinalisedBlock, + this.chainId, + blocksTimestamps, + ), ]); return { oftSentEvents: savedOftSentEvents, oftReceivedEvents: savedOftReceivedEvents, sponsoredOFTSendEvents: savedSponsoredOFTSendEvents, + simpleTransferFlowCompletedEvents: savedSimpleTransferFlowCompletedEvents, }; } @@ -317,13 +374,13 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { private getBlocksTimestamps( blocks: Record, - ): Record { + ): Record { return Object.entries(blocks).reduce( (acc, [blockHash, block]) => { - acc[blockHash] = new Date(block.timestamp * 1000); + acc[block.number] = new Date(block.timestamp * 1000); return acc; }, - {} as Record, + {} as Record, ); } } diff --git a/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts b/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts new file mode 100644 index 00000000..da91ceb1 --- /dev/null +++ b/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts @@ -0,0 +1,92 @@ +import { ethers, providers } from "ethers"; +import { SimpleTransferFlowCompletedLog } from "../model"; +import { EventDecoder } from "../../web3/EventDecoder"; +import { entities, SaveQueryResult } from "@repo/indexer-database"; +import * as across from "@across-protocol/sdk"; +import { BlockchainEventRepository } from "../../../../indexer-database/dist/src/utils"; + +/** + * Decodes and extracts `SimpleTransferFlowCompleted` events from a collection of transaction receipts. + * This function iterates over transaction receipts, decodes logs, and filters for `SimpleTransferFlowCompleted` events + * emitted by a specified HyperEVM executor contract. + * + * @param transactionReceipts A record of transaction receipts, indexed by their transaction hash. + * @param contractAddress The address of the HyperEVM executor contract to filter events from. + * @returns An array of decoded `SimpleTransferFlowCompletedLog` objects. + */ +export function getSimpleTransferFlowCompletedEventsFromTransactionReceipts( + transactionReceipts: Record, + contractAddress: string, +) { + const events: SimpleTransferFlowCompletedLog[] = []; + for (const txHash of Object.keys(transactionReceipts)) { + const transactionReceipt = transactionReceipts[ + txHash + ] as providers.TransactionReceipt; + const simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = + EventDecoder.decodeSimpleTransferFlowCompletedEvents( + transactionReceipt, + contractAddress, + ); + if (simpleTransferFlowCompletedEvents.length > 0) { + events.push(...simpleTransferFlowCompletedEvents); + } + } + + return events; +} + +/** + * Formats and saves `SimpleTransferFlowCompleted` events to the database. + * This function maps the raw event data to the database entity format, marks them as finalized if they are within the finalized block range, + * and then saves them to the database in batches. + * + * @param repository The repository for database operations, specifically for saving blockchain events. + * @param simpleTransferFlowCompletedEvents An array of `SimpleTransferFlowCompletedLog` events to be processed. + * @param lastFinalisedBlock The last block number that is considered finalized. + * @param chainId The ID of the chain where these events were emitted. + * @param blockDates A record mapping block numbers to their corresponding `Date` objects. + * @param chunkSize The number of events to save in a single batch. Defaults to 100. + * @returns A promise that resolves to an array of `SaveQueryResult` for the saved events. + */ +export async function formatAndSaveSimpleTransferFlowCompletedEvents( + repository: BlockchainEventRepository, + simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[], + lastFinalisedBlock: number, + chainId: number, + blockDates: Record, + chunkSize = 100, +) { + const formattedEvents: Partial[] = + simpleTransferFlowCompletedEvents.map((event) => { + return { + blockNumber: event.blockNumber, + logIndex: event.logIndex, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + blockTimestamp: blockDates[event.blockNumber]!, + chainId: chainId.toString(), + quoteNonce: event.args.quoteNonce, + finalRecipient: event.args.finalRecipient, + finalToken: event.args.finalToken.toString(), + evmAmountIn: event.args.evmAmountIn.toString(), + bridgingFeesIncurred: event.args.bridgingFeesIncurred.toString(), + evmAmountSponsored: event.args.evmAmountSponsored.toString(), + finalised: event.blockNumber <= lastFinalisedBlock, + }; + }); + + const chunkedEvents = across.utils.chunk(formattedEvents, chunkSize); + const savedEvents = await Promise.all( + chunkedEvents.map((eventsChunk) => + repository.saveAndHandleFinalisationBatch( + entities.SimpleTransferFlowCompleted, + eventsChunk, + ["chainId", "blockNumber", "transactionHash", "logIndex"], + [], + ), + ), + ); + const result = savedEvents.flat(); + return result; +} diff --git a/packages/indexer/src/data-indexing/tests/OFTIndexerDataHandler.integration.test.ts b/packages/indexer/src/data-indexing/tests/OFTIndexerDataHandler.integration.test.ts index ab8c3eb4..cb50353f 100644 --- a/packages/indexer/src/data-indexing/tests/OFTIndexerDataHandler.integration.test.ts +++ b/packages/indexer/src/data-indexing/tests/OFTIndexerDataHandler.integration.test.ts @@ -73,4 +73,41 @@ describe("OFTIndexerDataHandler", () => { expect(savedEvent!.transactionHash).to.equal(transactionHash); expect(savedEvent!.blockNumber).to.equal(blockNumber); }).timeout(20000); + + it("should process a block range and store SimpleTransferFlowCompleted event for OFT", async () => { + const transactionHash = + "0xf72cfb2c0a9f781057cd4f7beca6fc6bd9290f1d73adef1142b8ac1b0ed7186c"; + const blockNumber = 18414987; + const blockRange: BlockRange = { + from: blockNumber, + to: blockNumber, + }; + setupTestForChainId(CHAIN_IDs.HYPEREVM); + // We need to stub the filterTransactionsFromSwapApi method to avoid filtering out our test transaction + sinon.stub(handler as any, "filterTransactionsFromSwapApi").resolvesArg(1); + await handler.processBlockRange(blockRange, blockNumber - 1); + + const simpleTransferFlowCompletedRepo = dataSource.getRepository( + entities.SimpleTransferFlowCompleted, + ); + const savedEvent = await simpleTransferFlowCompletedRepo.findOne({ + where: { transactionHash: transactionHash }, + }); + + expect(savedEvent).to.exist; + expect(savedEvent!.transactionHash).to.equal(transactionHash); + expect(savedEvent!.blockNumber).to.equal(blockNumber); + expect(savedEvent!.quoteNonce).to.equal( + "0x49a117e77ab01fd0d76ce06b042baa7b634cc7ff8b8749afbbfd0d5b09797ea7", + ); + expect(savedEvent!.finalRecipient).to.equal( + "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + ); + expect(savedEvent!.finalToken).to.equal( + "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", + ); + expect(savedEvent!.evmAmountIn.toString()).to.equal("1000000"); + expect(savedEvent!.bridgingFeesIncurred.toString()).to.equal("0"); + expect(savedEvent!.evmAmountSponsored.toString()).to.equal("0"); + }).timeout(20000); }); diff --git a/packages/indexer/src/database/CctpRepository.ts b/packages/indexer/src/database/CctpRepository.ts index da455e26..998bd3a6 100644 --- a/packages/indexer/src/database/CctpRepository.ts +++ b/packages/indexer/src/database/CctpRepository.ts @@ -163,44 +163,31 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { return totalDeleted; } - public async formatAndSaveSimpleTransferFlowCompletedEvents( - simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[], + public async formatAndSaveBurnEvents( + burnEvents: BurnEventsPair[], lastFinalisedBlock: number, chainId: number, blockDates: Record, ) { - const formattedEvents: Partial[] = - simpleTransferFlowCompletedEvents.map((event) => { - return { - blockNumber: event.blockNumber, - logIndex: event.logIndex, - transactionHash: event.transactionHash, - transactionIndex: event.transactionIndex, - blockTimestamp: blockDates[event.blockNumber]!, - chainId: chainId.toString(), - quoteNonce: event.args.quoteNonce, - finalRecipient: event.args.finalRecipient, - finalToken: event.args.finalToken.toString(), - evmAmountIn: event.args.evmAmountIn.toString(), - bridgingFeesIncurred: event.args.bridgingFeesIncurred.toString(), - evmAmountSponsored: event.args.evmAmountSponsored.toString(), - finalised: event.blockNumber <= lastFinalisedBlock, - }; - }); - - const chunkedEvents = across.utils.chunk(formattedEvents, this.chunkSize); - const savedEvents = await Promise.all( - chunkedEvents.map((eventsChunk) => - this.saveAndHandleFinalisationBatch( - entities.SimpleTransferFlowCompleted, - eventsChunk, - ["chainId", "blockNumber", "transactionHash", "logIndex"], - [], - ), - ), - ); - const result = savedEvents.flat(); - return result; + const savedEvents: { + depositForBurnEvent: SaveQueryResult; + messageSentEvent: SaveQueryResult; + }[] = []; + const chunkedEvents = across.utils.chunk(burnEvents, this.chunkSize); + for (const eventsChunk of chunkedEvents) { + const savedEventsChunk = await Promise.all( + eventsChunk.map(async (eventsPair) => { + return this.formatAndSaveBurnEventsPair( + eventsPair, + lastFinalisedBlock, + chainId, + blockDates, + ); + }), + ); + savedEvents.push(...savedEventsChunk); + } + return savedEvents; } public async formatAndSaveArbitraryActionsExecutedEvents( @@ -282,33 +269,6 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { return result; } - public async formatAndSaveBurnEvents( - burnEvents: BurnEventsPair[], - lastFinalisedBlock: number, - chainId: number, - blockDates: Record, - ) { - const savedEvents: { - depositForBurnEvent: SaveQueryResult; - messageSentEvent: SaveQueryResult; - }[] = []; - const chunkedEvents = across.utils.chunk(burnEvents, this.chunkSize); - for (const eventsChunk of chunkedEvents) { - const savedEventsChunk = await Promise.all( - eventsChunk.map(async (eventsPair) => { - return this.formatAndSaveBurnEventsPair( - eventsPair, - lastFinalisedBlock, - chainId, - blockDates, - ); - }), - ); - savedEvents.push(...savedEventsChunk); - } - return savedEvents; - } - public async formatAndSaveSponsoredBurnEvents( sponsoredBurnEvents: SponsoredDepositForBurnWithBlock[], lastFinalisedBlock: number, diff --git a/packages/indexer/src/database/OftRepository.ts b/packages/indexer/src/database/OftRepository.ts index f5734a0e..b1948999 100644 --- a/packages/indexer/src/database/OftRepository.ts +++ b/packages/indexer/src/database/OftRepository.ts @@ -25,7 +25,11 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { lastFinalisedBlock: number, ) { const chainIdColumn = "chainId"; - const [oftSentEvents, oftReceivedEvents] = await Promise.all([ + const [ + oftSentEvents, + oftReceivedEvents, + simpleTransferFlowCompletedEvents, + ] = await Promise.all([ this.deleteUnfinalisedEvents( chainId, chainIdColumn, @@ -38,41 +42,18 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { lastFinalisedBlock, entities.OFTReceived, ), - ]); - - return { - oftSentEvents, - oftReceivedEvents, - }; - } - - public async formatAndSaveOftEvents( - oftSentEvents: OFTSentEvent[], - oftReceivedEvents: OFTReceivedEvent[], - lastFinalisedBlock: number, - chainId: number, - blockDates: Record, - tokenAddress: string, - ) { - const [savedOftSentEvents, savedOftReceivedEvents] = await Promise.all([ - this.formatAndSaveOftSentEvents( - oftSentEvents, - lastFinalisedBlock, + this.deleteUnfinalisedEvents( chainId, - blockDates, - tokenAddress, - ), - this.formatAndSaveOftReceivedEvents( - oftReceivedEvents, + chainIdColumn, lastFinalisedBlock, - chainId, - blockDates, - tokenAddress, + entities.SimpleTransferFlowCompleted, ), ]); + return { - savedOftSentEvents, - savedOftReceivedEvents, + oftSentEvents, + oftReceivedEvents, + simpleTransferFlowCompletedEvents, }; } @@ -80,7 +61,7 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { oftSentEvents: OFTSentEvent[], lastFinalisedBlock: number, chainId: number, - blockDates: Record, + blockDates: Record, tokenAddress: string, ) { const formattedEvents: Partial[] = oftSentEvents.map( @@ -88,7 +69,7 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { return { ...this.formatTransactionData(event), - blockTimestamp: blockDates[event.blockHash]!, + blockTimestamp: blockDates[event.blockNumber]!, chainId: chainId.toString(), guid: event.args.guid, @@ -140,7 +121,7 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { sponsoredOFTSendEvents: SponsoredOFTSendLog[], lastFinalisedBlock: number, chainId: number, - blockDates: Record, + blockDates: Record, ) { const formattedEvents: Partial[] = sponsoredOFTSendEvents.map((event) => { @@ -155,7 +136,7 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { return { ...this.formatTransactionData(event), - blockTimestamp: blockDates[event.blockHash]!, + blockTimestamp: blockDates[event.blockNumber]!, chainId: chainId.toString(), quoteNonce: event.args.quoteNonce, originSender: event.args.originSender, @@ -189,14 +170,14 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { oftReceivedEvents: OFTReceivedEvent[], lastFinalisedBlock: number, chainId: number, - blockDates: Record, + blockDates: Record, tokenAddress: string, ) { const formattedEvents: Partial[] = oftReceivedEvents.map((event) => { return { ...this.formatTransactionData(event), - blockTimestamp: blockDates[event.blockHash]!, + blockTimestamp: blockDates[event.blockNumber]!, chainId: chainId.toString(), guid: event.args.guid, srcEid: event.args.srcEid, diff --git a/packages/indexer/src/utils/contractUtils.ts b/packages/indexer/src/utils/contractUtils.ts index 10b23ec3..5cfdb756 100644 --- a/packages/indexer/src/utils/contractUtils.ts +++ b/packages/indexer/src/utils/contractUtils.ts @@ -9,7 +9,7 @@ import { AcrossConfigStore__factory as AcrossConfigStoreFactory, } from "@across-protocol/contracts"; import * as across from "@across-protocol/sdk"; - +import { ethers } from "ethers"; import { EvmSpokePoolClient, SvmSpokePoolClient } from "./clients"; import { SvmProvider } from "../web3/RetryProvidersFactory"; @@ -246,4 +246,62 @@ export function getHubPoolClient( ); } +export type DecodedEventWithTxHash = { + decodedEvent: ethers.utils.LogDescription; + transactionHash: string; +}; + +/** + * Fetches and decodes blockchain events from a specified contract within a given block range. + * This utility is designed to be generic, allowing it to be used for various event types, + * such as 'Transfer' events for both CCTP and OFT protocols. + * + * @param provider The ethers.js provider instance to interact with the blockchain. + * @param contractAddress The address of the contract to query for events. + * @param eventAbi A string containing the ABI of the single event to fetch (e.g., "event Transfer(address indexed from, address indexed to, uint256 value)"). + * @param fromBlock The starting block number for the event search. + * @param toBlock The ending block number for the event search. + * @returns A promise that resolves to an array of objects, each containing the decoded event and its transaction hash. + */ +export async function fetchEvents( + provider: ethers.providers.Provider, + contractAddress: string, + eventAbi: string, + fromBlock: number, + toBlock: number, +): Promise { + // Create an interface for the event ABI to parse logs. + const eventInterface = new ethers.utils.Interface([eventAbi]); + // The event ABI string should contain only one event definition. + // We extract the event name from the parsed ABI. + const eventKeys = Object.keys(eventInterface.events); + const firstEventKey = eventKeys[0]; + if (!firstEventKey) { + // If no event is found in the ABI, return an empty array. + return []; + } + const eventName = eventInterface.events[firstEventKey]; + if (!eventName) { + return []; + } + // Get the event topic hash to filter logs. This allows us to fetch only the logs for the specified event. + const eventTopic = eventInterface.getEventTopic(eventName); + const logs = await provider.getLogs({ + address: contractAddress, + fromBlock, + toBlock, + topics: [eventTopic], + }); + + // Decode each log and pair it with its transaction hash. + const decodedEventsWithTxHash = logs.map((log) => { + return { + decodedEvent: eventInterface.parseLog(log), + transactionHash: log.transactionHash, + }; + }); + + return decodedEventsWithTxHash; +} + export const BN_ZERO = across.utils.bnZero; diff --git a/packages/indexer/src/web3/EventDecoder.ts b/packages/indexer/src/web3/EventDecoder.ts index 2bcb2da4..c84684d8 100644 --- a/packages/indexer/src/web3/EventDecoder.ts +++ b/packages/indexer/src/web3/EventDecoder.ts @@ -179,6 +179,16 @@ export class EventDecoder { return events; } + /** + * Decodes `SimpleTransferFlowCompleted` events from a transaction receipt. + * This event is emitted by the HyperEVM executor contract when a simple transfer flow is completed. + * The event topic and ABI are taken from the HyperEVM executor contract. + * See: https://hyperevmscan.io/tx/0xf72cfb2c0a9f781057cd4f7beca6fc6bd9290f1d73adef1142b8ac1b0ed7186c#eventlog#37 + * + * @param receipt The transaction receipt to decode events from. + * @param contractAddress Optional address of the contract that emitted the event to avoid decoding events from other contracts. + * @returns An array of decoded `SimpleTransferFlowCompletedLog` objects. + */ static decodeSimpleTransferFlowCompletedEvents( receipt: ethers.providers.TransactionReceipt, contractAddress?: string,