diff --git a/packages/indexer-database/src/entities/evm/SponsoredDepositForBurn.ts b/packages/indexer-database/src/entities/evm/SponsoredDepositForBurn.ts index b3b673f4..5bb6d693 100644 --- a/packages/indexer-database/src/entities/evm/SponsoredDepositForBurn.ts +++ b/packages/indexer-database/src/entities/evm/SponsoredDepositForBurn.ts @@ -7,6 +7,7 @@ import { DeleteDateColumn, Unique, } from "typeorm"; +import { DataSourceType } from "../../model"; @Entity({ schema: "evm" }) @Unique("UK_sponsoredDepositForBurn_chain_block_tx_log", [ @@ -75,6 +76,9 @@ export class SponsoredDepositForBurn { @CreateDateColumn() createdAt: Date; + @Column({ type: "simple-enum", enum: DataSourceType, nullable: true }) + dataSource?: DataSourceType; + @DeleteDateColumn({ nullable: true }) deletedAt?: Date; } diff --git a/packages/indexer-database/src/migrations/1767709500000-AddDataSourceToSponsoredDepositForBurn.ts b/packages/indexer-database/src/migrations/1767709500000-AddDataSourceToSponsoredDepositForBurn.ts new file mode 100644 index 00000000..3e87023b --- /dev/null +++ b/packages/indexer-database/src/migrations/1767709500000-AddDataSourceToSponsoredDepositForBurn.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddDataSourceToSponsoredDepositForBurn1767709500000 + implements MigrationInterface +{ + name = "AddDataSourceToSponsoredDepositForBurn1767709500000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "evm"."sponsored_deposit_for_burn" ADD "dataSource" "evm"."datasource_type_enum" DEFAULT 'polling'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "evm"."sponsored_deposit_for_burn" DROP COLUMN "dataSource"`, + ); + } +} diff --git a/packages/indexer/src/data-indexing/model/abis.ts b/packages/indexer/src/data-indexing/model/abis.ts index 1a3afc1c..ea44579f 100644 --- a/packages/indexer/src/data-indexing/model/abis.ts +++ b/packages/indexer/src/data-indexing/model/abis.ts @@ -27,3 +27,7 @@ export const SWAP_FLOW_FINALIZED_ABI = [ export const SWAP_FLOW_INITIALIZED_ABI = [ "event SwapFlowInitialized(bytes32 indexed quoteNonce,address indexed finalRecipient,address indexed finalToken,uint256 evmAmountIn,uint256 bridgingFeesIncurred,uint256 coreAmountIn,uint64 minAmountToSend,uint64 maxAmountToSend)", ]; + +export const SPONSORED_DEPOSIT_FOR_BURN_ABI = [ + "event SponsoredDepositForBurn(bytes32 indexed quoteNonce, address indexed originSender, bytes32 indexed finalRecipient, uint256 quoteDeadline, uint256 maxBpsToSponsor, uint256 maxUserSlippageBps, bytes32 finalToken, bytes signature)", +]; diff --git a/packages/indexer/src/data-indexing/model/eventTypes.ts b/packages/indexer/src/data-indexing/model/eventTypes.ts index bf8d314c..cc1ac29e 100644 --- a/packages/indexer/src/data-indexing/model/eventTypes.ts +++ b/packages/indexer/src/data-indexing/model/eventTypes.ts @@ -61,10 +61,23 @@ export interface SwapFlowInitializedArgs { maxAmountToSend: bigint; } +export interface SponsoredDepositForBurnArgs { + quoteNonce: `0x${string}`; + originSender: `0x${string}`; + finalRecipient: `0x${string}`; + quoteDeadline: bigint; + maxBpsToSponsor: bigint; + maxUserSlippageBps: bigint; + finalToken: `0x${string}`; + signature: `0x${string}`; + destinationChainId?: number; +} + export type EventArgs = | DepositForBurnArgs | MessageSentArgs | MessageReceivedArgs | MintAndWithdrawArgs | SwapFlowFinalizedArgs - | SwapFlowInitializedArgs; + | SwapFlowInitializedArgs + | SponsoredDepositForBurnArgs; diff --git a/packages/indexer/src/data-indexing/service/config.ts b/packages/indexer/src/data-indexing/service/config.ts index 015940b4..6d720316 100644 --- a/packages/indexer/src/data-indexing/service/config.ts +++ b/packages/indexer/src/data-indexing/service/config.ts @@ -4,6 +4,7 @@ import { CCTP_DEPOSIT_FOR_BURN_ABI, CCTP_MESSAGE_RECEIVED_ABI, CCTP_MESSAGE_SENT_ABI, + SPONSORED_DEPOSIT_FOR_BURN_ABI, CCTP_MINT_AND_WITHDRAW_ABI, } from "../model/abis"; import { @@ -19,12 +20,16 @@ import { import { IndexerEventPayload } from "./genericEventListening"; import { IndexerEventHandler } from "./genericIndexing"; import { Logger } from "winston"; -import { extractRawArgs } from "./preprocessing"; +import { + extractRawArgs, + preprocessSponsoredDepositForBurn, +} from "./preprocessing"; import { DepositForBurnArgs, EventArgs, MessageReceivedArgs, MessageSentArgs, + SponsoredDepositForBurnArgs, MintAndWithdrawArgs, SwapFlowFinalizedArgs, SwapFlowInitializedArgs, @@ -39,6 +44,7 @@ import { transformDepositForBurnEvent, transformMessageReceivedEvent, transformMessageSentEvent, + transformSponsoredDepositForBurnEvent, transformMintAndWithdrawEvent, transformSwapFlowFinalizedEvent, transformSwapFlowInitializedEvent, @@ -47,6 +53,7 @@ import { storeDepositForBurnEvent, storeMessageReceivedEvent, storeMessageSentEvent, + storeSponsoredDepositForBurnEvent, storeMintAndWithdrawEvent, storeSwapFlowFinalizedEvent, storeSwapFlowInitializedEvent, @@ -165,6 +172,37 @@ export const CCTP_PROTOCOL: SupportedProtocols< }, }; +export const SPONSORED_CCTP_PROTOCOL: SupportedProtocols< + Partial, + BlockchainEventRepository, + IndexerEventPayload, + EventArgs +> = { + getEventHandlers: (logger: Logger, chainId: number) => { + const handlers = CCTP_PROTOCOL.getEventHandlers(logger, chainId); + return [ + ...handlers, + { + config: { + address: getSponsoredCCTPDstPeripheryAddress( + chainId, + ) as `0x${string}`, + abi: SPONSORED_DEPOSIT_FOR_BURN_ABI, + eventName: "SponsoredDepositForBurn", + }, + preprocess: (payload: IndexerEventPayload) => + preprocessSponsoredDepositForBurn(payload, logger), + filter: async () => true, + transform: ( + args: SponsoredDepositForBurnArgs, + payload: IndexerEventPayload, + ) => transformSponsoredDepositForBurnEvent(args, payload, logger), + store: storeSponsoredDepositForBurnEvent, + }, + ]; + }, +}; + /** * Configuration for Sponsored Bridging Protocol. */ @@ -217,7 +255,7 @@ export const CHAIN_PROTOCOLS: Record< EventArgs >[] > = { - [CHAIN_IDs.ARBITRUM]: [CCTP_PROTOCOL], + [CHAIN_IDs.ARBITRUM]: [SPONSORED_CCTP_PROTOCOL], [CHAIN_IDs.ARBITRUM_SEPOLIA]: [CCTP_PROTOCOL], [CHAIN_IDs.HYPEREVM]: [CCTP_PROTOCOL, SPONSORED_BRIDGING_PROTOCOL], [CHAIN_IDs.MAINNET]: [CCTP_PROTOCOL], diff --git a/packages/indexer/src/data-indexing/service/filtering.ts b/packages/indexer/src/data-indexing/service/filtering.ts index 998c96dc..2e579b1d 100644 --- a/packages/indexer/src/data-indexing/service/filtering.ts +++ b/packages/indexer/src/data-indexing/service/filtering.ts @@ -13,7 +13,7 @@ import { CCTP_DEPOSIT_FOR_BURN_ABI, CCTP_MESSAGE_RECEIVED_ABI, } from "../model/abis"; -import { decodeEventFromReceipt } from "./tranforming"; +import { decodeEventFromReceipt } from "./preprocessing"; import { DepositForBurnArgs, MessageReceivedArgs, diff --git a/packages/indexer/src/data-indexing/service/preprocessing.ts b/packages/indexer/src/data-indexing/service/preprocessing.ts index b36475d8..32ce7ab3 100644 --- a/packages/indexer/src/data-indexing/service/preprocessing.ts +++ b/packages/indexer/src/data-indexing/service/preprocessing.ts @@ -1,4 +1,41 @@ import { IndexerEventPayload } from "./genericEventListening"; +import { + Abi, + parseAbi, + parseEventLogs, + TransactionReceipt, + decodeEventLog, +} from "viem"; +import { + SPONSORED_DEPOSIT_FOR_BURN_ABI, + CCTP_DEPOSIT_FOR_BURN_ABI, +} from "../model/abis"; +import { DEPOSIT_FOR_BURN_EVENT_NAME } from "./constants"; +import { + DepositForBurnArgs, + SponsoredDepositForBurnArgs, +} from "../model/eventTypes"; +import { getCctpDestinationChainFromDomain } from "../adapter/cctp-v2/service"; +import { Logger } from "winston"; + +/** + * extracts and decodes a specific event from a transaction receipt's logs. + * @param receipt The transaction receipt. + * @param abi The Abi containing the event definition. + * @returns The decoded event arguments, or undefined if not found. + */ +export const decodeEventFromReceipt = ( + receipt: TransactionReceipt, + abi: Abi, + eventName: string, +): T | undefined => { + const logs = parseEventLogs({ + abi, + logs: receipt.logs, + }); + const log = logs.find((log) => log.eventName === eventName); + return (log?.args as T) ?? undefined; +}; export const extractRawArgs = ( payload: IndexerEventPayload, @@ -13,3 +50,31 @@ export const extractRawArgs = ( return rawArgs as TEvent; }; + +export const preprocessSponsoredDepositForBurn = ( + payload: IndexerEventPayload, + logger: Logger, +): SponsoredDepositForBurnArgs => { + const args = extractRawArgs(payload); + + if (payload.transactionReceipt) { + const depositArgs = decodeEventFromReceipt( + payload.transactionReceipt, + parseAbi(CCTP_DEPOSIT_FOR_BURN_ABI), + DEPOSIT_FOR_BURN_EVENT_NAME, + ); + if (depositArgs) { + args.destinationChainId = getCctpDestinationChainFromDomain( + depositArgs.destinationDomain, + ); + } else { + const message = `Failed to decode DepositForBurn event from transaction receipt to decode destination chain id for SponsoredDepositForBurnEvent.`; + logger.error({ + message, + payload, + }); + throw new Error(message); + } + } + return args; +}; diff --git a/packages/indexer/src/data-indexing/service/storing.ts b/packages/indexer/src/data-indexing/service/storing.ts index 9deed7fb..446f6b8f 100644 --- a/packages/indexer/src/data-indexing/service/storing.ts +++ b/packages/indexer/src/data-indexing/service/storing.ts @@ -9,6 +9,28 @@ const PK_CHAIN_BLOCK_TX_LOG = [ "logIndex", ]; +/** + * Stores a DepositForBurn event in the database. + * + * @param event The DepositForBurn entity to store. + * @param repository The BlockchainEventRepository instance. + * @returns A promise that resolves to the result of the save operation. + */ +export const storeSponsoredDepositForBurnEvent: Storer< + Partial, + dbUtils.BlockchainEventRepository +> = async ( + event: Partial, + repository: dbUtils.BlockchainEventRepository, +) => { + return repository.saveAndHandleFinalisationBatch( + entities.SponsoredDepositForBurn, + [{ ...event, dataSource: DataSourceType.WEB_SOCKET } as any], + PK_CHAIN_BLOCK_TX_LOG as (keyof entities.SponsoredDepositForBurn)[], + [], + ); +}; + /** * Stores a DepositForBurn event in the database. * diff --git a/packages/indexer/src/data-indexing/service/tranforming.ts b/packages/indexer/src/data-indexing/service/tranforming.ts index 1a20e490..2e36040e 100644 --- a/packages/indexer/src/data-indexing/service/tranforming.ts +++ b/packages/indexer/src/data-indexing/service/tranforming.ts @@ -3,7 +3,7 @@ import * as across from "@across-protocol/sdk"; import { IndexerEventPayload } from "./genericEventListening"; import { getCctpDestinationChainFromDomain, - decodeMessage, // New import + decodeMessage, } from "../adapter/cctp-v2/service"; import { formatFromAddressToChainFormat, safeJsonStringify } from "../../utils"; import { getFinalisedBlockBufferDistance } from "./constants"; @@ -11,18 +11,13 @@ import { DepositForBurnArgs, MessageReceivedArgs, MessageSentArgs, + SponsoredDepositForBurnArgs, MintAndWithdrawArgs, SwapFlowFinalizedArgs, SwapFlowInitializedArgs, } from "../model/eventTypes"; import { Logger } from "winston"; -import { arrayify } from "ethers/lib/utils"; // New import -import { - TransactionReceipt, - parseEventLogs, - ParseEventLogsReturnType, - Abi, -} from "viem"; +import { arrayify } from "ethers/lib/utils"; /** * A generic transformer for addresses. @@ -128,6 +123,43 @@ export const transformDepositForBurnEvent = ( }; }; +export const transformSponsoredDepositForBurnEvent = ( + preprocessed: SponsoredDepositForBurnArgs, + payload: IndexerEventPayload, + logger: Logger, +): Partial => { + const base = baseTransformer(payload, logger); + + const destinationChainId = preprocessed.destinationChainId; + if (!destinationChainId) { + const message = `Failed to decode DepositForBurn event from transaction receipt to decode destination chain id for SponsoredDepositForBurnEvent.`; + logger.error({ + message, + payload, + }); + throw new Error(message); + } + const finalRecipient = transformAddress( + preprocessed.finalRecipient, + destinationChainId, + ); + const finalToken = transformAddress( + preprocessed.finalToken, + destinationChainId, + ); + + return { + ...base, + quoteNonce: preprocessed.quoteNonce, + originSender: preprocessed.originSender.toLowerCase(), + finalRecipient: finalRecipient.toLowerCase(), + quoteDeadline: new Date(Number(preprocessed.quoteDeadline) * 1000), + maxBpsToSponsor: preprocessed.maxBpsToSponsor.toString(), + maxUserSlippageBps: preprocessed.maxUserSlippageBps.toString(), + finalToken: finalToken.toLowerCase(), + signature: preprocessed.signature, + }; +}; export const transformMessageSentEvent = ( preprocessed: MessageSentArgs, payload: IndexerEventPayload, @@ -165,25 +197,6 @@ export const transformMessageSentEvent = ( }; }; -/** - * extracts and decodes a specific event from a transaction receipt's logs. - * @param receipt The transaction receipt. - * @param abi The Abi containing the event definition. - * @returns The decoded event arguments, or undefined if not found. - */ -export const decodeEventFromReceipt = ( - receipt: TransactionReceipt, - abi: Abi, - eventName: string, -): T | undefined => { - const logs = parseEventLogs({ - abi, - logs: receipt.logs, - }); - const log = logs.find((log) => log.eventName === eventName); - return (log?.args as T) ?? undefined; -}; - /** * Transforms a raw `MessageReceived` event payload into a partial `MessageReceived` entity. * The 'finalised' property is set by the `baseTransformer` based on the event's block number diff --git a/packages/indexer/src/data-indexing/tests/Indexers.integration.test.ts b/packages/indexer/src/data-indexing/tests/Indexers.integration.test.ts index 82a3e891..677acc5a 100644 --- a/packages/indexer/src/data-indexing/tests/Indexers.integration.test.ts +++ b/packages/indexer/src/data-indexing/tests/Indexers.integration.test.ts @@ -5,14 +5,17 @@ import { startChainIndexing } from "../service/indexing"; import { MockWebSocketRPCServer } from "../../tests/testProvider"; import { utils as dbUtils } from "@repo/indexer-database"; import * as contractUtils from "../../utils/contractUtils"; -import { entities, utils, DataSourceType } from "@repo/indexer-database"; -import { MESSAGE_TRANSMITTER_ADDRESS_MAINNET } from "../service/constants"; +import { entities, DataSourceType } from "@repo/indexer-database"; import sinon from "sinon"; import { Logger } from "winston"; import { CHAIN_IDs } from "@across-protocol/constants"; import { createPublicClient, http, PublicClient } from "viem"; import { arbitrum, arbitrumSepolia, hyperEvm, mainnet } from "viem/chains"; -import { CCTP_PROTOCOL, SPONSORED_BRIDGING_PROTOCOL } from "../service/config"; +import { + CCTP_PROTOCOL, + SPONSORED_BRIDGING_PROTOCOL, + SPONSORED_CCTP_PROTOCOL, +} from "../service/config"; // Setup generic client for fetching data const getTestPublicClient = (chainId: number): PublicClient => { @@ -509,6 +512,76 @@ describe("Websocket Subscription", () => { hookData: "0x", }); }).timeout(20000); + it("should ingest sponsored CCTP events from Arbitrum tx 0xef55...78a0", async () => { + // Tx: https://arbiscan.io/tx/0xef55d3110094488b943525fd6609e7918328009168e661658b5fb858434b78a0 + const txHash = + "0xef55d3110094488b943525fd6609e7918328009168e661658b5fb858434b78a0"; + + const client = getTestPublicClient(CHAIN_IDs.ARBITRUM); + + // Stub contract Utils for finding the sponsored event from the periphery address + // The previous test suite (CCTPIndexerDataHandler) used: + // SponsoredCCTPSrcPeriphery: 0xAA4958EFa0Cf6DdD87e354a90785f1D7291a82c7 + sinon + .stub(contractUtils, "getSponsoredCCTPSrcPeripheryAddress") + .returns("0xAA4958EFa0Cf6DdD87e354a90785f1D7291a82c7"); + + const { block, receipt } = await fetchAndMockTransaction( + server, + client, + txHash, + ); + + // Start the Indexer + startChainIndexing({ + repo: blockchainRepository, + rpcUrl: rpcUrl, + logger, + sigterm: abortController.signal, + chainId: CHAIN_IDs.ARBITRUM, + protocols: [SPONSORED_CCTP_PROTOCOL], + }); + + await server.waitForSubscription(4); + + receipt.logs.forEach((log) => server.pushEvent(log)); + + // Wait for insertion + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Verify DepositForBurn Persistence + const depositRepo = dataSource.getRepository(entities.DepositForBurn); + const savedEvent = await depositRepo.findOne({ + where: { transactionHash: txHash }, + }); + expect(savedEvent).to.exist; + expect(savedEvent!.transactionHash).to.equal(txHash); + + // Verify SponsoredDepositForBurn Persistence + const sponsoredRepo = dataSource.getRepository( + entities.SponsoredDepositForBurn, + ); + const savedSponsoredEvent = await sponsoredRepo.findOne({ + where: { transactionHash: txHash }, + }); + expect(savedSponsoredEvent).to.exist; + expect(savedSponsoredEvent!).to.deep.include({ + chainId: CHAIN_IDs.ARBITRUM, + blockNumber: Number(block.number), + transactionHash: txHash, + quoteNonce: + "0x333d757477a9ebed33ed12e6320a8414d034cd86ca2acd292d9b687a99bdb866", + originSender: "0x9a8f92a830a5cb89a3816e3d267cb7791c16b04d", + finalRecipient: "0x9a8f92a830a5cb89a3816e3d267cb7791c16b04d", + quoteDeadline: new Date(1765996920 * 1000), + maxBpsToSponsor: 0, + maxUserSlippageBps: 50, + finalToken: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + signature: + "0x1ed01ce81157c25664616c112142037217f5b22318f451eb7f6eb07d2784810a00c1d0f648a6687fcd3dccfe7c37660c7e378dbc0c2a49a917d6b016cdb8f8571c", + dataSource: DataSourceType.WEB_SOCKET, + }); + }).timeout(20000); it("should ingest the MintAndWithdraw event from Arbitrum tx 0x3b3d...e813", async () => { // Real Transaction Data const txHash =