diff --git a/packages/indexer-database/src/entities/evm/FallbackHyperEVMFlowCompleted.ts b/packages/indexer-database/src/entities/evm/FallbackHyperEVMFlowCompleted.ts new file mode 100644 index 00000000..2efa8650 --- /dev/null +++ b/packages/indexer-database/src/entities/evm/FallbackHyperEVMFlowCompleted.ts @@ -0,0 +1,74 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + Unique, + Index, + CreateDateColumn, + DeleteDateColumn, +} from "typeorm"; + +@Entity({ schema: "evm" }) +@Unique("UK_fallback_hyper_evm_flow_completed_chain_block_tx_log", [ + "chainId", + "blockNumber", + "transactionHash", + "logIndex", +]) +@Index("IX_fallback_hyper_evm_flow_completed_chainId", ["chainId"]) +@Index("IX_fallback_hyper_evm_flow_completed_quoteNonce", ["quoteNonce"]) +@Index("IX_fallback_hyper_evm_flow_completed_blockNumber", ["blockNumber"]) +@Index("IX_fallback_hyper_evm_flow_completed_blockTimeStamp", [ + "blockTimestamp", +]) +@Index("IX_fallback_hyper_evm_flow_completed_deletedAt", ["deletedAt"]) +@Index("IX_fallback_hyper_evm_flow_completed_finalised", ["finalised"]) +export class FallbackHyperEVMFlowCompleted { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: "bigint" }) + chainId: string; + + @Column({ nullable: true }) + quoteNonce: string; + + @Column({ type: "varchar" }) + finalRecipient: string; + + @Column({ type: "varchar" }) + finalToken: string; + + @Column({ type: "numeric" }) + evmAmountIn: string; + + @Column({ type: "numeric" }) + bridgingFeesIncurred: string; + + @Column({ type: "numeric" }) + evmAmountSponsored: string; + + @Column() + blockNumber: number; + + @Column() + transactionHash: string; + + @Column() + transactionIndex: number; + + @Column() + logIndex: number; + + @Column("boolean") + finalised: boolean; + + @Column() + blockTimestamp: Date; + + @CreateDateColumn() + createdAt: Date; + + @DeleteDateColumn({ nullable: true }) + deletedAt?: Date; +} diff --git a/packages/indexer-database/src/entities/index.ts b/packages/indexer-database/src/entities/index.ts index 4f616fa8..1d48c39a 100644 --- a/packages/indexer-database/src/entities/index.ts +++ b/packages/indexer-database/src/entities/index.ts @@ -50,3 +50,4 @@ export * from "./evm/SponsoredOFTSend"; // HyperEVM export * from "./evm/SimpleTransferFlowCompleted"; export * from "./evm/ArbitraryActionsExecuted"; +export * from "./evm/FallbackHyperEVMFlowCompleted"; diff --git a/packages/indexer-database/src/main.ts b/packages/indexer-database/src/main.ts index 085b250d..90f658b7 100644 --- a/packages/indexer-database/src/main.ts +++ b/packages/indexer-database/src/main.ts @@ -79,6 +79,7 @@ export const createDataSource = (config: DatabaseConfig): DataSource => { // HyperEVM entities.SimpleTransferFlowCompleted, entities.ArbitraryActionsExecuted, + entities.FallbackHyperEVMFlowCompleted, ], migrationsTableName: "_migrations", migrations: ["migrations/*.ts"], diff --git a/packages/indexer-database/src/migrations/1762818039419-FallbackHyperEVMFlowCompleted.ts b/packages/indexer-database/src/migrations/1762818039419-FallbackHyperEVMFlowCompleted.ts new file mode 100644 index 00000000..51961833 --- /dev/null +++ b/packages/indexer-database/src/migrations/1762818039419-FallbackHyperEVMFlowCompleted.ts @@ -0,0 +1,74 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class FallbackHyperEVMFlowCompleted1762818039419 + implements MigrationInterface +{ + name = "FallbackHyperEVMFlowCompleted1762818039419"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "evm"."fallback_hyper_evm_flow_completed" ( + "id" SERIAL NOT NULL, + "chainId" bigint NOT NULL, + "quoteNonce" character varying, + "finalRecipient" character varying NOT NULL, + "finalToken" character varying NOT NULL, + "evmAmountIn" numeric NOT NULL, + "bridgingFeesIncurred" numeric NOT NULL, + "evmAmountSponsored" numeric NOT NULL, + "blockNumber" integer NOT NULL, + "transactionHash" character varying NOT NULL, + "transactionIndex" integer NOT NULL, + "logIndex" integer NOT NULL, + "finalised" boolean NOT NULL, + "blockTimestamp" TIMESTAMP NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deletedAt" TIMESTAMP, + CONSTRAINT "PK_fallback_hyper_evm_flow_completed" PRIMARY KEY ("id"), + CONSTRAINT "UK_fallback_hyper_evm_flow_completed_chain_block_tx_log" UNIQUE ("chainId", "blockNumber", "transactionHash", "logIndex") + )`, + ); + await queryRunner.query( + `CREATE INDEX "IX_fallback_hyper_evm_flow_completed_chainId" ON "evm"."fallback_hyper_evm_flow_completed" ("chainId") `, + ); + await queryRunner.query( + `CREATE INDEX "IX_fallback_hyper_evm_flow_completed_quoteNonce" ON "evm"."fallback_hyper_evm_flow_completed" ("quoteNonce") `, + ); + await queryRunner.query( + `CREATE INDEX "IX_fallback_hyper_evm_flow_completed_blockNumber" ON "evm"."fallback_hyper_evm_flow_completed" ("blockNumber") `, + ); + await queryRunner.query( + `CREATE INDEX "IX_fallback_hyper_evm_flow_completed_finalised" ON "evm"."fallback_hyper_evm_flow_completed" ("finalised") `, + ); + await queryRunner.query( + `CREATE INDEX "IX_fallback_hyper_evm_flow_completed_blockTimestamp" ON "evm"."fallback_hyper_evm_flow_completed" ("blockTimestamp") `, + ); + await queryRunner.query( + `CREATE INDEX "IX_fallback_hyper_evm_flow_completed_deletedAt" ON "evm"."fallback_hyper_evm_flow_completed" ("deletedAt") `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX "evm"."IX_fallback_hyper_evm_flow_completed_deletedAt"`, + ); + await queryRunner.query( + `DROP INDEX "evm"."IX_fallback_hyper_evm_flow_completed_blockTimestamp"`, + ); + await queryRunner.query( + `DROP INDEX "evm"."IX_fallback_hyper_evm_flow_completed_blockNumber"`, + ); + await queryRunner.query( + `DROP INDEX "evm"."IX_fallback_hyper_evm_flow_completed_quoteNonce"`, + ); + await queryRunner.query( + `DROP INDEX "evm"."IX_fallback_hyper_evm_flow_completed_finalised"`, + ); + await queryRunner.query( + `DROP INDEX "evm"."IX_fallback_hyper_evm_flow_completed_chainId"`, + ); + await queryRunner.query( + `DROP TABLE "evm"."fallback_hyper_evm_flow_completed"`, + ); + } +} 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 eeccf3ed..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: { @@ -22,12 +21,13 @@ export interface ArbitraryActionsExecutedLog 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", -}; - -// Taken from https://hyperevmscan.io/tx/0x869d1df5f1e7b6b91a824d8e2b455ac48d1f26f0b5f2823c96df391eb75dff34#eventlog#8 -export const ARBITRARY_EVM_FLOW_EXECUTOR_ADDRESS: { [key: number]: string } = { - [CHAIN_IDs.HYPEREVM]: "0x7B164050BBC8e7ef3253e7db0D74b713Ba3F1c95", -}; +export interface FallbackHyperEVMFlowCompletedLog extends providers.Log { + args: { + quoteNonce: string; + finalRecipient: string; + finalToken: string; + evmAmountIn: BigNumber; + bridgingFeesIncurred: BigNumber; + evmAmountSponsored: BigNumber; + }; +} diff --git a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts index 06235410..e58254c0 100644 --- a/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts +++ b/packages/indexer/src/data-indexing/service/CCTPIndexerDataHandler.ts @@ -5,10 +5,9 @@ import { CHAIN_IDs } from "@across-protocol/constants"; import { formatFromAddressToChainFormat, isTestnet } from "../../utils"; import { BlockRange, - HYPERCORE_FLOW_EXECUTOR_ADDRESS, SimpleTransferFlowCompletedLog, ArbitraryActionsExecutedLog, - ARBITRARY_EVM_FLOW_EXECUTOR_ADDRESS, + FallbackHyperEVMFlowCompletedLog, } from "../model"; import { IndexerDataHandler } from "./IndexerDataHandler"; import { EventDecoder } from "../../web3/EventDecoder"; @@ -40,6 +39,14 @@ import { isHypercoreWithdraw, } from "../adapter/cctp-v2/service"; import { entities, SaveQueryResult } from "@repo/indexer-database"; +import { + formatFallbackHyperEVMFlowCompletedEvent, + formatSimpleTransferFlowCompletedEvent, +} from "./hyperEvmExecutor"; +import { + formatAndSaveEvents, + getEventsFromTransactionReceipts, +} from "./eventProcessing"; export type EvmBurnEventsPair = { depositForBurn: DepositForBurnEvent; @@ -55,6 +62,7 @@ export type FetchEventsResult = { sponsoredBurnEvents: SponsoredDepositForBurnLog[]; simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[]; arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[]; + fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[]; blocks: Record; transactionReceipts: Record; transactions: Record; @@ -71,6 +79,7 @@ export type StoreEventsResult = { savedSponsoredBurnEvents: SaveQueryResult[]; savedSimpleTransferFlowCompletedEvents: SaveQueryResult[]; savedArbitraryActionsExecutedEvents: SaveQueryResult[]; + savedFallbackHyperEVMFlowCompletedEvents: SaveQueryResult[]; }; // Taken from https://developers.circle.com/cctp/evm-smart-contracts @@ -85,6 +94,12 @@ const MESSAGE_TRANSMITTER_ADDRESS_MAINNET: string = const MESSAGE_TRANSMITTER_ADDRESS_TESTNET: string = "0xE737e5cEBEEBa77EFE34D4aa090756590b1CE275"; +// TODO: Update this address once the contract is deployed +const SPONSORED_CCTP_DST_PERIPHERY_ADDRESS: { [key: number]: string } = { + // Taken from https://hyperevmscan.io/address/0x7B164050BBC8e7ef3253e7db0D74b713Ba3F1c95#code + [CHAIN_IDs.HYPEREVM]: "0x7B164050BBC8e7ef3253e7db0D74b713Ba3F1c95", +}; + // TODO: Update this address once the contract is deployed const SPONSORED_CCTP_SRC_PERIPHERY_ADDRESS: { [key: number]: string } = { [CHAIN_IDs.ARBITRUM_SEPOLIA]: "0x79176E2E91c77b57AC11c6fe2d2Ab2203D87AF85", @@ -161,10 +176,8 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { ): Promise { const sponsoredCCTPSrcPeripheryAddress = SPONSORED_CCTP_SRC_PERIPHERY_ADDRESS[this.chainId]; - const hyperEvmExecutorAddress = - HYPERCORE_FLOW_EXECUTOR_ADDRESS[this.chainId]; - const arbitraryEvmFlowExecutorAddress = - ARBITRARY_EVM_FLOW_EXECUTOR_ADDRESS[this.chainId]; + const sponsoredCCTPDstPeripheryAddress = + SPONSORED_CCTP_DST_PERIPHERY_ADDRESS[this.chainId]; const tokenMessengerAddress = isTestnet(this.chainId) ? TOKEN_MESSENGER_ADDRESS_TESTNET @@ -237,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, @@ -276,21 +290,27 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { let simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = []; - if (hyperEvmExecutorAddress) { - simpleTransferFlowCompletedEvents = - this.getSimpleTransferFlowCompletedEventsFromTransactionReceipts( - filteredMessageReceivedTxReceipts, - hyperEvmExecutorAddress, - ); - } - let arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[] = []; - if (arbitraryEvmFlowExecutorAddress) { - arbitraryActionsExecutedEvents = - this.getArbitraryActionsExecutedEventsFromTransactionReceipts( - filteredMessageReceivedTxReceipts, - arbitraryEvmFlowExecutorAddress, - ); + let fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[] = + []; + if (sponsoredCCTPDstPeripheryAddress) { + simpleTransferFlowCompletedEvents = getEventsFromTransactionReceipts( + filteredMessageReceivedTxReceipts, + sponsoredCCTPDstPeripheryAddress, + EventDecoder.decodeSimpleTransferFlowCompletedEvents, + ); + + arbitraryActionsExecutedEvents = getEventsFromTransactionReceipts( + filteredMessageReceivedTxReceipts, + sponsoredCCTPDstPeripheryAddress, + EventDecoder.decodeArbitraryActionsExecutedEvents, + ); + + fallbackHyperEVMFlowCompletedEvents = getEventsFromTransactionReceipts( + filteredMessageReceivedTxReceipts, + sponsoredCCTPDstPeripheryAddress, + EventDecoder.decodeFallbackHyperEVMFlowCompletedEvents, + ); } this.runChecks(burnEvents, mintEvents); @@ -314,6 +334,7 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { sponsoredBurnEvents, simpleTransferFlowCompletedEvents, arbitraryActionsExecutedEvents, + fallbackHyperEVMFlowCompletedEvents, blocks, transactionReceipts, transactions: depositForBurnTransactions, @@ -392,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, @@ -496,50 +471,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, - ) { - 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 async getTransactionsReceipts(uniqueTransactionHashes: string[]) { const transactionReceipts = await Promise.all( uniqueTransactionHashes.map(async (txHash) => { @@ -597,6 +528,7 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { sponsoredBurnEvents, simpleTransferFlowCompletedEvents, arbitraryActionsExecutedEvents, + fallbackHyperEVMFlowCompletedEvents, blocks, } = events; const blocksTimestamps = this.getBlocksTimestamps(blocks); @@ -613,13 +545,19 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { const chainAgnosticSponsoredBurnEvents = sponsoredBurnEvents.map((event) => this.convertSponsoredDepositForBurnToChainAgnostic(event), ); - + const primaryKeyColumns = [ + "chainId", + "blockNumber", + "transactionHash", + "logIndex", + ]; const [ savedBurnEvents, savedMintEvents, savedSponsoredBurnEvents, savedSimpleTransferFlowCompletedEvents, savedArbitraryActionsExecutedEvents, + savedFallbackHyperEVMFlowCompletedEvents, ] = await Promise.all([ this.cctpRepository.formatAndSaveBurnEvents( chainAgnosticBurnEvents, @@ -639,11 +577,15 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), - this.cctpRepository.formatAndSaveSimpleTransferFlowCompletedEvents( + formatAndSaveEvents( + this.cctpRepository, simpleTransferFlowCompletedEvents, lastFinalisedBlock, this.chainId, blocksTimestamps, + formatSimpleTransferFlowCompletedEvent, + entities.SimpleTransferFlowCompleted, + primaryKeyColumns as (keyof entities.SimpleTransferFlowCompleted)[], ), this.cctpRepository.formatAndSaveArbitraryActionsExecutedEvents( arbitraryActionsExecutedEvents, @@ -651,6 +593,16 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), + formatAndSaveEvents( + this.cctpRepository, + fallbackHyperEVMFlowCompletedEvents, + lastFinalisedBlock, + this.chainId, + blocksTimestamps, + formatFallbackHyperEVMFlowCompletedEvent, + entities.FallbackHyperEVMFlowCompleted, + primaryKeyColumns as (keyof entities.FallbackHyperEVMFlowCompleted)[], + ), ]); return { @@ -659,6 +611,7 @@ export class CCTPIndexerDataHandler implements IndexerDataHandler { savedSponsoredBurnEvents, savedSimpleTransferFlowCompletedEvents, savedArbitraryActionsExecutedEvents, + savedFallbackHyperEVMFlowCompletedEvents, }; } diff --git a/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts b/packages/indexer/src/data-indexing/service/OFTIndexerDataHandler.ts index d5ae44a4..194ac443 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 { + ArbitraryActionsExecutedLog, + BlockRange, + FallbackHyperEVMFlowCompletedLog, + SimpleTransferFlowCompletedLog, +} from "../model"; import { IndexerDataHandler } from "./IndexerDataHandler"; import { O_ADAPTER_UPGRADEABLE_ABI } from "../adapter/oft/abis"; import { @@ -18,26 +23,50 @@ 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 { + formatAndSaveEvents, + getEventsFromTransactionReceipts, +} from "./eventProcessing"; +import { + formatArbitraryActionsExecutedEvent, + formatFallbackHyperEVMFlowCompletedEvent, + formatSimpleTransferFlowCompletedEvent, +} from "./hyperEvmExecutor"; +import { CHAIN_IDs } from "@across-protocol/constants"; export type FetchEventsResult = { oftSentEvents: OFTSentEvent[]; oftReceivedEvents: OFTReceivedEvent[]; sponsoredOFTSendEvents: SponsoredOFTSendLog[]; + simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[]; + fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[]; + arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[]; blocks: Record; }; export type StoreEventsResult = { oftSentEvents: SaveQueryResult[]; oftReceivedEvents: SaveQueryResult[]; sponsoredOFTSendEvents: SaveQueryResult[]; + simpleTransferFlowCompletedEvents: SaveQueryResult[]; + fallbackHyperEVMFlowCompletedEvents: SaveQueryResult[]; + arbitraryActionsExecutedEvents: 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 +146,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 +162,7 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { blockRange.to, ) as Promise, ]); + let blockHashes = []; const oftSentTransactions = await this.getTransactions([ ...new Set(oftSentEvents.map((event) => event.transactionHash)), ]); @@ -144,22 +176,63 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { await this.getTransactionsReceipts([ ...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, + ); + } + + let simpleTransferFlowCompletedEvents: SimpleTransferFlowCompletedLog[] = + []; + let fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[] = + []; + let arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[] = []; + 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), + ); + 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), + ); + } } - const blocks = await this.getBlocks([ - ...new Set([ - ...filteredOftSentEvents.map((event) => event.blockHash), - ...filteredOftReceivedEvents.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 +249,9 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { oftSentEvents: filteredOftSentEvents, oftReceivedEvents: filteredOftReceivedEvents, sponsoredOFTSendEvents, + simpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents, blocks, }; } @@ -185,13 +261,29 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { lastFinalisedBlock: number, tokenAddress: string, ): Promise { - const { blocks, oftReceivedEvents, oftSentEvents, sponsoredOFTSendEvents } = - events; + const { + blocks, + oftReceivedEvents, + 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, @@ -213,12 +305,46 @@ export class OFTIndexerDataHandler implements IndexerDataHandler { this.chainId, blocksTimestamps, ), + 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)[], + ), ]); return { oftSentEvents: savedOftSentEvents, oftReceivedEvents: savedOftReceivedEvents, sponsoredOFTSendEvents: savedSponsoredOFTSendEvents, + simpleTransferFlowCompletedEvents: savedSimpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents: + savedFallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents: savedArbitraryActionsExecutedEvents, }; } @@ -254,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[], ) { @@ -317,13 +423,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/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 new file mode 100644 index 00000000..6df8df39 --- /dev/null +++ b/packages/indexer/src/data-indexing/service/hyperEvmExecutor.ts @@ -0,0 +1,95 @@ +import { + ArbitraryActionsExecutedLog, + FallbackHyperEVMFlowCompletedLog, + SimpleTransferFlowCompletedLog, +} from "../model"; +import { entities } from "@repo/indexer-database"; + +/** + * @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 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, +}); + +/** + * @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 const formatArbitraryActionsExecutedEvent = ( + event: ArbitraryActionsExecutedLog, + 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, + initialToken: event.args.initialToken, + initialAmount: event.args.initialAmount.toString(), + finalToken: event.args.finalToken, + finalAmount: event.args.finalAmount.toString(), + finalised, +}); + +/** + * @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/CCTPIndexerDataHandler.integration.test.ts b/packages/indexer/src/data-indexing/tests/CCTPIndexerDataHandler.integration.test.ts index dff302ca..96ebfd91 100644 --- a/packages/indexer/src/data-indexing/tests/CCTPIndexerDataHandler.integration.test.ts +++ b/packages/indexer/src/data-indexing/tests/CCTPIndexerDataHandler.integration.test.ts @@ -116,9 +116,9 @@ describe("CCTPIndexerDataHandler", () => { it("should fetch and store SimpleTransferFlowCompleted event in the database", async () => { const transactionHash = - "0x1bf0dc091249341d0e91380b1c1d7dca683ab1b6773f7fb011b71a3d017a8fc9"; - const blockNumber = 36200188; - setupTestForChainId(CHAIN_IDs.HYPEREVM_TESTNET); + "0x0e07cf92929a5e3c9d18ba28c71bf50b678d357eb9f433ed305ac6ab958f0abb"; + const blockNumber = 18541961; + setupTestForChainId(CHAIN_IDs.HYPEREVM); const blockRange: BlockRange = { from: blockNumber, @@ -181,6 +181,46 @@ describe("CCTPIndexerDataHandler", () => { expect(savedEvent!.finalAmount.toString()).to.equal("99990"); }).timeout(10000); + it("should fetch and store FallbackHyperEVMFlowCompleted event in the database", async () => { + const transactionHash = + "0xb940059314450f7f7cb92972182cdf3f5fb5f54aab27c28b7426a78e6fb32d02"; + const blockNumber = 18913313; + setupTestForChainId(CHAIN_IDs.HYPEREVM); + + const blockRange: BlockRange = { + from: blockNumber, + to: blockNumber, + }; + + // We need to stub the filterMintTransactions method to avoid filtering out our test transaction + sinon.stub(handler as any, "filterMintTransactions").returnsArg(0); + + await handler.processBlockRange(blockRange, blockNumber); + + const fallbackHyperEVMFlowCompletedRepository = dataSource.getRepository( + entities.FallbackHyperEVMFlowCompleted, + ); + const savedEvent = await fallbackHyperEVMFlowCompletedRepository.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( + "0xd4731c4ab33b3a364d599940d9ba46df41f6a75233a361e2d312e072540ed184", + ); + expect(savedEvent!.finalRecipient).to.equal( + "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D", + ); + expect(savedEvent!.finalToken).to.equal( + "0xb88339CB7199b77E23DB6E890353E22632Ba630f", + ); + expect(savedEvent!.evmAmountIn.toString()).to.equal("999900"); + expect(savedEvent!.bridgingFeesIncurred.toString()).to.equal("100"); + expect(savedEvent!.evmAmountSponsored.toString()).to.equal("0"); + }).timeout(10000); + it("should fetch hypercore withdraw data and be able to decode the hookData", async () => { const transactionHash = "0x13b9b9dfb7f8804d385db96454d094791b8ab618556fcd37fb17c4b206499871"; 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..74b887e3 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,78 @@ 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); + + 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/CctpRepository.ts b/packages/indexer/src/database/CctpRepository.ts index 20679aef..998bd3a6 100644 --- a/packages/indexer/src/database/CctpRepository.ts +++ b/packages/indexer/src/database/CctpRepository.ts @@ -1,15 +1,12 @@ import winston from "winston"; import { ethers } from "ethers"; import * as across from "@across-protocol/sdk"; -import { CHAIN_IDs } from "@across-protocol/constants"; - import { DataSource, entities, utils as dbUtils, SaveQueryResult, } from "@repo/indexer-database"; - import { DepositForBurnWithBlock, MessageSentWithBlock, @@ -26,6 +23,7 @@ import { formatFromAddressToChainFormat } from "../utils"; import { SimpleTransferFlowCompletedLog, ArbitraryActionsExecutedLog, + FallbackHyperEVMFlowCompletedLog, } from "../data-indexing/model"; // Chain-agnostic types - both EVM and SVM handlers must convert to these @@ -61,6 +59,7 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { sponsoredDepositForBurnEvents, simpleTransferFlowCompletedEvents, arbitraryActionsExecutedEvents, + fallbackHyperEVMFlowCompletedEvents, ] = await Promise.all([ this.deleteUnfinalisedEvents( chainId, @@ -104,6 +103,12 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { lastFinalisedBlock, entities.ArbitraryActionsExecuted, ), + this.deleteUnfinalisedEvents( + chainId, + chainIdColumn, + lastFinalisedBlock, + entities.FallbackHyperEVMFlowCompleted, + ), ]); return { @@ -114,6 +119,7 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { sponsoredDepositForBurnEvents, simpleTransferFlowCompletedEvents, arbitraryActionsExecutedEvents, + fallbackHyperEVMFlowCompletedEvents, }; } @@ -157,14 +163,41 @@ 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 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( + arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[], lastFinalisedBlock: number, chainId: number, blockDates: Record, ) { - const formattedEvents: Partial[] = - simpleTransferFlowCompletedEvents.map((event) => { + const formattedEvents: Partial[] = + arbitraryActionsExecutedEvents.map((event) => { return { blockNumber: event.blockNumber, logIndex: event.logIndex, @@ -173,11 +206,10 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { 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(), + initialToken: event.args.initialToken, + initialAmount: event.args.initialAmount.toString(), + finalToken: event.args.finalToken, + finalAmount: event.args.finalAmount.toString(), finalised: event.blockNumber <= lastFinalisedBlock, }; }); @@ -185,8 +217,8 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { const chunkedEvents = across.utils.chunk(formattedEvents, this.chunkSize); const savedEvents = await Promise.all( chunkedEvents.map((eventsChunk) => - this.saveAndHandleFinalisationBatch( - entities.SimpleTransferFlowCompleted, + this.saveAndHandleFinalisationBatch( + entities.ArbitraryActionsExecuted, eventsChunk, ["chainId", "blockNumber", "transactionHash", "logIndex"], [], @@ -197,14 +229,14 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { return result; } - public async formatAndSaveArbitraryActionsExecutedEvents( - arbitraryActionsExecutedEvents: ArbitraryActionsExecutedLog[], + public async formatAndSaveFallbackHyperEVMFlowCompletedEvents( + fallbackHyperEVMFlowCompletedEvents: FallbackHyperEVMFlowCompletedLog[], lastFinalisedBlock: number, chainId: number, blockDates: Record, ) { - const formattedEvents: Partial[] = - arbitraryActionsExecutedEvents.map((event) => { + const formattedEvents: Partial[] = + fallbackHyperEVMFlowCompletedEvents.map((event) => { return { blockNumber: event.blockNumber, logIndex: event.logIndex, @@ -213,10 +245,11 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { blockTimestamp: blockDates[event.blockNumber]!, chainId: chainId.toString(), quoteNonce: event.args.quoteNonce, - initialToken: event.args.initialToken, - initialAmount: event.args.initialAmount.toString(), + finalRecipient: event.args.finalRecipient, finalToken: event.args.finalToken, - finalAmount: event.args.finalAmount.toString(), + evmAmountIn: event.args.evmAmountIn.toString(), + bridgingFeesIncurred: event.args.bridgingFeesIncurred.toString(), + evmAmountSponsored: event.args.evmAmountSponsored.toString(), finalised: event.blockNumber <= lastFinalisedBlock, }; }); @@ -224,8 +257,8 @@ export class CCTPRepository extends dbUtils.BlockchainEventRepository { const chunkedEvents = across.utils.chunk(formattedEvents, this.chunkSize); const savedEvents = await Promise.all( chunkedEvents.map((eventsChunk) => - this.saveAndHandleFinalisationBatch( - entities.ArbitraryActionsExecuted, + this.saveAndHandleFinalisationBatch( + entities.FallbackHyperEVMFlowCompleted, eventsChunk, ["chainId", "blockNumber", "transactionHash", "logIndex"], [], @@ -236,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..e7079a02 100644 --- a/packages/indexer/src/database/OftRepository.ts +++ b/packages/indexer/src/database/OftRepository.ts @@ -25,7 +25,13 @@ export class OftRepository extends dbUtils.BlockchainEventRepository { lastFinalisedBlock: number, ) { const chainIdColumn = "chainId"; - const [oftSentEvents, oftReceivedEvents] = await Promise.all([ + const [ + oftSentEvents, + oftReceivedEvents, + simpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents, + ] = await Promise.all([ this.deleteUnfinalisedEvents( chainId, chainIdColumn, @@ -38,41 +44,32 @@ 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, + chainIdColumn, + lastFinalisedBlock, + entities.SimpleTransferFlowCompleted, ), - this.formatAndSaveOftReceivedEvents( - oftReceivedEvents, + this.deleteUnfinalisedEvents( + chainId, + chainIdColumn, lastFinalisedBlock, + entities.FallbackHyperEVMFlowCompleted, + ), + this.deleteUnfinalisedEvents( chainId, - blockDates, - tokenAddress, + chainIdColumn, + lastFinalisedBlock, + entities.ArbitraryActionsExecuted, ), ]); + return { - savedOftSentEvents, - savedOftReceivedEvents, + oftSentEvents, + oftReceivedEvents, + simpleTransferFlowCompletedEvents, + fallbackHyperEVMFlowCompletedEvents, + arbitraryActionsExecutedEvents, }; } @@ -80,7 +77,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 +85,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 +137,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 +152,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 +186,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 735d399c..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); } @@ -179,6 +173,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, @@ -190,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); } @@ -207,7 +211,28 @@ 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, + ); + if (contractAddress) { + events = events.filter((event) => event.address === contractAddress); + } + return events; + } + + static decodeFallbackHyperEVMFlowCompletedEvents( + receipt: ethers.providers.TransactionReceipt, + contractAddress?: string, + ) { + // Taken from https://hyperevmscan.io/tx/0xb940059314450f7f7cb92972182cdf3f5fb5f54aab27c28b7426a78e6fb32d02#eventlog#25 + const eventTopic = + "0x4755f239bb1b047245415cb917deced72a3ca8baebcef109c396ff332ea6f50f"; + const eventAbi = [ + "event FallbackHyperEVMFlowCompleted(bytes32 indexed quoteNonce, address indexed finalRecipient, address indexed finalToken, uint256 evmAmountIn, uint256 bridgingFeesIncurred, uint256 evmAmountSponsored)", + ]; + let events: any[] = EventDecoder.decodeTransactionReceiptLogs( receipt, eventTopic, eventAbi,