Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 5 additions & 19 deletions src/clients/SpokePoolClient/SpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import {
assign,
getRelayEventKey,
isDefined,
getMessageHash,
isSlowFill,
validateFillForDeposit,
chainIsEvm,
chainIsProd,
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 {
Expand All @@ -45,6 +44,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;
Expand Down Expand Up @@ -522,23 +522,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<DepositWithBlock, "quoteBlockNumber" | "fromLiteChain" | "toLiteChain"> = {
...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);

Expand Down
34 changes: 33 additions & 1 deletion src/interfaces/SpokePool.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<typeof RelayDataStruct>;

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<typeof DepositRawStruct>;
84 changes: 84 additions & 0 deletions src/utils/EventDecoder.ts
Original file line number Diff line number Diff line change
@@ -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<TRawArgs, TParsedArgs, TContext = unknown> {
struct: Struct<TRawArgs>;
parse(valid: TRawArgs, context?: TContext): TParsedArgs;
}

function decodeEvent<TRawArgs, TParsedArgs, TContext>(
raw: unknown,
decoder: EventArgsDecoder<TRawArgs, TParsedArgs, TContext>,
context?: TContext
): TParsedArgs {
const validated = create(raw, decoder.struct);
return decoder.parse(validated, context);
}

export function decodeSortableEvent<TRawArgs, TParsedArgs, TContext>(
sortableEvent: SortableEvent,
rawArgs: unknown,
decoder: EventArgsDecoder<TRawArgs, TParsedArgs, TContext>,
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<Deposit, "fromLiteChain" | "toLiteChain">,
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<Deposit, "fromLiteChain" | "toLiteChain">;

return parsed;
},
};
16 changes: 14 additions & 2 deletions src/utils/ValidatorUtils.ts
Original file line number Diff line number Diff line change
@@ -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<string>("AddressValidator", (v) => ethersUtils.isAddress(String(v)));
export const AddressValidator = define<string>("AddressValidator", (v) => ethersUtils.isAddress(String(v)));
export const HexEvmAddress = AddressValidator;

export const HexString32Bytes = define<string>(
"HexString32Bytes",
(v) => typeof v === "string" && ethersUtils.isHexString(v, 32)
);
const HexValidator = define<string>("HexValidator", (v) => ethersUtils.isHexString(String(v)));

export const BigNumberValidator = define<BigNumber>("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,
Expand Down
Loading