From 9839edc71d6b40695b5aaa4c4d9324fbb7b9700c Mon Sep 17 00:00:00 2001 From: Nikolas Haimerl Date: Tue, 18 Nov 2025 09:24:47 +0100 Subject: [PATCH 1/7] refactor: extract SimpleTransferFlowCompleted event handling to shared utility Move SimpleTransferFlowCompleted event processing logic from CctpRepository to a shared hyperEvmExecutor utility module. Add support for tracking these events in OFT indexer by fetching ComposeDelivered events and extracting SimpleTransferFlowCompleted events from transaction receipts. Update method signatures to use block --- .../src/utils/BlockchainEventRepository.ts | 2 +- .../src/data-indexing/adapter/oft/model.ts | 9 ++ .../data-indexing/model/hyperEvmExecutor.ts | 6 ++ .../service/CCTPIndexerDataHandler.ts | 31 ++----- .../service/OFTIndexerDataHandler.ts | 85 ++++++++++++++--- .../data-indexing/service/hyperEvmExecutor.ts | 92 +++++++++++++++++++ .../OFTIndexerDataHandler.integration.test.ts | 37 ++++++++ .../indexer/src/database/CctpRepository.ts | 82 +++++------------ .../indexer/src/database/OftRepository.ts | 42 ++------- packages/indexer/src/utils/contractUtils.ts | 60 +++++++++++- packages/indexer/src/web3/EventDecoder.ts | 10 ++ 11 files changed, 318 insertions(+), 138 deletions(-) create mode 100644 packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts 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 8bef26f0..333298e7 100644 --- a/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts +++ b/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts @@ -35,9 +35,15 @@ export interface FallbackHyperEVMFlowCompletedLog extends providers.Log { // Taken from https://testnet.purrsec.com/tx/0x1bf0dc091249341d0e91380b1c1d7dca683ab1b6773f7fb011b71a3d017a8fc9 export const HYPERCORE_FLOW_EXECUTOR_ADDRESS: { [key: number]: string } = { [CHAIN_IDs.HYPEREVM_TESTNET]: "0x06C61D54958a0772Ee8aF41789466d39FfeaeB13", + [CHAIN_IDs.HYPEREVM]: "0x2beF20D17a17f6903017d27D1A35CC9Dc72b0888", }; // Taken from https://hyperevmscan.io/tx/0x869d1df5f1e7b6b91a824d8e2b455ac48d1f26f0b5f2823c96df391eb75dff34#eventlog#8 export const ARBITRARY_EVM_FLOW_EXECUTOR_ADDRESS: { [key: number]: string } = { [CHAIN_IDs.HYPEREVM]: "0x7B164050BBC8e7ef3253e7db0D74b713Ba3F1c95", }; + +// Taken from https://hyperevmscan.io/tx/0xf72cfb2c0a9f781057cd4f7beca6fc6bd9290f1d73adef1142b8ac1b0ed7186c#eventlog#37 +export const ENDPOINT_V2_ADDRESS: { [key: number]: string } = { + [CHAIN_IDs.HYPEREVM]: "0x3a73033c0b1407574c76bdbac67f126f6b4a9aa9", +}; diff --git a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts index fbbbb0b0..279bc9e3 100644 --- a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts @@ -42,6 +42,10 @@ import { } from "../adapter/cctp-v2/service"; import { createMapWithDefault } from "../../utils/map"; import { entities, SaveQueryResult } from "@repo/indexer-database"; +import { + formatAndSaveSimpleTransferFlowCompletedEvents, + getSimpleTransferFlowCompletedEventsFromTransactionReceipts, +} from "./hyperEvmExecutor"; export type EvmBurnEventsPair = { depositForBurn: DepositForBurnEvent; @@ -300,7 +304,7 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { []; if (hyperEvmExecutorAddress) { simpleTransferFlowCompletedEvents = - this.getSimpleTransferFlowCompletedEventsFromTransactionReceipts( + getSimpleTransferFlowCompletedEventsFromTransactionReceipts( filteredMessageReceivedTxReceipts, hyperEvmExecutorAddress, ); @@ -529,28 +533,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, @@ -696,7 +678,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..b6b70fe3 100644 --- a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts @@ -2,9 +2,14 @@ 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, + HYPERCORE_FLOW_EXECUTOR_ADDRESS, + SimpleTransferFlowCompletedLog, + ENDPOINT_V2_ADDRESS, +} from "../model"; import { IndexerDataHandler } from "./IndexerDataHandler"; import { O_ADAPTER_UPGRADEABLE_ABI } from "../adapter/oft/abis"; import { @@ -18,26 +23,31 @@ 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"; export type FetchEventsResult = { oftSentEvents: OFTSentEvent[]; oftReceivedEvents: OFTReceivedEvent[]; sponsoredOFTSendEvents: SponsoredOFTSendLog[]; + simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[]; blocks: Record; }; export type StoreEventsResult = { oftSentEvents: SaveQueryResult[]; oftReceivedEvents: SaveQueryResult[]; sponsoredOFTSendEvents: SaveQueryResult[]; + simpleTransferFlowCompletedEvents: SaveQueryResult[]; }; const SWAP_API_CALLDATA_MARKER = "73c0de"; export class OFTIndexerDataHandler implements IndexerDataHandler { private isInitialized: boolean; - private oftTransferAggregator: OftTransferAggregator; constructor( private logger: Logger, @@ -117,6 +127,9 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { O_ADAPTER_UPGRADEABLE_ABI, this.provider, ); + const hypercoreFlowExecutorAddress = + HYPERCORE_FLOW_EXECUTOR_ADDRESS[this.chainId]; + const endpointAddress = ENDPOINT_V2_ADDRESS[this.chainId]; const sponsoredOFTSrcPeripheryAddress = SPONSORED_OFT_SRC_PERIPHERY_ADDRESS[this.chainId]; const [oftSentEvents, oftReceivedEvents] = await Promise.all([ @@ -131,6 +144,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 +152,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 +173,33 @@ 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 (endpointAddress) { + const composeDeliveredEvents = await fetchEvents( + this.provider, + endpointAddress, + "event ComposeDelivered(address from, address to, bytes32 guid, uint16 index)", + blockRange.from, + blockRange.to, + ); + if (composeDeliveredEvents.length > 0) { + if (hypercoreFlowExecutorAddress) { + const transactionReceipts = await this.getTransactionsReceipts( + composeDeliveredEvents.map((event) => event.transactionHash), + ); + const events = + getSimpleTransferFlowCompletedEventsFromTransactionReceipts( + transactionReceipts, + hypercoreFlowExecutorAddress, + ); + 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 +216,7 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftSentEvents: filteredOftSentEvents, oftReceivedEvents: filteredOftReceivedEvents, sponsoredOFTSendEvents, + simpleTransferFlowCompletedEvents, blocks, }; } @@ -185,13 +226,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 +260,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 +372,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..20f9b2bf --- /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 hyperEvmExecutorAddress The address of the HyperEVM executor contract to filter events from. + * @returns An array of decoded `SimpleTransferFlowCompletedLog` objects. + */ +export function 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; +} + +/** + * 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..df2c57b7 100644 --- a/packages/indexer/src/database/OftRepository.ts +++ b/packages/indexer/src/database/OftRepository.ts @@ -46,41 +46,11 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { }; } - 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, - chainId, - blockDates, - tokenAddress, - ), - this.formatAndSaveOftReceivedEvents( - oftReceivedEvents, - lastFinalisedBlock, - chainId, - blockDates, - tokenAddress, - ), - ]); - return { - savedOftSentEvents, - savedOftReceivedEvents, - }; - } - public async formatAndSaveOftSentEvents( oftSentEvents: OFTSentEvent[], lastFinalisedBlock: number, chainId: number, - blockDates: Record, + blockDates: Record, tokenAddress: string, ) { const formattedEvents: Partial[] = oftSentEvents.map( @@ -88,7 +58,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 +110,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 +125,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 +159,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, From 64f3f75173f6089b0e8e1947e550d8acec879698 Mon Sep 17 00:00:00 2001 From: Nikolas Haimerl Date: Tue, 18 Nov 2025 09:38:53 +0100 Subject: [PATCH 2/7] add addresses for hyperEVM --- .../indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts index 6e3643cd..be44be7b 100644 --- a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts @@ -84,12 +84,14 @@ export type StoreEventsResult = { const TOKEN_MESSENGER_ADDRESS: { [key: number]: string } = { [CHAIN_IDs.ARBITRUM_SEPOLIA]: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA", [CHAIN_IDs.HYPEREVM_TESTNET]: "0x8FE6B999Dc680CcFDD5Bf7EB0974218be2542DAA", + [CHAIN_IDs.HYPEREVM]: "0x28b5a0e9C621a5BadaA536219b3a228C8168cf5d", }; // Taken from https://developers.circle.com/cctp/evm-smart-contracts const MESSAGE_TRANSMITTER_ADDRESS: { [key: number]: string } = { [CHAIN_IDs.ARBITRUM_SEPOLIA]: "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275", [CHAIN_IDs.HYPEREVM_TESTNET]: "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275", + [CHAIN_IDs.HYPEREVM]: "0x81D40F21F12A8F0E3252Bccb954D722d4c464B64", }; // TODO: Update this address once the contract is deployed From 6a9e5044cb2296ca901b6994ec56c1658fb579ed Mon Sep 17 00:00:00 2001 From: Nikolas Haimerl Date: Tue, 18 Nov 2025 12:27:14 +0100 Subject: [PATCH 3/7] move endpoint address --- .../data-indexing/model/hyperEvmExecutor.ts | 5 --- .../service/OFTIndexerDataHandler.ts | 44 +++++++++---------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts b/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts index 333298e7..b3fb1661 100644 --- a/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts +++ b/packages/indexer/src/data-indexing/model/hyperEvmExecutor.ts @@ -42,8 +42,3 @@ export const HYPERCORE_FLOW_EXECUTOR_ADDRESS: { [key: number]: string } = { export const ARBITRARY_EVM_FLOW_EXECUTOR_ADDRESS: { [key: number]: string } = { [CHAIN_IDs.HYPEREVM]: "0x7B164050BBC8e7ef3253e7db0D74b713Ba3F1c95", }; - -// Taken from https://hyperevmscan.io/tx/0xf72cfb2c0a9f781057cd4f7beca6fc6bd9290f1d73adef1142b8ac1b0ed7186c#eventlog#37 -export const ENDPOINT_V2_ADDRESS: { [key: number]: string } = { - [CHAIN_IDs.HYPEREVM]: "0x3a73033c0b1407574c76bdbac67f126f6b4a9aa9", -}; diff --git a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts index b6b70fe3..e952d22d 100644 --- a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts @@ -8,7 +8,6 @@ import { BlockRange, HYPERCORE_FLOW_EXECUTOR_ADDRESS, SimpleTransferFlowCompletedLog, - ENDPOINT_V2_ADDRESS, } from "../model"; import { IndexerDataHandler } from "./IndexerDataHandler"; import { O_ADAPTER_UPGRADEABLE_ABI } from "../adapter/oft/abis"; @@ -44,6 +43,9 @@ export type StoreEventsResult = { 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 SWAP_API_CALLDATA_MARKER = "73c0de"; export class OFTIndexerDataHandler implements IndexerDataHandler { @@ -129,9 +131,9 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { ); const hypercoreFlowExecutorAddress = HYPERCORE_FLOW_EXECUTOR_ADDRESS[this.chainId]; - const endpointAddress = ENDPOINT_V2_ADDRESS[this.chainId]; const sponsoredOFTSrcPeripheryAddress = SPONSORED_OFT_SRC_PERIPHERY_ADDRESS[this.chainId]; + const [oftSentEvents, oftReceivedEvents] = await Promise.all([ oftAdapterContract.queryFilter( "OFTSent", @@ -175,27 +177,25 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { const simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = []; - if (endpointAddress) { - const composeDeliveredEvents = await fetchEvents( - this.provider, - endpointAddress, - "event ComposeDelivered(address from, address to, bytes32 guid, uint16 index)", - blockRange.from, - blockRange.to, - ); - if (composeDeliveredEvents.length > 0) { - if (hypercoreFlowExecutorAddress) { - const transactionReceipts = await this.getTransactionsReceipts( - composeDeliveredEvents.map((event) => event.transactionHash), + 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) { + if (hypercoreFlowExecutorAddress) { + const transactionReceipts = await this.getTransactionsReceipts( + composeDeliveredEvents.map((event) => event.transactionHash), + ); + const events = + getSimpleTransferFlowCompletedEventsFromTransactionReceipts( + transactionReceipts, + hypercoreFlowExecutorAddress, ); - const events = - getSimpleTransferFlowCompletedEventsFromTransactionReceipts( - transactionReceipts, - hypercoreFlowExecutorAddress, - ); - simpleTransferFlowCompletedEvents.push(...events); - blockHashes.push(...events.map((event) => event.blockHash)); - } + simpleTransferFlowCompletedEvents.push(...events); + blockHashes.push(...events.map((event) => event.blockHash)); } } From 2125cf0d60619cd99968bc32d1dc683458ceedb3 Mon Sep 17 00:00:00 2001 From: Nikolas Haimerl Date: Wed, 19 Nov 2025 12:54:15 +0100 Subject: [PATCH 4/7] update delete function --- packages/indexer/src/database/OftRepository.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/indexer/src/database/OftRepository.ts b/packages/indexer/src/database/OftRepository.ts index df2c57b7..7d268fd2 100644 --- a/packages/indexer/src/database/OftRepository.ts +++ b/packages/indexer/src/database/OftRepository.ts @@ -38,6 +38,12 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { lastFinalisedBlock, entities.OFTReceived, ), + this.deleteUnfinalisedEvents( + chainId, + chainIdColumn, + lastFinalisedBlock, + entities.SimpleTransferFlowCompleted, + ), ]); return { From 5deacb4b2bed2a98b23be978ff3e5f0b850cdeaa Mon Sep 17 00:00:00 2001 From: Nikolas Haimerl Date: Wed, 19 Nov 2025 12:58:03 +0100 Subject: [PATCH 5/7] update delete function --- packages/indexer/src/database/OftRepository.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/indexer/src/database/OftRepository.ts b/packages/indexer/src/database/OftRepository.ts index 7d268fd2..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, @@ -49,6 +53,7 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { return { oftSentEvents, oftReceivedEvents, + simpleTransferFlowCompletedEvents, }; } From 2b99900f86308885af113d169a528ed2a67efc1f Mon Sep 17 00:00:00 2001 From: Nikolas Haimerl Date: Wed, 19 Nov 2025 17:33:53 +0100 Subject: [PATCH 6/7] Optimize OFT indexer by conditionally fetching ComposeDelivered events --- .../service/OFTIndexerDataHandler.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts index ce7e7a17..d74e7aca 100644 --- a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts @@ -48,7 +48,6 @@ 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"; @@ -180,15 +179,15 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { const simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = []; - 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) { - if (dstOftHandlerAddress) { + 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), ); From 0e016f58f26ec6ad6e97b2e0ab0626377e9e4807 Mon Sep 17 00:00:00 2001 From: Nikolas Haimerl <113891786+NikolasHaimerl@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:44:11 +0100 Subject: [PATCH 7/7] feat: track fallback hyper evm flow completed oft (#434) --- .../service/CCTPIndexerDataHandler.ts | 159 +++++----------- .../service/OFTIndexerDataHandler.ts | 127 +++++++++---- .../data-indexing/service/eventProcessing.ts | 93 ++++++++++ .../data-indexing/service/hyperEvmExecutor.ts | 171 +++++++++--------- .../OFTIndexerDataHandler.integration.test.ts | 37 ++++ .../indexer/src/database/OftRepository.ts | 16 ++ packages/indexer/src/web3/EventDecoder.ts | 24 +-- 7 files changed, 373 insertions(+), 254 deletions(-) create mode 100644 packages/indexer/src/data-indexing/service/eventProcessing.ts diff --git a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts index 90319613..e58254c0 100644 --- a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts @@ -40,9 +40,13 @@ import { } from "../adapter/cctp-v2/service"; import { entities, SaveQueryResult } from "@repo/indexer-database"; import { - formatAndSaveSimpleTransferFlowCompletedEvents, - getSimpleTransferFlowCompletedEventsFromTransactionReceipts, + formatFallbackHyperEVMFlowCompletedEvent, + formatSimpleTransferFlowCompletedEvent, } from "./hyperEvmExecutor"; +import { + formatAndSaveEvents, + getEventsFromTransactionReceipts, +} from "./eventProcessing"; export type EvmBurnEventsPair = { depositForBurn: DepositForBurnEvent; @@ -246,16 +250,17 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { ), ]); - const messageSentEvents = this.getMessageSentEventsFromTransactionReceipts( + const messageSentEvents = getEventsFromTransactionReceipts( filteredDepositForBurnTxReceipts, messageTransmitterAddress, + EventDecoder.decodeCCTPMessageSentEvents, ); - const mintAndWithdrawEvents = - this.getMintAndWithdrawEventsFromTransactionReceipts( - filteredMessageReceivedTxReceipts, - tokenMessengerAddress, - ); + const mintAndWithdrawEvents = getEventsFromTransactionReceipts( + filteredMessageReceivedTxReceipts, + tokenMessengerAddress, + EventDecoder.decodeCCTPMintAndWithdrawEvents, + ); const burnEvents = await this.matchDepositForBurnWithMessageSentEvents( filteredDepositForBurnEvents, @@ -289,23 +294,23 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { let fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[] = []; if (sponsoredCCTPDstPeripheryAddress) { - simpleTransferFlowCompletedEvents = - getSimpleTransferFlowCompletedEventsFromTransactionReceipts( - filteredMessageReceivedTxReceipts, - sponsoredCCTPDstPeripheryAddress, - ); + simpleTransferFlowCompletedEvents = getEventsFromTransactionReceipts( + filteredMessageReceivedTxReceipts, + sponsoredCCTPDstPeripheryAddress, + EventDecoder.decodeSimpleTransferFlowCompletedEvents, + ); - arbitraryActionsExecutedEvents = - this.getArbitraryActionsExecutedEventsFromTransactionReceipts( - filteredMessageReceivedTxReceipts, - sponsoredCCTPDstPeripheryAddress, - ); + arbitraryActionsExecutedEvents = getEventsFromTransactionReceipts( + filteredMessageReceivedTxReceipts, + sponsoredCCTPDstPeripheryAddress, + EventDecoder.decodeArbitraryActionsExecutedEvents, + ); - fallbackHyperEVMFlowCompletedEvents = - this.getFallbackHyperEVMFlowCompletedEventsFromTransactionReceipts( - filteredMessageReceivedTxReceipts, - sponsoredCCTPDstPeripheryAddress, - ); + fallbackHyperEVMFlowCompletedEvents = getEventsFromTransactionReceipts( + filteredMessageReceivedTxReceipts, + sponsoredCCTPDstPeripheryAddress, + EventDecoder.decodeFallbackHyperEVMFlowCompletedEvents, + ); } this.runChecks(burnEvents, mintEvents); @@ -408,52 +413,6 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { } } - private getMessageSentEventsFromTransactionReceipts( - transactionReceipts: Record, - messageTransmitterAddress: string, - ) { - const events: MessageSentLog[] = []; - - for (const txHash of Object.keys(transactionReceipts)) { - const transactionReceipt = transactionReceipts[ - txHash - ] as providers.TransactionReceipt; - const messageSentEvents: MessageSentLog[] = - EventDecoder.decodeCCTPMessageSentEvents( - transactionReceipt, - messageTransmitterAddress, - ); - if (messageSentEvents.length > 0) { - events.push(...messageSentEvents); - } - } - - return events; - } - - private getMintAndWithdrawEventsFromTransactionReceipts( - transactionReceipts: Record, - tokenMessengerAddress: string, - ) { - const events: MintAndWithdrawLog[] = []; - - for (const txHash of Object.keys(transactionReceipts)) { - const transactionReceipt = transactionReceipts[ - txHash - ] as providers.TransactionReceipt; - const mintAndWithdrawEvents: MintAndWithdrawLog[] = - EventDecoder.decodeCCTPMintAndWithdrawEvents( - transactionReceipt, - tokenMessengerAddress, - ); - if (mintAndWithdrawEvents.length > 0) { - events.push(...mintAndWithdrawEvents); - } - } - - return events; - } - private getSponsoredDepositForBurnEventsFromTransactionReceipts( transactionReceipts: Record, sponsoredCCTPSrcPeripheryAddress: string, @@ -512,50 +471,6 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { return events; } - private getArbitraryActionsExecutedEventsFromTransactionReceipts( - transactionReceipts: Record, - arbitraryEvmFlowExecutorAddress: string, - ) { - const events: ArbitraryActionsExecutedLog[] = []; - for (const txHash of Object.keys(transactionReceipts)) { - const transactionReceipt = transactionReceipts[ - txHash - ] as providers.TransactionReceipt; - const arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[] = - EventDecoder.decodeArbitraryActionsExecutedEvents( - transactionReceipt, - arbitraryEvmFlowExecutorAddress, - ); - if (arbitraryActionsExecutedEvents.length > 0) { - events.push(...arbitraryActionsExecutedEvents); - } - } - - return events; - } - - private getFallbackHyperEVMFlowCompletedEventsFromTransactionReceipts( - transactionReceipts: Record, - arbitraryEvmFlowExecutorAddress: string, - ) { - const events: FallbackHyperEVMFlowCompletedLog[] = []; - for (const txHash of Object.keys(transactionReceipts)) { - const transactionReceipt = transactionReceipts[ - txHash - ] as providers.TransactionReceipt; - const fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[] = - EventDecoder.decodeFallbackHyperEVMFlowCompletedEvents( - transactionReceipt, - arbitraryEvmFlowExecutorAddress, - ); - if (fallbackHyperEVMFlowCompletedEvents.length > 0) { - events.push(...fallbackHyperEVMFlowCompletedEvents); - } - } - - return events; - } - private async getTransactionsReceipts(uniqueTransactionHashes: string[]) { const transactionReceipts = await Promise.all( uniqueTransactionHashes.map(async (txHash) => { @@ -630,7 +545,12 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { const chainAgnosticSponsoredBurnEvents = sponsoredBurnEvents.map((event) => this.convertSponsoredDepositForBurnToChainAgnostic(event), ); - + const primaryKeyColumns = [ + "chainId", + "blockNumber", + "transactionHash", + "logIndex", + ]; const [ savedBurnEvents, savedMintEvents, @@ -657,12 +577,15 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), - formatAndSaveSimpleTransferFlowCompletedEvents( + formatAndSaveEvents( this.cctpRepository, simpleTransferFlowCompletedEvents, lastFinalisedBlock, this.chainId, blocksTimestamps, + formatSimpleTransferFlowCompletedEvent, + entities.SimpleTransferFlowCompleted, + primaryKeyColumns as (keyof entities.SimpleTransferFlowCompleted)[], ), this.cctpRepository.formatAndSaveArbitraryActionsExecutedEvents( arbitraryActionsExecutedEvents, @@ -670,11 +593,15 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), - this.cctpRepository.formatAndSaveFallbackHyperEVMFlowCompletedEvents( + formatAndSaveEvents( + this.cctpRepository, fallbackHyperEVMFlowCompletedEvents, lastFinalisedBlock, this.chainId, blocksTimestamps, + formatFallbackHyperEVMFlowCompletedEvent, + entities.FallbackHyperEVMFlowCompleted, + primaryKeyColumns as (keyof entities.FallbackHyperEVMFlowCompleted)[], ), ]); diff --git a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts index d74e7aca..194ac443 100644 --- a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts @@ -4,7 +4,12 @@ import * as across from "@across-protocol/sdk"; import { entities, SaveQueryResult } from "@repo/indexer-database"; -import { BlockRange, SimpleTransferFlowCompletedLog } from "../model"; +import { + ArbitraryActionsExecutedLog, + BlockRange, + FallbackHyperEVMFlowCompletedLog, + SimpleTransferFlowCompletedLog, +} from "../model"; import { IndexerDataHandler } from "./IndexerDataHandler"; import { O_ADAPTER_UPGRADEABLE_ABI } from "../adapter/oft/abis"; import { @@ -21,8 +26,13 @@ import { import { EventDecoder } from "../../web3/EventDecoder"; import { fetchEvents } from "../../utils/contractUtils"; import { - formatAndSaveSimpleTransferFlowCompletedEvents, - getSimpleTransferFlowCompletedEventsFromTransactionReceipts, + formatAndSaveEvents, + getEventsFromTransactionReceipts, +} from "./eventProcessing"; +import { + formatArbitraryActionsExecutedEvent, + formatFallbackHyperEVMFlowCompletedEvent, + formatSimpleTransferFlowCompletedEvent, } from "./hyperEvmExecutor"; import { CHAIN_IDs } from "@across-protocol/constants"; @@ -31,6 +41,8 @@ export type FetchEventsResult = { oftReceivedEvents: OFTReceivedEvent[]; sponsoredOFTSendEvents: SponsoredOFTSendLog[]; simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[]; + fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[]; + arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[]; blocks: Record; }; export type StoreEventsResult = { @@ -38,6 +50,8 @@ export type StoreEventsResult = { oftReceivedEvents: SaveQueryResult[]; sponsoredOFTSendEvents: SaveQueryResult[]; simpleTransferFlowCompletedEvents: SaveQueryResult[]; + fallbackHyperEVMFlowCompletedEvents: SaveQueryResult[]; + arbitraryActionsExecutedEvents: SaveQueryResult[]; }; // Taken from https://hyperevmscan.io/tx/0xf72cfb2c0a9f781057cd4f7beca6fc6bd9290f1d73adef1142b8ac1b0ed7186c#eventlog#37 @@ -156,8 +170,6 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftSentTransactions, oftSentEvents, ); - blockHashes.push(...filteredOftSentEvents.map((event) => event.blockHash)); - const filteredOftReceivedEvents = await this.filterTransactionsForSupportedEndpointIds(oftReceivedEvents); const filteredOftSentTransactionReceipts = @@ -165,20 +177,24 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { ...new Set(filteredOftSentEvents.map((event) => event.transactionHash)), ]); blockHashes.push( + ...filteredOftSentEvents.map((event) => event.blockHash), ...filteredOftReceivedEvents.map((event) => event.blockHash), ); let sponsoredOFTSendEvents: SponsoredOFTSendLog[] = []; if (sponsoredOFTSrcPeripheryAddress) { - sponsoredOFTSendEvents = - this.getSponsoredOFTSendEventsFromTransactionReceipts( - filteredOftSentTransactionReceipts, - sponsoredOFTSrcPeripheryAddress, - ); + sponsoredOFTSendEvents = getEventsFromTransactionReceipts( + filteredOftSentTransactionReceipts, + sponsoredOFTSrcPeripheryAddress, + EventDecoder.decodeOFTSponsoredSendEvents, + ); } - const simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = + let simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = + []; + let fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[] = []; + let arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[] = []; if (dstOftHandlerAddress) { const composeDeliveredEvents = await fetchEvents( this.provider, @@ -191,13 +207,28 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { 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)); + simpleTransferFlowCompletedEvents = getEventsFromTransactionReceipts( + transactionReceipts, + dstOftHandlerAddress, + EventDecoder.decodeSimpleTransferFlowCompletedEvents, + ); + fallbackHyperEVMFlowCompletedEvents = getEventsFromTransactionReceipts( + transactionReceipts, + dstOftHandlerAddress, + EventDecoder.decodeFallbackHyperEVMFlowCompletedEvents, + ); + arbitraryActionsExecutedEvents = getEventsFromTransactionReceipts( + transactionReceipts, + dstOftHandlerAddress, + EventDecoder.decodeArbitraryActionsExecutedEvents, + ); + blockHashes.push( + ...simpleTransferFlowCompletedEvents.map((event) => event.blockHash), + ...fallbackHyperEVMFlowCompletedEvents.map( + (event) => event.blockHash, + ), + ...arbitraryActionsExecutedEvents.map((event) => event.blockHash), + ); } } @@ -219,6 +250,8 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftReceivedEvents: filteredOftReceivedEvents, sponsoredOFTSendEvents, simpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents, blocks, }; } @@ -234,13 +267,23 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftSentEvents, sponsoredOFTSendEvents, simpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents, } = events; const blocksTimestamps = this.getBlocksTimestamps(blocks); + const primaryKeyColumns = [ + "chainId", + "blockNumber", + "transactionHash", + "logIndex", + ]; const [ savedOftSentEvents, savedOftReceivedEvents, savedSponsoredOFTSendEvents, savedSimpleTransferFlowCompletedEvents, + savedFallbackHyperEVMFlowCompletedEvents, + savedArbitraryActionsExecutedEvents, ] = await Promise.all([ this.oftRepository.formatAndSaveOftSentEvents( oftSentEvents, @@ -262,12 +305,35 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), - formatAndSaveSimpleTransferFlowCompletedEvents( + formatAndSaveEvents( this.oftRepository, simpleTransferFlowCompletedEvents, lastFinalisedBlock, this.chainId, blocksTimestamps, + formatSimpleTransferFlowCompletedEvent, + entities.SimpleTransferFlowCompleted, + primaryKeyColumns as (keyof entities.SimpleTransferFlowCompleted)[], + ), + formatAndSaveEvents( + this.oftRepository, + fallbackHyperEVMFlowCompletedEvents, + lastFinalisedBlock, + this.chainId, + blocksTimestamps, + formatFallbackHyperEVMFlowCompletedEvent, + entities.FallbackHyperEVMFlowCompleted, + primaryKeyColumns as (keyof entities.FallbackHyperEVMFlowCompleted)[], + ), + formatAndSaveEvents( + this.oftRepository, + arbitraryActionsExecutedEvents, + lastFinalisedBlock, + this.chainId, + blocksTimestamps, + formatArbitraryActionsExecutedEvent, + entities.ArbitraryActionsExecuted, + primaryKeyColumns as (keyof entities.ArbitraryActionsExecuted)[], ), ]); @@ -276,6 +342,9 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftReceivedEvents: savedOftReceivedEvents, sponsoredOFTSendEvents: savedSponsoredOFTSendEvents, simpleTransferFlowCompletedEvents: savedSimpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents: + savedFallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents: savedArbitraryActionsExecutedEvents, }; } @@ -311,26 +380,6 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { return transactionReceiptsMap; } - private getSponsoredOFTSendEventsFromTransactionReceipts( - transactionReceipts: Record, - sponsoredOFTSrcPeripheryAddress: string, - ) { - const events: SponsoredOFTSendLog[] = []; - for (const txHash of Object.keys(transactionReceipts)) { - const transactionReceipt = transactionReceipts[ - txHash - ] as providers.TransactionReceipt; - const sponsoredOFTSendEvents = EventDecoder.decodeOFTSponsoredSendEvents( - transactionReceipt, - sponsoredOFTSrcPeripheryAddress, - ); - if (sponsoredOFTSendEvents.length > 0) { - events.push(...sponsoredOFTSendEvents); - } - } - return events; - } - private async filterTransactionsForSupportedEndpointIds( oftReceivedEvents: OFTReceivedEvent[], ) { diff --git a/packages/indexer/src/data-indexing/service/eventProcessing.ts b/packages/indexer/src/data-indexing/service/eventProcessing.ts new file mode 100644 index 00000000..11aa14ca --- /dev/null +++ b/packages/indexer/src/data-indexing/service/eventProcessing.ts @@ -0,0 +1,93 @@ +import { SaveQueryResult } from "@repo/indexer-database"; +import * as across from "@across-protocol/sdk"; +import { utils } from "@repo/indexer-database"; +import { ObjectLiteral } from "typeorm"; +import { providers } from "ethers"; + +/** + * Formats and saves a batch of blockchain events to the database using a provided formatting function. + * This generic function is designed to handle different types of events by accepting a specific formatting function for each event type. + * It 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 events An array of 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 formatEvent A function that takes an event and returns a partial entity. + * @param entity The entity to save the events to. + * @param primaryKeyColumns The primary key columns of the entity. + * @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 formatAndSaveEvents( + repository: utils.BlockchainEventRepository, + events: T[], + lastFinalisedBlock: number, + chainId: number, + blockDates: Record, + formatEvent: ( + event: T, + finalised: boolean, + blockTimestamp: Date, + chainId: number, + ) => Partial, + entity: new () => TEntity, + primaryKeyColumns: (keyof TEntity)[], + chunkSize = 100, +): Promise[]> { + const formattedEvents = events.map((event: any) => { + const finalised = event.blockNumber <= lastFinalisedBlock; + const blockTimestamp = blockDates[event.blockNumber]!; + return formatEvent(event, finalised, blockTimestamp, chainId); + }); + + const chunkedEvents = across.utils.chunk(formattedEvents, chunkSize); + const savedEvents = await Promise.all( + chunkedEvents.map((eventsChunk) => + repository.saveAndHandleFinalisationBatch( + entity, + eventsChunk, + primaryKeyColumns as string[], + [], + ), + ), + ); + const result = savedEvents.flat(); + return result; +} + +/** + * Decodes and extracts events from a collection of transaction receipts using a provided decoding function. + * This generic function iterates over transaction receipts, decodes logs, and filters for events + * emitted by a specified contract address. + * + * @param transactionReceipts A record of transaction receipts, indexed by their transaction hash. + * @param contractAddress The address of the contract to filter events from. + * @param decodeEvents A function that takes a transaction receipt and contract address and returns an array of decoded events. + * @returns An array of decoded event objects. + */ +export function getEventsFromTransactionReceipts( + transactionReceipts: Record, + contractAddress: string, + decodeEvents: ( + receipt: providers.TransactionReceipt, + contractAddress?: string, + ) => T[], +): T[] { + const events: T[] = []; + for (const txHash of Object.keys(transactionReceipts)) { + const transactionReceipt = transactionReceipts[ + txHash + ] as providers.TransactionReceipt; + const decodedEvents: T[] = decodeEvents( + transactionReceipt, + contractAddress, + ); + if (decodedEvents.length > 0) { + events.push(...decodedEvents); + } + } + return events; +} diff --git a/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts b/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts index da91ceb1..6df8df39 100644 --- a/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts +++ b/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts @@ -1,92 +1,95 @@ -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"; +import { + ArbitraryActionsExecutedLog, + FallbackHyperEVMFlowCompletedLog, + SimpleTransferFlowCompletedLog, +} from "../model"; +import { entities } from "@repo/indexer-database"; /** - * 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. + * @constant formatSimpleTransferFlowCompletedEvent + * Formats a `SimpleTransferFlowCompletedLog` event into a partial `SimpleTransferFlowCompleted` entity. + * @param event The `SimpleTransferFlowCompletedLog` event to format. + * @param finalised A boolean indicating if the event is finalized. + * @param blockTimestamp The timestamp of the block where the event was emitted. + * @param chainId The ID of the chain where the event was emitted. + * @returns A partial `SimpleTransferFlowCompleted` entity. */ -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; -} +export const formatSimpleTransferFlowCompletedEvent = ( + event: SimpleTransferFlowCompletedLog, + finalised: boolean, + blockTimestamp: Date, + chainId: number, +): Partial => ({ + blockNumber: event.blockNumber, + logIndex: event.logIndex, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + blockTimestamp: blockTimestamp, + 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, +}); /** - * 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. + * @constant formatArbitraryActionsExecutedEvent + * Formats an `ArbitraryActionsExecutedLog` event into a partial `ArbitraryActionsExecuted` entity. + * @param event The `ArbitraryActionsExecutedLog` event to format. + * @param finalised A boolean indicating if the event is finalized. + * @param blockTimestamp The timestamp of the block where the event was emitted. + * @param chainId The ID of the chain where the event was emitted. + * @returns A partial `ArbitraryActionsExecuted` entity. */ -export async function formatAndSaveSimpleTransferFlowCompletedEvents( - repository: BlockchainEventRepository, - simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[], - lastFinalisedBlock: number, +export const formatArbitraryActionsExecutedEvent = ( + event: ArbitraryActionsExecutedLog, + finalised: boolean, + blockTimestamp: Date, 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, - }; - }); +): Partial => ({ + blockNumber: event.blockNumber, + logIndex: event.logIndex, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + blockTimestamp: blockTimestamp, + chainId: chainId.toString(), + quoteNonce: event.args.quoteNonce, + initialToken: event.args.initialToken, + initialAmount: event.args.initialAmount.toString(), + finalToken: event.args.finalToken, + finalAmount: event.args.finalAmount.toString(), + finalised, +}); - 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; -} +/** + * @constant formatFallbackHyperEVMFlowCompletedEvent + * Formats a `FallbackHyperEVMFlowCompletedLog` event into a partial `FallbackHyperEVMFlowCompleted` entity. + * @param event The `FallbackHyperEVMFlowCompletedLog` event to format. + * @param finalised A boolean indicating if the event is finalized. + * @param blockTimestamp The timestamp of the block where the event was emitted. + * @param chainId The ID of the chain where the event was emitted. + * @returns A partial `FallbackHyperEVMFlowCompleted` entity. + */ +export const formatFallbackHyperEVMFlowCompletedEvent = ( + event: FallbackHyperEVMFlowCompletedLog, + finalised: boolean, + blockTimestamp: Date, + chainId: number, +): Partial => ({ + blockNumber: event.blockNumber, + logIndex: event.logIndex, + transactionHash: event.transactionHash, + transactionIndex: event.transactionIndex, + blockTimestamp: blockTimestamp, + 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, +}); 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 cb50353f..74b887e3 100644 --- a/packages/indexer/src/data-indexing/tests/OFTIndexerDataHandler.integration.test.ts +++ b/packages/indexer/src/data-indexing/tests/OFTIndexerDataHandler.integration.test.ts @@ -110,4 +110,41 @@ describe("OFTIndexerDataHandler", () => { expect(savedEvent!.bridgingFeesIncurred.toString()).to.equal("0"); expect(savedEvent!.evmAmountSponsored.toString()).to.equal("0"); }).timeout(20000); + + it("should process a block range and store FallbackHyperEVMFlowCompleted event for OFT", async () => { + const transactionHash = + "0x05ccdbd44e8ffbed8f057762f40dee73fb218049347705d88f839dfe3c368c52"; + const blockNumber = 17917691; + 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 fallbackHyperEVMFlowCompletedRepo = dataSource.getRepository( + entities.FallbackHyperEVMFlowCompleted, + ); + const savedEvent = await fallbackHyperEVMFlowCompletedRepo.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( + "0x0000000000000000000000000000000000000000000000000000000069041bd4", + ); + expect(savedEvent!.finalRecipient).to.equal( + "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + ); + expect(savedEvent!.finalToken).to.equal( + "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", + ); + expect(savedEvent!.evmAmountIn.toString()).to.equal("1005000"); + expect(savedEvent!.bridgingFeesIncurred.toString()).to.equal("0"); + expect(savedEvent!.evmAmountSponsored.toString()).to.equal("0"); + }).timeout(20000); }); diff --git a/packages/indexer/src/database/OftRepository.ts b/packages/indexer/src/database/OftRepository.ts index b1948999..e7079a02 100644 --- a/packages/indexer/src/database/OftRepository.ts +++ b/packages/indexer/src/database/OftRepository.ts @@ -29,6 +29,8 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { oftSentEvents, oftReceivedEvents, simpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents, ] = await Promise.all([ this.deleteUnfinalisedEvents( chainId, @@ -48,12 +50,26 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { lastFinalisedBlock, entities.SimpleTransferFlowCompleted, ), + this.deleteUnfinalisedEvents( + chainId, + chainIdColumn, + lastFinalisedBlock, + entities.FallbackHyperEVMFlowCompleted, + ), + this.deleteUnfinalisedEvents( + chainId, + chainIdColumn, + lastFinalisedBlock, + entities.ArbitraryActionsExecuted, + ), ]); return { oftSentEvents, oftReceivedEvents, simpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents, }; } diff --git a/packages/indexer/src/web3/EventDecoder.ts b/packages/indexer/src/web3/EventDecoder.ts index c84684d8..b400c0a1 100644 --- a/packages/indexer/src/web3/EventDecoder.ts +++ b/packages/indexer/src/web3/EventDecoder.ts @@ -102,7 +102,7 @@ export class EventDecoder { const eventTopic = "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036"; const eventAbi = ["event MessageSent (bytes message)"]; - let events: MessageSentLog[] = this.decodeTransactionReceiptLogs( + let events: MessageSentLog[] = EventDecoder.decodeTransactionReceiptLogs( receipt, eventTopic, eventAbi, @@ -123,11 +123,8 @@ export class EventDecoder { const eventAbi = [ "event MintAndWithdraw(address indexed mintRecipient, uint256 amount, address indexed mintToken, uint256 feeCollected)", ]; - let events: MintAndWithdrawLog[] = this.decodeTransactionReceiptLogs( - receipt, - eventTopic, - eventAbi, - ); + let events: MintAndWithdrawLog[] = + EventDecoder.decodeTransactionReceiptLogs(receipt, eventTopic, eventAbi); if (contractAddress) { events = events.filter((event) => event.address === contractAddress); } @@ -148,7 +145,7 @@ export class EventDecoder { ]; let events: SponsoredDepositForBurnLog[] = - this.decodeTransactionReceiptLogs(receipt, eventTopic, eventAbi); + EventDecoder.decodeTransactionReceiptLogs(receipt, eventTopic, eventAbi); if (contractAddress) { events = events.filter((event) => event.address === contractAddress); } @@ -167,11 +164,8 @@ export class EventDecoder { "event SponsoredOFTSend(bytes32 indexed quoteNonce, address indexed originSender, bytes32 indexed finalRecipient, bytes32 destinationHandler, uint256 quoteDeadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, bytes32 finalToken, bytes sig)", ]; - let events: SponsoredOFTSendLog[] = this.decodeTransactionReceiptLogs( - receipt, - eventTopic, - eventAbi, - ); + let events: SponsoredOFTSendLog[] = + EventDecoder.decodeTransactionReceiptLogs(receipt, eventTopic, eventAbi); if (contractAddress) { events = events.filter((event) => event.address === contractAddress); } @@ -200,7 +194,7 @@ export class EventDecoder { "event SimpleTransferFlowCompleted(bytes32 indexed quoteNonce,address indexed finalRecipient,address indexed finalToken,uint256 evmAmountIn,uint256 bridgingFeesIncurred,uint256 evmAmountSponsored)", ]; let events: SimpleTransferFlowCompletedLog[] = - this.decodeTransactionReceiptLogs(receipt, eventTopic, eventAbi); + EventDecoder.decodeTransactionReceiptLogs(receipt, eventTopic, eventAbi); if (contractAddress) { events = events.filter((event) => event.address === contractAddress); } @@ -217,7 +211,7 @@ export class EventDecoder { const eventAbi = [ "event ArbitraryActionsExecuted(bytes32 indexed quoteNonce, address indexed initialToken, uint256 initialAmount, address indexed finalToken, uint256 finalAmount)", ]; - let events: any[] = this.decodeTransactionReceiptLogs( + let events: any[] = EventDecoder.decodeTransactionReceiptLogs( receipt, eventTopic, eventAbi, @@ -238,7 +232,7 @@ export class EventDecoder { const eventAbi = [ "event FallbackHyperEVMFlowCompleted(bytes32 indexed quoteNonce, address indexed finalRecipient, address indexed finalToken, uint256 evmAmountIn, uint256 bridgingFeesIncurred, uint256 evmAmountSponsored)", ]; - let events: any[] = this.decodeTransactionReceiptLogs( + let events: any[] = EventDecoder.decodeTransactionReceiptLogs( receipt, eventTopic, eventAbi,