From 0d668f38c3b1861596b96c1a58fd16ff233e6b9b Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Tue, 29 Jul 2025 13:06:02 -0700 Subject: [PATCH 1/2] add SS Decoder for Deposit event type Signed-off-by: Ihor Farion --- .../SpokePoolClient/SpokePoolClient.ts | 21 +---- src/interfaces/SpokePool.ts | 34 +++++++- src/utils/EventDecoder.ts | 84 +++++++++++++++++++ src/utils/ValidatorUtils.ts | 16 +++- 4 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 src/utils/EventDecoder.ts diff --git a/src/clients/SpokePoolClient/SpokePoolClient.ts b/src/clients/SpokePoolClient/SpokePoolClient.ts index 90c3d8111..1a4c3bebf 100644 --- a/src/clients/SpokePoolClient/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SpokePoolClient.ts @@ -45,6 +45,7 @@ import { BaseAbstractClient, UpdateFailureReason } from "../BaseAbstractClient"; import { AcrossConfigStoreClient } from "../AcrossConfigStoreClient"; import { getRefundInformationFromFill } from "../BundleDataClient"; import { HubPoolClient } from "../HubPoolClient"; +import { DepositArgsDecoder, decodeSortableEvent } from "../../utils/EventDecoder"; export type SpokePoolUpdateSuccess = { success: true; @@ -522,23 +523,9 @@ export abstract class SpokePoolClient extends BaseAbstractClient { const queryDepositEvents = async (eventName: string) => { const depositEvents = (queryResults[eventsToQuery.indexOf(eventName)] ?? []) .map((event) => { - if (!FundsDepositedRaw.is(event)) { - this.log("warn", `Skipping malformed ${eventName} event.`, { event }); - return; - } - - const deposit: Omit = { - ...event, - originChainId: this.chainId, - depositor: toAddressType(event.depositor, this.chainId), - recipient: toAddressType(event.recipient, event.destinationChainId), - inputToken: toAddressType(event.inputToken, this.chainId), - outputToken: toAddressType(event.outputToken, event.destinationChainId), - exclusiveRelayer: toAddressType(event.exclusiveRelayer, event.destinationChainId), - messageHash: getMessageHash(event.message), - }; - - return deposit; + return decodeSortableEvent(event, event, DepositArgsDecoder, { + chainId: this.chainId, + }); }) .filter(isDefined); diff --git a/src/interfaces/SpokePool.ts b/src/interfaces/SpokePool.ts index 1fa4c3ef8..af917db0c 100644 --- a/src/interfaces/SpokePool.ts +++ b/src/interfaces/SpokePool.ts @@ -1,7 +1,8 @@ import { SortableEvent } from "./Common"; import { SpokePoolClient } from "../clients"; -import { BigNumber, Address, EvmAddress } from "../utils"; +import { BigNumber, Address, EvmAddress, BigNumberishStruct, HexEvmAddress } from "../utils"; import { RelayerRefundLeaf } from "./HubPool"; +import { Infer, assign, number, object, optional, string } from "superstruct"; export interface RelayData { originChainId: number; @@ -171,3 +172,34 @@ export interface BridgedToHubPoolWithBlock extends SortableEvent { export interface SpokePoolClientsByChain { [chainId: number]: SpokePoolClient; } + +// @todo ihor: these types are similar to src/clients/SpokePoolClient/types.ts . Need to reconcile the two +export const RelayDataStruct = object({ + originChainId: number(), + depositor: string(), + recipient: string(), + depositId: BigNumberishStruct, + inputToken: string(), + inputAmount: BigNumberishStruct, + outputToken: string(), + outputAmount: BigNumberishStruct, + message: string(), + fillDeadline: number(), + exclusiveRelayer: string(), + exclusivityDeadline: number(), +}); +export type RelayDataRaw = Infer; + +const DepositExtraFieldsStruct = object({ + destinationChainId: number(), + quoteTimestamp: number(), + // Optional SpeedUpCommon fields and depositor-authorised speed up signature. + updatedRecipient: optional(HexEvmAddress), + updatedOutputAmount: optional(BigNumberishStruct), + updatedMessage: optional(string()), + speedUpSignature: optional(string()), +}); + +export const DepositRawStruct = assign(RelayDataStruct, DepositExtraFieldsStruct); + +export type DepositRaw = Infer; diff --git a/src/utils/EventDecoder.ts b/src/utils/EventDecoder.ts new file mode 100644 index 000000000..0ee6552b4 --- /dev/null +++ b/src/utils/EventDecoder.ts @@ -0,0 +1,84 @@ +import { create, Struct } from "superstruct"; +import { Deposit, DepositRaw, DepositRawStruct, SortableEvent } from "../interfaces"; +import { toAddressType } from "./AddressUtils"; +import { BigNumber } from "./BigNumberUtils"; +import { getMessageHash } from "./SpokeUtils"; + +export interface EventArgsDecoder { + struct: Struct; + parse(valid: TRawArgs, context?: TContext): TParsedArgs; +} + +function decodeEvent( + raw: unknown, + decoder: EventArgsDecoder, + context?: TContext +): TParsedArgs { + const validated = create(raw, decoder.struct); + return decoder.parse(validated, context); +} + +export function decodeSortableEvent( + sortableEvent: SortableEvent, + rawArgs: unknown, + decoder: EventArgsDecoder, + context?: TContext +): TParsedArgs & SortableEvent { + // Validate and parse the event-specific args from the raw log. + const parsedArgs = decodeEvent(rawArgs, decoder, context); + + // Merge the parsed args with the existing SortableEvent data. + return { + ...parsedArgs, + ...sortableEvent, + }; +} + +type SpokePoolClientContext = { + chainId: number; +}; + +export const DepositArgsDecoder: EventArgsDecoder< + DepositRaw, + Omit, + SpokePoolClientContext +> = { + struct: DepositRawStruct, + parse: (raw, context) => { + if (!context) throw new Error("chainId context is required"); + + const { + // Separate out fields that are going to be re-typed + depositor, + recipient, + inputToken, + outputToken, + exclusiveRelayer, + depositId, + inputAmount, + outputAmount, + updatedRecipient, + updatedOutputAmount, + // Spread the rest of the fields + ...rest + } = raw; + + const parsed = { + ...rest, + depositor: toAddressType(depositor, context.chainId), + recipient: toAddressType(recipient, raw.destinationChainId), + inputToken: toAddressType(inputToken, context.chainId), + outputToken: toAddressType(outputToken, raw.destinationChainId), + exclusiveRelayer: toAddressType(exclusiveRelayer, raw.destinationChainId), + depositId: BigNumber.from(depositId), + inputAmount: BigNumber.from(inputAmount), + outputAmount: BigNumber.from(outputAmount), + messageHash: getMessageHash(raw.message), + updatedRecipient: + updatedRecipient !== undefined ? toAddressType(updatedRecipient, raw.destinationChainId) : undefined, + updatedOutputAmount: updatedOutputAmount !== undefined ? BigNumber.from(updatedOutputAmount) : undefined, + } satisfies Omit; + + return parsed; + }, +}; diff --git a/src/utils/ValidatorUtils.ts b/src/utils/ValidatorUtils.ts index 40dc6dba4..a0dd167aa 100644 --- a/src/utils/ValidatorUtils.ts +++ b/src/utils/ValidatorUtils.ts @@ -1,12 +1,24 @@ import { utils as ethersUtils } from "ethers"; -import { object, min as Min, define, optional, string, integer, boolean } from "superstruct"; +import { object, min as Min, define, optional, string, integer, boolean, union, number } from "superstruct"; import { DepositWithBlock } from "../interfaces"; import { BigNumber } from "../utils"; -const AddressValidator = define("AddressValidator", (v) => ethersUtils.isAddress(String(v))); +export const AddressValidator = define("AddressValidator", (v) => ethersUtils.isAddress(String(v))); +export const HexEvmAddress = AddressValidator; + +export const HexString32Bytes = define( + "HexString32Bytes", + (v) => typeof v === "string" && ethersUtils.isHexString(v, 32) +); const HexValidator = define("HexValidator", (v) => ethersUtils.isHexString(String(v))); + export const BigNumberValidator = define("BigNumberValidator", (v) => BigNumber.isBigNumber(v)); +// Event arguments that represent a uint256 can be returned from ethers as a BigNumber +// object, but can also be represented as a hex string or number in other contexts. +// This struct validates that the value is one of these types. +export const BigNumberishStruct = union([string(), number(), BigNumberValidator]); + const V3DepositSchema = object({ depositId: BigNumberValidator, depositor: AddressValidator, From b3bb124fabe44f99c90585011e670c0449a027b9 Mon Sep 17 00:00:00 2001 From: Ihor Farion Date: Tue, 29 Jul 2025 13:07:37 -0700 Subject: [PATCH 2/2] remove redundant imports Signed-off-by: Ihor Farion --- src/clients/SpokePoolClient/SpokePoolClient.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/clients/SpokePoolClient/SpokePoolClient.ts b/src/clients/SpokePoolClient/SpokePoolClient.ts index 1a4c3bebf..6a3ecec4c 100644 --- a/src/clients/SpokePoolClient/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SpokePoolClient.ts @@ -13,7 +13,6 @@ import { assign, getRelayEventKey, isDefined, - getMessageHash, isSlowFill, validateFillForDeposit, chainIsEvm, @@ -21,7 +20,7 @@ import { Address, toAddressType, } from "../../utils"; -import { FundsDepositedRaw, FilledRelayRaw } from "./types"; +import { FilledRelayRaw } from "./types"; import { duplicateEvent, sortEventsAscendingInPlace } from "../../utils/EventUtils"; import { CHAIN_IDs, ZERO_ADDRESS } from "../../constants"; import {