From 1b82a7326f76b68bf41b012f5d43beba499b91cd Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Fri, 13 Jun 2025 12:28:13 +0200 Subject: [PATCH 01/14] feature: Get all Invalid Fills --- .../SpokePoolClient/SpokePoolClient.ts | 15 +++++ src/interfaces/SpokePool.ts | 8 +++ src/utils/SpokeUtils.ts | 66 ++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/clients/SpokePoolClient/SpokePoolClient.ts b/src/clients/SpokePoolClient/SpokePoolClient.ts index 309a553ac..4135c31ab 100644 --- a/src/clients/SpokePoolClient/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SpokePoolClient.ts @@ -758,6 +758,21 @@ export abstract class SpokePoolClient extends BaseAbstractClient { ); } + /** + * Find all deposits (including duplicates) based on its deposit ID. + * @param depositId The unique ID of the deposit being queried. + * @returns Array of all deposits with the given depositId, including duplicates. + */ + public getDepositsForDepositId(depositId: BigNumber): DepositWithBlock[] { + const deposit = this.getDeposit(depositId); + if (!deposit) { + return []; + } + const depositHash = getRelayEventKey(deposit); + const duplicates = this.duplicateDepositHashes[depositHash] ?? []; + return [deposit, ...duplicates]; + } + // /////////////////////// // // ABSTRACT METHODS // // /////////////////////// diff --git a/src/interfaces/SpokePool.ts b/src/interfaces/SpokePool.ts index a8887ee56..6f6b2a4dc 100644 --- a/src/interfaces/SpokePool.ts +++ b/src/interfaces/SpokePool.ts @@ -66,6 +66,14 @@ export interface Fill extends Omit { relayExecutionInfo: RelayExecutionEventInfo; } +export interface InvalidFill { + fill: FillWithBlock; + validationResults: Array<{ + reason: string; + deposit?: DepositWithBlock; + }>; +} + export interface FillWithBlock extends Fill, SortableEvent {} export interface FillWithTime extends Fill, SortableEvent { fillTimestamp: number; diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index c1c0db1ed..13a9d0661 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,12 +1,13 @@ import { encodeAbiParameters, Hex, keccak256 } from "viem"; import { fixedPointAdjustment as fixedPoint } from "./common"; import { MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS, ZERO_BYTES } from "../constants"; -import { Deposit, Fill, FillType, RelayData, SlowFillLeaf } from "../interfaces"; +import { Deposit, DepositWithBlock, Fill, FillType, InvalidFill, RelayData, SlowFillLeaf } from "../interfaces"; import { toBytes32 } from "./AddressUtils"; import { BigNumber } from "./BigNumberUtils"; -import { isMessageEmpty } from "./DepositUtils"; +import { isMessageEmpty, validateFillForDeposit } from "./DepositUtils"; import { chainIsSvm } from "./NetworkUtils"; import { svm } from "../arch"; +import { SpokePoolClient } from "../clients"; export function isSlowFill(fill: Fill): boolean { return fill.relayExecutionInfo.fillType === FillType.SlowFill; @@ -102,3 +103,64 @@ export function isZeroAddress(address: string): boolean { export function getMessageHash(message: string): string { return isMessageEmpty(message) ? ZERO_BYTES : keccak256(message as Hex); } + +export function findInvalidFills(spokePoolClients: { [chainId: number]: SpokePoolClient }): InvalidFill[] { + const invalidFills: InvalidFill[] = []; + + // Iterate through each spoke pool client + Object.values(spokePoolClients).forEach((spokePoolClient) => { + // Get all fills for this client + const fills = spokePoolClient.getFills(); + + // Process each fill + fills.forEach((fill) => { + // Skip fills with unsafe deposit IDs + if (isUnsafeDepositId(fill.depositId)) { + return; + } + + // Get all deposits (including duplicates) for this fill's depositId + const deposits = spokePoolClients[fill.originChainId].getDepositsForDepositId(fill.depositId); + + // If no deposits found at all + if (deposits.length === 0) { + invalidFills.push({ + fill, + validationResults: [ + { + reason: `no deposit with depositId ${fill.depositId} found`, + } + ] + }); + return; + } + + // Try to find a valid deposit for this fill + let foundValidDeposit = false; + const validationResults: Array<{ reason: string; deposit: DepositWithBlock }> = []; + + for (const deposit of deposits) { + // Validate the fill against the deposit + const validationResult = validateFillForDeposit(fill, deposit); + if (validationResult.valid) { + foundValidDeposit = true; + break; + } + validationResults.push({ + reason: validationResult.reason, + deposit + }); + } + + // If no valid deposit was found, add to invalid fills with all validation results + if (!foundValidDeposit) { + invalidFills.push({ + fill, + validationResults + }); + } + }); + }); + + return invalidFills; +} From 1b8aaed7279af062e779db0c2a508da98d75fa42 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Fri, 13 Jun 2025 12:43:28 +0200 Subject: [PATCH 02/14] Lint fix --- src/utils/SpokeUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 13a9d0661..362282667 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -129,8 +129,8 @@ export function findInvalidFills(spokePoolClients: { [chainId: number]: SpokePoo validationResults: [ { reason: `no deposit with depositId ${fill.depositId} found`, - } - ] + }, + ], }); return; } @@ -148,7 +148,7 @@ export function findInvalidFills(spokePoolClients: { [chainId: number]: SpokePoo } validationResults.push({ reason: validationResult.reason, - deposit + deposit, }); } @@ -156,7 +156,7 @@ export function findInvalidFills(spokePoolClients: { [chainId: number]: SpokePoo if (!foundValidDeposit) { invalidFills.push({ fill, - validationResults + validationResults, }); } }); From 4a67c15f3f6ee3c80eb69b9e146fdf735da28205 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Mon, 16 Jun 2025 13:04:18 +0200 Subject: [PATCH 03/14] Add onchain search for multiple deposits for both EVM and SVM --- src/arch/svm/SpokeUtils.ts | 114 +++++++++++++++--- .../SpokePoolClient/EVMSpokePoolClient.ts | 112 ++++++++++++++--- .../SpokePoolClient/SVMSpokePoolClient.ts | 39 +++++- .../SpokePoolClient/SpokePoolClient.ts | 10 ++ src/utils/DepositUtils.ts | 4 + src/utils/SpokeUtils.ts | 24 ++-- 6 files changed, 256 insertions(+), 47 deletions(-) diff --git a/src/arch/svm/SpokeUtils.ts b/src/arch/svm/SpokeUtils.ts index 35e9d69c7..086c0d131 100644 --- a/src/arch/svm/SpokeUtils.ts +++ b/src/arch/svm/SpokeUtils.ts @@ -37,7 +37,7 @@ import { getStatePda, unwrapEventData, } from "./"; -import { SVMEventNames, SVMProvider } from "./types"; +import { EventWithData, SVMEventNames, SVMProvider } from "./types"; /** * @note: Average Solana slot duration is about 400-500ms. We can be conservative @@ -74,6 +74,40 @@ export function getDepositIdAtBlock(_contract: unknown, _blockTag: number): Prom throw new Error("getDepositIdAtBlock: not implemented"); } +/** + * Helper function to query deposit events within a time window. + * @param eventClient - SvmCpiEventsClient instance + * @param depositId - The deposit ID to search for + * @param slot - The slot to search up to (defaults to current slot) + * @param secondsLookback - The number of seconds to look back for deposits (defaults to 2 days) + * @returns Array of deposit events within the slot window + */ +async function queryDepositEventsInWindow( + eventClient: SvmCpiEventsClient, + depositId: BigNumber, + slot?: bigint, + secondsLookback = 2 * 24 * 60 * 60 // 2 days +): Promise { + // We can only perform this search when we have a safe deposit ID. + if (isUnsafeDepositId(depositId)) { + throw new Error(`Cannot binary search for depositId ${depositId}`); + } + + const provider = eventClient.getRpc(); + const currentSlot = await provider.getSlot({ commitment: "confirmed" }).send(); + + // If no slot is provided, use the current slot + // If a slot is provided, ensure it's not in the future + const endSlot = slot !== undefined ? BigInt(Math.min(Number(slot), Number(currentSlot))) : currentSlot; + + // Calculate start slot (approximately secondsLookback seconds earlier) + const slotsInElapsed = BigInt(Math.round((secondsLookback * 1000) / SLOT_DURATION_MS)); + const startSlot = endSlot - slotsInElapsed; + + // Query for the deposit events with this limited slot range + return eventClient.queryEvents("FundsDeposited", startSlot, endSlot) || []; +} + /** * Finds deposit events within a 2-day window ending at the specified slot. * @@ -110,24 +144,10 @@ export async function findDeposit( slot?: bigint, secondsLookback = 2 * 24 * 60 * 60 // 2 days ): Promise { - // We can only perform this search when we have a safe deposit ID. - if (isUnsafeDepositId(depositId)) { - throw new Error(`Cannot binary search for depositId ${depositId}`); - } - - const provider = eventClient.getRpc(); - const currentSlot = await provider.getSlot({ commitment: "confirmed" }).send(); - - // If no slot is provided, use the current slot - // If a slot is provided, ensure it's not in the future - const endSlot = slot !== undefined ? BigInt(Math.min(Number(slot), Number(currentSlot))) : currentSlot; - - // Calculate start slot (approximately secondsLookback seconds earlier) - const slotsInElapsed = BigInt(Math.round((secondsLookback * 1000) / SLOT_DURATION_MS)); - const startSlot = endSlot - slotsInElapsed; + const depositEvents = await queryDepositEventsInWindow(eventClient, depositId, slot, secondsLookback); - // Query for the deposit events with this limited slot range. Filter by deposit id. - const depositEvent = (await eventClient.queryEvents("FundsDeposited", startSlot, endSlot))?.find((event) => + // Find the first matching deposit event + const depositEvent = depositEvents.find((event) => depositId.eq((event.data as unknown as { depositId: BigNumber }).depositId) ); @@ -146,6 +166,64 @@ export async function findDeposit( } as DepositWithBlock; } +/** + * Finds all deposit events within a 2-day window ending at the specified slot. + * + * @remarks + * This implementation uses a slot-limited search approach because Solana PDA state has + * limitations that prevent directly referencing old deposit IDs. Unlike EVM chains where + * we might use binary search across the entire chain history, in Solana we must query within + * a constrained slot range. + * + * The search window is calculated by: + * 1. Using the provided slot (or current confirmed slot if none is provided) + * 2. Looking back 2 days worth of slots from that point + * + * We use a 2-day window because: + * 1. Most valid deposits that need to be processed will be recent + * 2. This covers multiple bundle submission periods + * 3. It balances performance with practical deposit age + * + * @important + * This function may return an empty array for valid deposit IDs that are older than the search + * window (approximately 2 days before the specified slot). This is an acceptable limitation + * as deposits this old are typically not relevant to current operations. + * + * @param eventClient - SvmCpiEventsClient instance + * @param depositId - The deposit ID to search for + * @param slot - The slot to search up to (defaults to current slot). The search will look + * for deposits between (slot - secondsLookback) and slot. + * @param secondsLookback - The number of seconds to look back for deposits (defaults to 2 days). + * @returns Array of deposits if found within the slot window, empty array otherwise + */ +export async function findAllDeposits( + eventClient: SvmCpiEventsClient, + depositId: BigNumber, + slot?: bigint, + secondsLookback = 2 * 24 * 60 * 60 // 2 days +): Promise { + const depositEvents = await queryDepositEventsInWindow(eventClient, depositId, slot, secondsLookback); + + // Filter for all matching deposit events + const matchingEvents = depositEvents.filter((event) => + depositId.eq((event.data as unknown as { depositId: BigNumber }).depositId) + ); + + // If no deposit events are found, return empty array + if (!matchingEvents || matchingEvents.length === 0) { + return []; + } + + // Return all deposit events with block info + return matchingEvents.map((event) => ({ + txnRef: event.signature.toString(), + blockNumber: Number(event.slot), + txnIndex: 0, + logIndex: 0, + ...(unwrapEventData(event.data) as Record), + })) as DepositWithBlock[]; +} + /** * Resolves the fill status of a deposit at a specific slot or at the current confirmed one. * diff --git a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts index e48c81159..115d41cb7 100644 --- a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts @@ -7,7 +7,7 @@ import { relayFillStatus, getTimestampForBlock as _getTimestampForBlock, } from "../../arch/evm"; -import { DepositWithBlock, FillStatus, RelayData } from "../../interfaces"; +import { DepositWithBlock, FillStatus, Log, RelayData } from "../../interfaces"; import { BigNumber, DepositSearchResult, @@ -17,6 +17,7 @@ import { MakeOptional, toBN, EvmAddress, + MultipleDepositSearchResult, } from "../../utils"; import { EventSearchConfig, @@ -146,28 +147,25 @@ export class EVMSpokePoolClient extends SpokePoolClient { return _getTimeAt(this.spokePool, blockNumber); } - public override async findDeposit(depositId: BigNumber): Promise { - let deposit = this.getDeposit(depositId); - if (deposit) { - return { found: true, deposit }; - } - - // No deposit found; revert to searching for it. - const upperBound = this.latestHeightSearched || undefined; // Don't permit block 0 as the high block. + private async queryDepositEvents( + depositId: BigNumber + ): Promise<{ events: Log[]; from: number; chain: string; elapsedMs: number } | { reason: string }> { + const tStart = Date.now(); + const upperBound = this.latestHeightSearched || undefined; const from = await findDepositBlock(this.spokePool, depositId, this.deploymentBlock, upperBound); const chain = getNetworkName(this.chainId); + if (!from) { - const reason = - `Unable to find ${chain} depositId ${depositId}` + - ` within blocks [${this.deploymentBlock}, ${upperBound ?? "latest"}].`; - return { found: false, code: InvalidFill.DepositIdNotFound, reason }; + return { + reason: `Unable to find ${chain} depositId ${depositId} within blocks [${this.deploymentBlock}, ${ + upperBound ?? "latest" + }].`, + }; } const to = from; - const tStart = Date.now(); - // Check both V3FundsDeposited and FundsDeposited events to look for a specified depositId. const { maxLookBack } = this.eventSearchConfig; - const query = ( + const events = ( await Promise.all([ paginatedEventQuery( this.spokePool, @@ -181,7 +179,25 @@ export class EVMSpokePoolClient extends SpokePoolClient { ), ]) ).flat(); + const tStop = Date.now(); + return { events, from, chain, elapsedMs: tStop - tStart }; + } + + public override async findDeposit(depositId: BigNumber): Promise { + let deposit = this.getDeposit(depositId); + if (deposit) { + return { found: true, deposit }; + } + + // No deposit found; revert to searching for it. + const result = await this.queryDepositEvents(depositId); + + if ("reason" in result) { + return { found: false, code: InvalidFill.DepositIdNotFound, reason: result.reason }; + } + + const { events: query, from, chain, elapsedMs } = result; const event = query.find(({ args }) => args["depositId"].eq(depositId)); if (event === undefined) { @@ -210,12 +226,74 @@ export class EVMSpokePoolClient extends SpokePoolClient { at: "SpokePoolClient#findDeposit", message: "Located deposit outside of SpokePoolClient's search range", deposit, - elapsedMs: tStop - tStart, + elapsedMs, }); return { found: true, deposit }; } + public override async findAllDeposits(depositId: BigNumber): Promise { + // First check memory for deposits + const memoryDeposits = this.getDepositsForDepositId(depositId); + if (memoryDeposits.length > 0) { + return { found: true, deposits: memoryDeposits }; + } + + // If no deposits found in memory, try to find on-chain + const result = await this.queryDepositEvents(depositId); + if ("reason" in result) { + return { found: false, code: InvalidFill.DepositIdNotFound, reason: result.reason }; + } + + const { events: query, chain, elapsedMs } = result; + + // Find all events with matching depositId + const matchingEvents = query.filter(({ args }) => args["depositId"].eq(depositId)); + + if (matchingEvents.length === 0) { + return { + found: false, + code: InvalidFill.DepositIdNotFound, + reason: `${chain} depositId ${depositId} not found at block ${result.from}.`, + }; + } + + // First do all synchronous operations + const deposits = matchingEvents.map((event) => { + const deposit = { + ...spreadEventWithBlockNumber(event), + originChainId: this.chainId, + fromLiteChain: true, + toLiteChain: true, + } as DepositWithBlock; + + if (isZeroAddress(deposit.outputToken)) { + deposit.outputToken = this.getDestinationTokenForDeposit(deposit); + } + deposit.fromLiteChain = this.isOriginLiteChain(deposit); + deposit.toLiteChain = this.isDestinationLiteChain(deposit); + + return deposit; + }); + + // Then do all async operations in parallel + const enrichedDeposits = await Promise.all( + deposits.map(async (deposit) => ({ + ...deposit, + quoteBlockNumber: await this.getBlockNumber(Number(deposit.quoteTimestamp)), + })) + ); + + this.logger.debug({ + at: "SpokePoolClient#findAllDeposits", + message: "Located deposits outside of SpokePoolClient's search range", + deposits: enrichedDeposits, + elapsedMs, + }); + + return { found: true, deposits: enrichedDeposits }; + } + public override getTimestampForBlock(blockNumber: number): Promise { return _getTimestampForBlock(this.spokePool.provider, blockNumber); } diff --git a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts index 7f46d1c76..53bd28927 100644 --- a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts @@ -11,7 +11,7 @@ import { relayFillStatus, fillStatusArray, } from "../../arch/svm"; -import { FillStatus, RelayData, SortableEvent } from "../../interfaces"; +import { DepositWithBlock, FillStatus, RelayData, SortableEvent } from "../../interfaces"; import { BigNumber, DepositSearchResult, @@ -19,12 +19,14 @@ import { InvalidFill, isZeroAddress, MakeOptional, + MultipleDepositSearchResult, sortEventsAscendingInPlace, SvmAddress, } from "../../utils"; import { isUpdateFailureReason } from "../BaseAbstractClient"; import { HubPoolClient } from "../HubPoolClient"; import { knownEventNames, SpokePoolClient, SpokePoolUpdate } from "./SpokePoolClient"; +import { findAllDeposits } from "../../arch/svm/SpokeUtils"; /** * SvmSpokePoolClient is a client for the SVM SpokePool program. It extends the base SpokePoolClient @@ -231,6 +233,41 @@ export class SVMSpokePoolClient extends SpokePoolClient { }; } + public override async findAllDeposits(depositId: BigNumber): Promise { + // TODO: Should we have something like this? In findDeposit we don't look in memory. + // // First check memory for deposits + // const memoryDeposits = this.getDepositsForDepositId(depositId); + // if (memoryDeposits.length > 0) { + // return { found: true, deposits: memoryDeposits }; + // } + + // If no deposits found in memory, try to find on-chain + const deposits = await findAllDeposits(this.svmEventsClient, depositId); + if (!deposits || deposits.length === 0) { + return { + found: false, + code: InvalidFill.DepositIdNotFound, + reason: `Deposit with ID ${depositId} not found`, + }; + } + + // Enrich all deposits with additional information + const enrichedDeposits = await Promise.all( + deposits.map(async (deposit: DepositWithBlock) => ({ + ...deposit, + quoteBlockNumber: await this.getBlockNumber(Number(deposit.quoteTimestamp)), + originChainId: this.chainId, + fromLiteChain: this.isOriginLiteChain(deposit), + toLiteChain: this.isDestinationLiteChain(deposit), + outputToken: isZeroAddress(deposit.outputToken) + ? this.getDestinationTokenForDeposit(deposit) + : deposit.outputToken, + })) + ); + + return { found: true, deposits: enrichedDeposits }; + } + /** * Retrieves the fill status for a given relay data from the SVM chain. */ diff --git a/src/clients/SpokePoolClient/SpokePoolClient.ts b/src/clients/SpokePoolClient/SpokePoolClient.ts index 4135c31ab..b0e89da60 100644 --- a/src/clients/SpokePoolClient/SpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SpokePoolClient.ts @@ -20,6 +20,7 @@ import { chainIsEvm, chainIsProd, Address, + MultipleDepositSearchResult, } from "../../utils"; import { duplicateEvent, sortEventsAscendingInPlace } from "../../utils/EventUtils"; import { ZERO_ADDRESS } from "../../constants"; @@ -773,6 +774,15 @@ export abstract class SpokePoolClient extends BaseAbstractClient { return [deposit, ...duplicates]; } + /** + * Find all deposits for a given depositId, both in memory and on-chain. + * This method will first check memory for deposits, and if none are found, + * it will search on-chain for the deposit. + * @param depositId The unique ID of the deposit being queried. + * @returns Array of all deposits with the given depositId, including duplicates and on-chain deposits. + */ + public abstract findAllDeposits(depositId: BigNumber): Promise; + // /////////////////////// // // ABSTRACT METHODS // // /////////////////////// diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 70edd7336..48bc7af44 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -25,6 +25,10 @@ export type DepositSearchResult = | { found: true; deposit: DepositWithBlock } | { found: false; code: InvalidFill; reason: string }; +export type MultipleDepositSearchResult = + | { found: true; deposits: DepositWithBlock[] } + | { found: false; code: InvalidFill; reason: string }; + /** * Attempts to resolve a deposit for a fill. If the fill's deposit Id is within the spoke pool client's search range, * the deposit is returned immediately. Otherwise, the deposit is queried first from the provided cache, and if it is diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 362282667..470cafe1e 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -104,26 +104,28 @@ export function getMessageHash(message: string): string { return isMessageEmpty(message) ? ZERO_BYTES : keccak256(message as Hex); } -export function findInvalidFills(spokePoolClients: { [chainId: number]: SpokePoolClient }): InvalidFill[] { +export async function findInvalidFills(spokePoolClients: { + [chainId: number]: SpokePoolClient; +}): Promise { const invalidFills: InvalidFill[] = []; // Iterate through each spoke pool client - Object.values(spokePoolClients).forEach((spokePoolClient) => { + for (const spokePoolClient of Object.values(spokePoolClients)) { // Get all fills for this client const fills = spokePoolClient.getFills(); // Process each fill - fills.forEach((fill) => { + for (const fill of fills) { // Skip fills with unsafe deposit IDs if (isUnsafeDepositId(fill.depositId)) { - return; + continue; } - // Get all deposits (including duplicates) for this fill's depositId - const deposits = spokePoolClients[fill.originChainId].getDepositsForDepositId(fill.depositId); + // Get all deposits (including duplicates) for this fill's depositId, both in memory and on-chain + const depositResult = await spokePoolClients[fill.originChainId].findAllDeposits(fill.depositId); // If no deposits found at all - if (deposits.length === 0) { + if (!depositResult.found) { invalidFills.push({ fill, validationResults: [ @@ -132,14 +134,14 @@ export function findInvalidFills(spokePoolClients: { [chainId: number]: SpokePoo }, ], }); - return; + continue; } // Try to find a valid deposit for this fill let foundValidDeposit = false; const validationResults: Array<{ reason: string; deposit: DepositWithBlock }> = []; - for (const deposit of deposits) { + for (const deposit of depositResult.deposits) { // Validate the fill against the deposit const validationResult = validateFillForDeposit(fill, deposit); if (validationResult.valid) { @@ -159,8 +161,8 @@ export function findInvalidFills(spokePoolClients: { [chainId: number]: SpokePoo validationResults, }); } - }); - }); + } + } return invalidFills; } From 9727794e2f692c399cdd540ee0cd92133e201a9a Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Mon, 16 Jun 2025 13:48:32 +0200 Subject: [PATCH 04/14] Change variable naming --- .../SpokePoolClient/EVMSpokePoolClient.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts index 115d41cb7..40bb08ca6 100644 --- a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts @@ -234,9 +234,9 @@ export class EVMSpokePoolClient extends SpokePoolClient { public override async findAllDeposits(depositId: BigNumber): Promise { // First check memory for deposits - const memoryDeposits = this.getDepositsForDepositId(depositId); - if (memoryDeposits.length > 0) { - return { found: true, deposits: memoryDeposits }; + let deposits = this.getDepositsForDepositId(depositId); + if (deposits.length > 0) { + return { found: true, deposits: deposits }; } // If no deposits found in memory, try to find on-chain @@ -248,9 +248,9 @@ export class EVMSpokePoolClient extends SpokePoolClient { const { events: query, chain, elapsedMs } = result; // Find all events with matching depositId - const matchingEvents = query.filter(({ args }) => args["depositId"].eq(depositId)); + const events = query.filter(({ args }) => args["depositId"].eq(depositId)); - if (matchingEvents.length === 0) { + if (events.length === 0) { return { found: false, code: InvalidFill.DepositIdNotFound, @@ -259,7 +259,7 @@ export class EVMSpokePoolClient extends SpokePoolClient { } // First do all synchronous operations - const deposits = matchingEvents.map((event) => { + deposits = events.map((event) => { const deposit = { ...spreadEventWithBlockNumber(event), originChainId: this.chainId, @@ -277,7 +277,7 @@ export class EVMSpokePoolClient extends SpokePoolClient { }); // Then do all async operations in parallel - const enrichedDeposits = await Promise.all( + deposits = await Promise.all( deposits.map(async (deposit) => ({ ...deposit, quoteBlockNumber: await this.getBlockNumber(Number(deposit.quoteTimestamp)), @@ -287,11 +287,11 @@ export class EVMSpokePoolClient extends SpokePoolClient { this.logger.debug({ at: "SpokePoolClient#findAllDeposits", message: "Located deposits outside of SpokePoolClient's search range", - deposits: enrichedDeposits, + deposits: deposits, elapsedMs, }); - return { found: true, deposits: enrichedDeposits }; + return { found: true, deposits: deposits }; } public override getTimestampForBlock(blockNumber: number): Promise { From a54510a4f89307e13e921842ce5601c521df3fee Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Tue, 17 Jun 2025 14:09:02 +0200 Subject: [PATCH 05/14] Add unit tests --- test/SpokePoolClient.FindDeposits.ts | 221 +++++++++++++++++++++++++++ test/SpokeUtils.ts | 142 ++++++++++++++++- 2 files changed, 361 insertions(+), 2 deletions(-) create mode 100644 test/SpokePoolClient.FindDeposits.ts diff --git a/test/SpokePoolClient.FindDeposits.ts b/test/SpokePoolClient.FindDeposits.ts new file mode 100644 index 000000000..b2b1f5373 --- /dev/null +++ b/test/SpokePoolClient.FindDeposits.ts @@ -0,0 +1,221 @@ +import { EVMSpokePoolClient, SpokePoolClient } from "../src/clients"; +import { + bnOne, + toBN, + InvalidFill, + deploy as deployMulticall, +} from "../src/utils"; +import { CHAIN_ID_TEST_LIST, originChainId, destinationChainId, repaymentChainId } from "./constants"; +import { + expect, + BigNumber, + toBNWei, + ethers, + SignerWithAddress, + deposit, + setupTokensForWallet, + deploySpokePoolWithToken, + Contract, + createSpyLogger, + deployAndConfigureHubPool, + enableRoutesOnHubPool, + deployConfigStore, + getLastBlockTime, + winston, +} from "./utils"; +import { MockConfigStoreClient, MockHubPoolClient } from "./mocks"; +import sinon from "sinon"; + +describe("SpokePoolClient: Find Deposits", function () { + let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract, hubPool: Contract; + let owner: SignerWithAddress, depositor: SignerWithAddress, relayer: SignerWithAddress; + let spokePool1DeploymentBlock: number, spokePool2DeploymentBlock: number; + let l1Token: Contract, configStore: Contract; + let spyLogger: winston.Logger; + let spokePoolClient2: SpokePoolClient, hubPoolClient: MockHubPoolClient; + let spokePoolClient1: SpokePoolClient, configStoreClient: MockConfigStoreClient; + let inputToken: string, outputToken: string; + let inputAmount: BigNumber, outputAmount: BigNumber; + + beforeEach(async function () { + [owner, depositor, relayer] = await ethers.getSigners(); + await deployMulticall(owner); + ({ + spokePool: spokePool_1, + erc20: erc20_1, + deploymentBlock: spokePool1DeploymentBlock, + } = await deploySpokePoolWithToken(originChainId, destinationChainId)); + ({ + spokePool: spokePool_2, + erc20: erc20_2, + deploymentBlock: spokePool2DeploymentBlock, + } = await deploySpokePoolWithToken(destinationChainId, originChainId)); + ({ hubPool, l1Token_1: l1Token } = await deployAndConfigureHubPool(owner, [ + { l2ChainId: destinationChainId, spokePool: spokePool_2 }, + { l2ChainId: originChainId, spokePool: spokePool_1 }, + { l2ChainId: repaymentChainId, spokePool: spokePool_1 }, + { l2ChainId: 1, spokePool: spokePool_1 }, + ])); + await enableRoutesOnHubPool(hubPool, [ + { destinationChainId: originChainId, l1Token, destinationToken: erc20_1 }, + { destinationChainId: destinationChainId, l1Token, destinationToken: erc20_2 }, + ]); + ({ spy, spyLogger } = createSpyLogger()); + ({ configStore } = await deployConfigStore(owner, [l1Token])); + configStoreClient = new MockConfigStoreClient(spyLogger, configStore, undefined, undefined, CHAIN_ID_TEST_LIST); + await configStoreClient.update(); + hubPoolClient = new MockHubPoolClient(spyLogger, hubPool, configStoreClient); + hubPoolClient.setTokenMapping(l1Token.address, originChainId, erc20_1.address); + hubPoolClient.setTokenMapping(l1Token.address, destinationChainId, erc20_2.address); + await hubPoolClient.update(); + spokePoolClient1 = new EVMSpokePoolClient( + spyLogger, + spokePool_1, + hubPoolClient, + originChainId, + spokePool1DeploymentBlock + ); + spokePoolClient2 = new EVMSpokePoolClient( + createSpyLogger().spyLogger, + spokePool_2, + null, + destinationChainId, + spokePool2DeploymentBlock + ); + await setupTokensForWallet(spokePool_1, depositor, [erc20_1], undefined, 10); + await setupTokensForWallet(spokePool_2, relayer, [erc20_2], undefined, 10); + await spokePool_1.setCurrentTime(await getLastBlockTime(spokePool_1.provider)); + inputToken = erc20_1.address; + inputAmount = toBNWei(1); + outputToken = erc20_2.address; + outputAmount = inputAmount.sub(bnOne); + }); + + describe("findAllDeposits", function () { + it("finds deposits in memory and on-chain", async function () { + const depositEvent = await deposit( + spokePool_1, + destinationChainId, + depositor, + inputToken, + inputAmount, + outputToken, + outputAmount + ); + await spokePoolClient1.update(); + const result = await spokePoolClient1.findAllDeposits(depositEvent.depositId); + expect(result.found).to.be.true; + if (result.found) { + expect(result.deposits).to.have.lengthOf(1); + expect(result.deposits[0]).to.deep.include({ + depositId: depositEvent.depositId, + originChainId: depositEvent.originChainId, + destinationChainId: depositEvent.destinationChainId, + depositor: depositEvent.depositor, + recipient: depositEvent.recipient, + inputToken: depositEvent.inputToken, + outputToken: depositEvent.outputToken, + inputAmount: depositEvent.inputAmount, + outputAmount: depositEvent.outputAmount, + }); + } + }); + + it("returns empty result for non-existent deposit ID", async function () { + await spokePoolClient1.update(); + const nonExistentId = toBN(999999); + const result = await spokePoolClient1.findAllDeposits(nonExistentId); + expect(result.found).to.be.false; + if (!result.found) { + expect(result.code).to.equal(InvalidFill.DepositIdNotFound); + expect(result.reason).to.be.a("string"); + } + }); + + it("finds a single deposit for a given ID", async function () { + const depositEvent = await deposit( + spokePool_1, + destinationChainId, + depositor, + inputToken, + inputAmount, + outputToken, + outputAmount + ); + await spokePoolClient1.update(); + const result = await spokePoolClient1.findAllDeposits(depositEvent.depositId); + expect(result.found).to.be.true; + if (result.found) { + expect(result.deposits).to.have.lengthOf(1); + expect(result.deposits[0]).to.deep.include({ + depositId: depositEvent.depositId, + originChainId: depositEvent.originChainId, + destinationChainId: depositEvent.destinationChainId, + depositor: depositEvent.depositor, + recipient: depositEvent.recipient, + inputToken: depositEvent.inputToken, + outputToken: depositEvent.outputToken, + inputAmount: depositEvent.inputAmount, + outputAmount: depositEvent.outputAmount, + }); + } + }); + + it("simulates fetching a deposit from chain during update", async function () { + const depositEvent = await deposit( + spokePool_1, + destinationChainId, + depositor, + inputToken, + inputAmount, + outputToken, + outputAmount + ); + await spokePoolClient1.update(); + delete spokePoolClient1['depositHashes'][depositEvent.depositId.toString()]; + const filter = spokePool_1.filters.FundsDeposited(); + const fakeEvent = { + args: { + depositId: depositEvent.depositId, + originChainId: depositEvent.originChainId, + destinationChainId: depositEvent.destinationChainId, + depositor: depositEvent.depositor, + recipient: depositEvent.recipient, + inputToken: depositEvent.inputToken, + inputAmount: depositEvent.inputAmount, + outputToken: depositEvent.outputToken, + outputAmount: depositEvent.outputAmount, + quoteTimestamp: depositEvent.quoteTimestamp, + message: depositEvent.message, + fillDeadline: depositEvent.fillDeadline, + exclusivityDeadline: depositEvent.exclusivityDeadline, + exclusiveRelayer: depositEvent.exclusiveRelayer, + }, + blockNumber: depositEvent.blockNumber, + transactionHash: depositEvent.txnRef, + transactionIndex: depositEvent.txnIndex, + logIndex: depositEvent.logIndex, + }; + const queryFilterStub = sinon.stub(spokePool_1, "queryFilter"); + queryFilterStub.withArgs(filter).resolves([fakeEvent]); + await spokePoolClient1.update(); + const result = await spokePoolClient1.findAllDeposits(depositEvent.depositId); + expect(result.found).to.be.true; + if (result.found) { + expect(result.deposits).to.have.lengthOf(1); + expect(result.deposits[0]).to.deep.include({ + depositId: depositEvent.depositId, + originChainId: depositEvent.originChainId, + destinationChainId: depositEvent.destinationChainId, + depositor: depositEvent.depositor, + recipient: depositEvent.recipient, + inputToken: depositEvent.inputToken, + outputToken: depositEvent.outputToken, + inputAmount: depositEvent.inputAmount, + outputAmount: depositEvent.outputAmount, + }); + } + queryFilterStub.restore(); + }); + }); +}); \ No newline at end of file diff --git a/test/SpokeUtils.ts b/test/SpokeUtils.ts index bbab28cda..bc3b9359e 100644 --- a/test/SpokeUtils.ts +++ b/test/SpokeUtils.ts @@ -1,6 +1,6 @@ import { utils as ethersUtils } from "ethers"; -import { UNDEFINED_MESSAGE_HASH, ZERO_BYTES } from "../src/constants"; -import { getMessageHash, getRelayEventKey, keccak256, randomAddress, toBN, validateFillForDeposit } from "../src/utils"; +import { MAX_SAFE_DEPOSIT_ID, UNDEFINED_MESSAGE_HASH, ZERO_BYTES } from "../src/constants"; +import { findInvalidFills, getMessageHash, getRelayEventKey, keccak256, randomAddress, toBN, validateFillForDeposit } from "../src/utils"; import { expect } from "./utils"; const random = () => Math.round(Math.random() * 1e8); @@ -89,4 +89,142 @@ describe("SpokeUtils", function () { const message = randomBytes(); expect(getMessageHash(message)).to.equal(keccak256(message)); }); + + describe("findInvalidFills", function () { + let mockSpokePoolClient: any; + let mockSpokePoolClients: { [chainId: number]: any }; + + beforeEach(function () { + mockSpokePoolClient = { + getFills: () => [], + findAllDeposits: async () => ({ found: false, deposits: [] }), + }; + + mockSpokePoolClients = { + [sampleData.originChainId]: mockSpokePoolClient, + }; + }); + + it("returns empty array when no fills exist", async function () { + const invalidFills = await findInvalidFills(mockSpokePoolClients); + expect(invalidFills).to.be.an("array").that.is.empty; + }); + + it("skips fills with unsafe deposit IDs", async function () { + const unsafeDepositId = toBN(MAX_SAFE_DEPOSIT_ID).add(1); + mockSpokePoolClient.getFills = () => [{ + ...sampleData, + depositId: unsafeDepositId, + messageHash, + }]; + + const invalidFills = await findInvalidFills(mockSpokePoolClients); + expect(invalidFills).to.be.an("array").that.is.empty; + }); + + it("detects fills with no matching deposits", async function () { + mockSpokePoolClient.getFills = () => [{ + ...sampleData, + depositId: toBN(random()), + messageHash, + }]; + + const invalidFills = await findInvalidFills(mockSpokePoolClients); + expect(invalidFills).to.have.lengthOf(1); + expect(invalidFills[0].validationResults).to.have.lengthOf(1); + expect(invalidFills[0].validationResults[0].reason).to.include("no deposit with depositId"); + }); + + it("detects fills with mismatched deposit attributes", async function () { + const deposit = { + ...sampleData, + blockNumber: random(), + txnRef: randomBytes(), + txnIndex: random(), + logIndex: random(), + quoteTimestamp: random(), + quoteBlockNumber: random(), + fromLiteChain: false, + toLiteChain: false, + messageHash, + }; + + const fill = { + ...deposit, + recipient: randomAddress(), + relayer: randomAddress(), + repaymentChainId: random(), + relayExecutionInfo: { + updatedRecipient: randomAddress(), + updatedOutputAmount: deposit.outputAmount, + updatedMessageHash: deposit.messageHash, + fillType: 0, + }, + messageHash, + }; + + mockSpokePoolClient.getFills = () => [fill]; + mockSpokePoolClient.findAllDeposits = async () => ({ + found: true, + deposits: [deposit], + }); + + const invalidFills = await findInvalidFills(mockSpokePoolClients); + expect(invalidFills).to.have.lengthOf(1); + expect(invalidFills[0].validationResults).to.have.lengthOf(1); + expect(invalidFills[0].validationResults[0].reason).to.include("recipient mismatch"); + }); + + it("handles multiple fills with different validation results", async function () { + const validDeposit = { + ...sampleData, + blockNumber: random(), + txnRef: randomBytes(), + txnIndex: random(), + logIndex: random(), + quoteTimestamp: random(), + quoteBlockNumber: random(), + fromLiteChain: false, + toLiteChain: false, + messageHash, + }; + + const validFill = { + ...validDeposit, + relayer: randomAddress(), + repaymentChainId: random(), + relayExecutionInfo: { + updatedRecipient: validDeposit.recipient, + updatedOutputAmount: validDeposit.outputAmount, + updatedMessageHash: validDeposit.messageHash, + fillType: 0, + }, + messageHash, + }; + + const invalidFill = { + ...validDeposit, + recipient: randomAddress(), + relayer: randomAddress(), + repaymentChainId: random(), + relayExecutionInfo: { + updatedRecipient: randomAddress(), + updatedOutputAmount: validDeposit.outputAmount, + updatedMessageHash: validDeposit.messageHash, + fillType: 0, + }, + messageHash, + }; + + mockSpokePoolClient.getFills = () => [validFill, invalidFill]; + mockSpokePoolClient.findAllDeposits = async () => ({ + found: true, + deposits: [validDeposit], + }); + + const invalidFills = await findInvalidFills(mockSpokePoolClients); + expect(invalidFills).to.have.lengthOf(1); + expect(invalidFills[0].fill).to.deep.equal(invalidFill); + }); + }); }); From e6c9da30559fff3f372616d6d48b6b79a8164f44 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Tue, 17 Jun 2025 14:38:25 +0200 Subject: [PATCH 06/14] Lint Fix --- test/SpokePoolClient.FindDeposits.ts | 35 +++----- test/SpokeUtils.ts | 127 ++++++++++++++++++++------- 2 files changed, 107 insertions(+), 55 deletions(-) diff --git a/test/SpokePoolClient.FindDeposits.ts b/test/SpokePoolClient.FindDeposits.ts index b2b1f5373..5435a9f6e 100644 --- a/test/SpokePoolClient.FindDeposits.ts +++ b/test/SpokePoolClient.FindDeposits.ts @@ -1,10 +1,5 @@ import { EVMSpokePoolClient, SpokePoolClient } from "../src/clients"; -import { - bnOne, - toBN, - InvalidFill, - deploy as deployMulticall, -} from "../src/utils"; +import { bnOne, toBN, InvalidFill, deploy as deployMulticall } from "../src/utils"; import { CHAIN_ID_TEST_LIST, originChainId, destinationChainId, repaymentChainId } from "./constants"; import { expect, @@ -29,13 +24,13 @@ import sinon from "sinon"; describe("SpokePoolClient: Find Deposits", function () { let spokePool_1: Contract, erc20_1: Contract, spokePool_2: Contract, erc20_2: Contract, hubPool: Contract; let owner: SignerWithAddress, depositor: SignerWithAddress, relayer: SignerWithAddress; - let spokePool1DeploymentBlock: number, spokePool2DeploymentBlock: number; + let spokePool1DeploymentBlock: number; let l1Token: Contract, configStore: Contract; let spyLogger: winston.Logger; - let spokePoolClient2: SpokePoolClient, hubPoolClient: MockHubPoolClient; let spokePoolClient1: SpokePoolClient, configStoreClient: MockConfigStoreClient; let inputToken: string, outputToken: string; let inputAmount: BigNumber, outputAmount: BigNumber; + let hubPoolClient: MockHubPoolClient; beforeEach(async function () { [owner, depositor, relayer] = await ethers.getSigners(); @@ -44,12 +39,8 @@ describe("SpokePoolClient: Find Deposits", function () { spokePool: spokePool_1, erc20: erc20_1, deploymentBlock: spokePool1DeploymentBlock, - } = await deploySpokePoolWithToken(originChainId, destinationChainId)); - ({ - spokePool: spokePool_2, - erc20: erc20_2, - deploymentBlock: spokePool2DeploymentBlock, - } = await deploySpokePoolWithToken(destinationChainId, originChainId)); + } = await deploySpokePoolWithToken(originChainId)); + ({ spokePool: spokePool_2, erc20: erc20_2 } = await deploySpokePoolWithToken(destinationChainId)); ({ hubPool, l1Token_1: l1Token } = await deployAndConfigureHubPool(owner, [ { l2ChainId: destinationChainId, spokePool: spokePool_2 }, { l2ChainId: originChainId, spokePool: spokePool_1 }, @@ -60,7 +51,7 @@ describe("SpokePoolClient: Find Deposits", function () { { destinationChainId: originChainId, l1Token, destinationToken: erc20_1 }, { destinationChainId: destinationChainId, l1Token, destinationToken: erc20_2 }, ]); - ({ spy, spyLogger } = createSpyLogger()); + ({ spyLogger } = createSpyLogger()); ({ configStore } = await deployConfigStore(owner, [l1Token])); configStoreClient = new MockConfigStoreClient(spyLogger, configStore, undefined, undefined, CHAIN_ID_TEST_LIST); await configStoreClient.update(); @@ -75,13 +66,6 @@ describe("SpokePoolClient: Find Deposits", function () { originChainId, spokePool1DeploymentBlock ); - spokePoolClient2 = new EVMSpokePoolClient( - createSpyLogger().spyLogger, - spokePool_2, - null, - destinationChainId, - spokePool2DeploymentBlock - ); await setupTokensForWallet(spokePool_1, depositor, [erc20_1], undefined, 10); await setupTokensForWallet(spokePool_2, relayer, [erc20_2], undefined, 10); await spokePool_1.setCurrentTime(await getLastBlockTime(spokePool_1.provider)); @@ -172,7 +156,7 @@ describe("SpokePoolClient: Find Deposits", function () { outputAmount ); await spokePoolClient1.update(); - delete spokePoolClient1['depositHashes'][depositEvent.depositId.toString()]; + delete spokePoolClient1["depositHashes"][depositEvent.depositId.toString()]; const filter = spokePool_1.filters.FundsDeposited(); const fakeEvent = { args: { @@ -197,7 +181,8 @@ describe("SpokePoolClient: Find Deposits", function () { logIndex: depositEvent.logIndex, }; const queryFilterStub = sinon.stub(spokePool_1, "queryFilter"); - queryFilterStub.withArgs(filter).resolves([fakeEvent]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + queryFilterStub.withArgs(filter).resolves([fakeEvent as any]); await spokePoolClient1.update(); const result = await spokePoolClient1.findAllDeposits(depositEvent.depositId); expect(result.found).to.be.true; @@ -218,4 +203,4 @@ describe("SpokePoolClient: Find Deposits", function () { queryFilterStub.restore(); }); }); -}); \ No newline at end of file +}); diff --git a/test/SpokeUtils.ts b/test/SpokeUtils.ts index bc3b9359e..861aa189f 100644 --- a/test/SpokeUtils.ts +++ b/test/SpokeUtils.ts @@ -1,10 +1,40 @@ import { utils as ethersUtils } from "ethers"; import { MAX_SAFE_DEPOSIT_ID, UNDEFINED_MESSAGE_HASH, ZERO_BYTES } from "../src/constants"; -import { findInvalidFills, getMessageHash, getRelayEventKey, keccak256, randomAddress, toBN, validateFillForDeposit } from "../src/utils"; -import { expect } from "./utils"; +import { + findInvalidFills, + getMessageHash, + getRelayEventKey, + keccak256, + randomAddress, + toBN, + validateFillForDeposit, +} from "../src/utils"; +import { expect, deploySpokePoolWithToken, Contract } from "./utils"; +import { MockSpokePoolClient } from "./mocks"; +import winston from "winston"; const random = () => Math.round(Math.random() * 1e8); const randomBytes = () => `0x${ethersUtils.randomBytes(48).join("").slice(0, 64)}`; +const dummyLogger = winston.createLogger({ transports: [new winston.transports.Console()] }); + +const dummyFillProps = { + relayer: randomAddress(), + repaymentChainId: random(), + relayExecutionInfo: { + updatedRecipient: randomAddress(), + updatedOutputAmount: toBN(random()), + updatedMessageHash: ZERO_BYTES, + fillType: 0, + }, + blockNumber: random(), + txnRef: randomBytes(), + txnIndex: random(), + logIndex: random(), + quoteTimestamp: random(), + quoteBlockNumber: random(), + fromLiteChain: false, + toLiteChain: false, +}; describe("SpokeUtils", function () { const message = randomBytes(); @@ -24,8 +54,20 @@ describe("SpokeUtils", function () { fillDeadline: random(), exclusiveRelayer: randomAddress(), exclusivityDeadline: random(), + ...dummyFillProps, }; + let spokePool: Contract; + let deploymentBlock: number; + + beforeEach(async function () { + const { spokePool: _spokePool, deploymentBlock: _deploymentBlock } = await deploySpokePoolWithToken( + sampleData.originChainId + ); + spokePool = _spokePool; + deploymentBlock = _deploymentBlock; + }); + it("getRelayEventKey correctly concatenates an event key", function () { const eventKey = getRelayEventKey(sampleData); const expectedKey = @@ -91,13 +133,15 @@ describe("SpokeUtils", function () { }); describe("findInvalidFills", function () { - let mockSpokePoolClient: any; - let mockSpokePoolClients: { [chainId: number]: any }; + let mockSpokePoolClient: MockSpokePoolClient; + let mockSpokePoolClients: { [chainId: number]: MockSpokePoolClient }; beforeEach(function () { - mockSpokePoolClient = { - getFills: () => [], - findAllDeposits: async () => ({ found: false, deposits: [] }), + mockSpokePoolClient = new MockSpokePoolClient(dummyLogger, spokePool, sampleData.originChainId, deploymentBlock); + mockSpokePoolClient.getFills = () => []; + mockSpokePoolClient.findAllDeposits = async () => { + await Promise.resolve(); + return { found: false, code: 0, reason: "Deposit not found" }; }; mockSpokePoolClients = { @@ -112,22 +156,28 @@ describe("SpokeUtils", function () { it("skips fills with unsafe deposit IDs", async function () { const unsafeDepositId = toBN(MAX_SAFE_DEPOSIT_ID).add(1); - mockSpokePoolClient.getFills = () => [{ - ...sampleData, - depositId: unsafeDepositId, - messageHash, - }]; + mockSpokePoolClient.getFills = () => [ + { + ...sampleData, + depositId: unsafeDepositId, + messageHash, + ...dummyFillProps, + }, + ]; const invalidFills = await findInvalidFills(mockSpokePoolClients); expect(invalidFills).to.be.an("array").that.is.empty; }); it("detects fills with no matching deposits", async function () { - mockSpokePoolClient.getFills = () => [{ - ...sampleData, - depositId: toBN(random()), - messageHash, - }]; + mockSpokePoolClient.getFills = () => [ + { + ...sampleData, + depositId: toBN(random()), + messageHash, + ...dummyFillProps, + }, + ]; const invalidFills = await findInvalidFills(mockSpokePoolClients); expect(invalidFills).to.have.lengthOf(1); @@ -146,7 +196,14 @@ describe("SpokeUtils", function () { quoteBlockNumber: random(), fromLiteChain: false, toLiteChain: false, - messageHash, + relayer: randomAddress(), + repaymentChainId: random(), + relayExecutionInfo: { + updatedRecipient: randomAddress(), + updatedOutputAmount: sampleData.outputAmount, + updatedMessageHash: sampleData.messageHash, + fillType: 0, + }, }; const fill = { @@ -160,14 +217,16 @@ describe("SpokeUtils", function () { updatedMessageHash: deposit.messageHash, fillType: 0, }, - messageHash, }; mockSpokePoolClient.getFills = () => [fill]; - mockSpokePoolClient.findAllDeposits = async () => ({ - found: true, - deposits: [deposit], - }); + mockSpokePoolClient.findAllDeposits = async () => { + await Promise.resolve(); + return { + found: true, + deposits: [deposit], + }; + }; const invalidFills = await findInvalidFills(mockSpokePoolClients); expect(invalidFills).to.have.lengthOf(1); @@ -186,7 +245,14 @@ describe("SpokeUtils", function () { quoteBlockNumber: random(), fromLiteChain: false, toLiteChain: false, - messageHash, + relayer: randomAddress(), + repaymentChainId: random(), + relayExecutionInfo: { + updatedRecipient: sampleData.recipient, + updatedOutputAmount: sampleData.outputAmount, + updatedMessageHash: sampleData.messageHash, + fillType: 0, + }, }; const validFill = { @@ -199,7 +265,6 @@ describe("SpokeUtils", function () { updatedMessageHash: validDeposit.messageHash, fillType: 0, }, - messageHash, }; const invalidFill = { @@ -213,14 +278,16 @@ describe("SpokeUtils", function () { updatedMessageHash: validDeposit.messageHash, fillType: 0, }, - messageHash, }; mockSpokePoolClient.getFills = () => [validFill, invalidFill]; - mockSpokePoolClient.findAllDeposits = async () => ({ - found: true, - deposits: [validDeposit], - }); + mockSpokePoolClient.findAllDeposits = async () => { + await Promise.resolve(); + return { + found: true, + deposits: [validDeposit], + }; + }; const invalidFills = await findInvalidFills(mockSpokePoolClients); expect(invalidFills).to.have.lengthOf(1); From 64b60e32009033e7887d0e0824e56869dcc052c4 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Wed, 18 Jun 2025 11:53:02 +0200 Subject: [PATCH 07/14] Style fixes --- src/arch/svm/SpokeUtils.ts | 19 +++++++---------- .../SpokePoolClient/EVMSpokePoolClient.ts | 21 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/arch/svm/SpokeUtils.ts b/src/arch/svm/SpokeUtils.ts index 086c0d131..cf5769ee5 100644 --- a/src/arch/svm/SpokeUtils.ts +++ b/src/arch/svm/SpokeUtils.ts @@ -90,7 +90,7 @@ async function queryDepositEventsInWindow( ): Promise { // We can only perform this search when we have a safe deposit ID. if (isUnsafeDepositId(depositId)) { - throw new Error(`Cannot binary search for depositId ${depositId}`); + throw new Error(`Cannot find historical deposit for unsafe deposit ID ${depositId}.`); } const provider = eventClient.getRpc(); @@ -105,11 +105,11 @@ async function queryDepositEventsInWindow( const startSlot = endSlot - slotsInElapsed; // Query for the deposit events with this limited slot range - return eventClient.queryEvents("FundsDeposited", startSlot, endSlot) || []; + return eventClient.queryEvents("FundsDeposited", startSlot, endSlot); } /** - * Finds deposit events within a 2-day window ending at the specified slot. + * Finds deposit events within a time window (default 2 days) ending at the specified slot. * * @remarks * This implementation uses a slot-limited search approach because Solana PDA state has @@ -129,7 +129,8 @@ async function queryDepositEventsInWindow( * @important * This function may return `undefined` for valid deposit IDs that are older than the search * window (approximately 2 days before the specified slot). This is an acceptable limitation - * as deposits this old are typically not relevant to current operations. + * as deposits this old are typically not relevant to current operations. This can be an issue + * if no proposal was made for a chain over a period of > 1.5 days. * * @param eventClient - SvmCpiEventsClient instance * @param depositId - The deposit ID to search for @@ -167,7 +168,7 @@ export async function findDeposit( } /** - * Finds all deposit events within a 2-day window ending at the specified slot. + * Finds all deposit events within a time window (default 2 days) ending at the specified slot. * * @remarks * This implementation uses a slot-limited search approach because Solana PDA state has @@ -187,7 +188,8 @@ export async function findDeposit( * @important * This function may return an empty array for valid deposit IDs that are older than the search * window (approximately 2 days before the specified slot). This is an acceptable limitation - * as deposits this old are typically not relevant to current operations. + * as deposits this old are typically not relevant to current operations. This can be an issue + * if no proposal was made for a chain over a period of > 1.5 days. * * @param eventClient - SvmCpiEventsClient instance * @param depositId - The deposit ID to search for @@ -209,11 +211,6 @@ export async function findAllDeposits( depositId.eq((event.data as unknown as { depositId: BigNumber }).depositId) ); - // If no deposit events are found, return empty array - if (!matchingEvents || matchingEvents.length === 0) { - return []; - } - // Return all deposit events with block info return matchingEvents.map((event) => ({ txnRef: event.signature.toString(), diff --git a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts index 40bb08ca6..b496da156 100644 --- a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts @@ -149,7 +149,7 @@ export class EVMSpokePoolClient extends SpokePoolClient { private async queryDepositEvents( depositId: BigNumber - ): Promise<{ events: Log[]; from: number; chain: string; elapsedMs: number } | { reason: string }> { + ): Promise<{ events: Log[]; from: number; elapsedMs: number } | { reason: string }> { const tStart = Date.now(); const upperBound = this.latestHeightSearched || undefined; const from = await findDepositBlock(this.spokePool, depositId, this.deploymentBlock, upperBound); @@ -178,10 +178,12 @@ export class EVMSpokePoolClient extends SpokePoolClient { { from, to, maxLookBack } ), ]) - ).flat(); + ) + .flat() + .filter(({ args }) => args["depositId"].eq(depositId)); const tStop = Date.now(); - return { events, from, chain, elapsedMs: tStop - tStart }; + return { events, from, elapsedMs: tStop - tStart }; } public override async findDeposit(depositId: BigNumber): Promise { @@ -197,14 +199,14 @@ export class EVMSpokePoolClient extends SpokePoolClient { return { found: false, code: InvalidFill.DepositIdNotFound, reason: result.reason }; } - const { events: query, from, chain, elapsedMs } = result; + const { events: query, from, elapsedMs } = result; const event = query.find(({ args }) => args["depositId"].eq(depositId)); if (event === undefined) { return { found: false, code: InvalidFill.DepositIdNotFound, - reason: `${chain} depositId ${depositId} not found at block ${from}.`, + reason: `${getNetworkName(this.chainId)} depositId ${depositId} not found at block ${from}.`, }; } @@ -236,7 +238,7 @@ export class EVMSpokePoolClient extends SpokePoolClient { // First check memory for deposits let deposits = this.getDepositsForDepositId(depositId); if (deposits.length > 0) { - return { found: true, deposits: deposits }; + return { found: true, deposits }; } // If no deposits found in memory, try to find on-chain @@ -245,16 +247,13 @@ export class EVMSpokePoolClient extends SpokePoolClient { return { found: false, code: InvalidFill.DepositIdNotFound, reason: result.reason }; } - const { events: query, chain, elapsedMs } = result; - - // Find all events with matching depositId - const events = query.filter(({ args }) => args["depositId"].eq(depositId)); + const { events, elapsedMs } = result; if (events.length === 0) { return { found: false, code: InvalidFill.DepositIdNotFound, - reason: `${chain} depositId ${depositId} not found at block ${result.from}.`, + reason: `${getNetworkName(this.chainId)} depositId ${depositId} not found at block ${result.from}.`, }; } From 64bcaf4aca1e7556c001c2ee1623e961344d26a2 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Tue, 24 Jun 2025 11:10:43 +0200 Subject: [PATCH 08/14] Increase SDK version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 130c8c113..80b95abe3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.2.15", + "version": "4.2.16", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ From f9d939e0c89510228a95daa7e1bf92fcba695e5f Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Wed, 25 Jun 2025 11:35:33 +0200 Subject: [PATCH 09/14] Coding style fix --- src/clients/SpokePoolClient/EVMSpokePoolClient.ts | 2 +- src/clients/SpokePoolClient/SVMSpokePoolClient.ts | 3 ++- src/utils/SpokeUtils.ts | 9 +++++---- test/SpokePoolClient.FindDeposits.ts | 9 +++++---- test/SpokeUtils.ts | 8 +++----- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts index b496da156..da8cb20b5 100644 --- a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts @@ -290,7 +290,7 @@ export class EVMSpokePoolClient extends SpokePoolClient { elapsedMs, }); - return { found: true, deposits: deposits }; + return { found: true, deposits }; } public override getTimestampForBlock(blockNumber: number): Promise { diff --git a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts index 53bd28927..181fc0e35 100644 --- a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts @@ -16,6 +16,7 @@ import { BigNumber, DepositSearchResult, EventSearchConfig, + getNetworkName, InvalidFill, isZeroAddress, MakeOptional, @@ -247,7 +248,7 @@ export class SVMSpokePoolClient extends SpokePoolClient { return { found: false, code: InvalidFill.DepositIdNotFound, - reason: `Deposit with ID ${depositId} not found`, + reason: `${getNetworkName(this.chainId)} deposit with ID ${depositId} not found`, }; } diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 470cafe1e..1e0584d59 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -5,7 +5,7 @@ import { Deposit, DepositWithBlock, Fill, FillType, InvalidFill, RelayData, Slow import { toBytes32 } from "./AddressUtils"; import { BigNumber } from "./BigNumberUtils"; import { isMessageEmpty, validateFillForDeposit } from "./DepositUtils"; -import { chainIsSvm } from "./NetworkUtils"; +import { chainIsSvm, getNetworkName } from "./NetworkUtils"; import { svm } from "../arch"; import { SpokePoolClient } from "../clients"; @@ -117,20 +117,21 @@ export async function findInvalidFills(spokePoolClients: { // Process each fill for (const fill of fills) { // Skip fills with unsafe deposit IDs + // @TODO Deposits with unsafe depositIds should be processed after some time if (isUnsafeDepositId(fill.depositId)) { continue; } // Get all deposits (including duplicates) for this fill's depositId, both in memory and on-chain - const depositResult = await spokePoolClients[fill.originChainId].findAllDeposits(fill.depositId); + const depositResult = await spokePoolClients[fill.originChainId]?.findAllDeposits(fill.depositId); // If no deposits found at all - if (!depositResult.found) { + if (!depositResult?.found) { invalidFills.push({ fill, validationResults: [ { - reason: `no deposit with depositId ${fill.depositId} found`, + reason: `No ${getNetworkName(fill.originChainId)} deposit with depositId ${fill.depositId} found`, }, ], }); diff --git a/test/SpokePoolClient.FindDeposits.ts b/test/SpokePoolClient.FindDeposits.ts index 5435a9f6e..4764287f1 100644 --- a/test/SpokePoolClient.FindDeposits.ts +++ b/test/SpokePoolClient.FindDeposits.ts @@ -1,5 +1,5 @@ import { EVMSpokePoolClient, SpokePoolClient } from "../src/clients"; -import { bnOne, toBN, InvalidFill, deploy as deployMulticall } from "../src/utils"; +import { bnOne, toBN, InvalidFill, deploy as deployMulticall, getRelayEventKey } from "../src/utils"; import { CHAIN_ID_TEST_LIST, originChainId, destinationChainId, repaymentChainId } from "./constants"; import { expect, @@ -156,7 +156,8 @@ describe("SpokePoolClient: Find Deposits", function () { outputAmount ); await spokePoolClient1.update(); - delete spokePoolClient1["depositHashes"][depositEvent.depositId.toString()]; + const depositHash = getRelayEventKey(depositEvent); + delete spokePoolClient1["depositHashes"][depositHash]; const filter = spokePool_1.filters.FundsDeposited(); const fakeEvent = { args: { @@ -182,12 +183,12 @@ describe("SpokePoolClient: Find Deposits", function () { }; const queryFilterStub = sinon.stub(spokePool_1, "queryFilter"); // eslint-disable-next-line @typescript-eslint/no-explicit-any - queryFilterStub.withArgs(filter).resolves([fakeEvent as any]); + queryFilterStub.resolves([fakeEvent as any]); await spokePoolClient1.update(); const result = await spokePoolClient1.findAllDeposits(depositEvent.depositId); expect(result.found).to.be.true; if (result.found) { - expect(result.deposits).to.have.lengthOf(1); + expect(result.deposits).to.have.lengthOf(2); expect(result.deposits[0]).to.deep.include({ depositId: depositEvent.depositId, originChainId: depositEvent.originChainId, diff --git a/test/SpokeUtils.ts b/test/SpokeUtils.ts index 861aa189f..ef1a54e04 100644 --- a/test/SpokeUtils.ts +++ b/test/SpokeUtils.ts @@ -61,11 +61,9 @@ describe("SpokeUtils", function () { let deploymentBlock: number; beforeEach(async function () { - const { spokePool: _spokePool, deploymentBlock: _deploymentBlock } = await deploySpokePoolWithToken( + ({ spokePool, deploymentBlock } = await deploySpokePoolWithToken( sampleData.originChainId - ); - spokePool = _spokePool; - deploymentBlock = _deploymentBlock; + )); }); it("getRelayEventKey correctly concatenates an event key", function () { @@ -182,7 +180,7 @@ describe("SpokeUtils", function () { const invalidFills = await findInvalidFills(mockSpokePoolClients); expect(invalidFills).to.have.lengthOf(1); expect(invalidFills[0].validationResults).to.have.lengthOf(1); - expect(invalidFills[0].validationResults[0].reason).to.include("no deposit with depositId"); + expect(invalidFills[0].validationResults[0].reason).to.include("deposit with depositId"); }); it("detects fills with mismatched deposit attributes", async function () { From 1be82adc85272bca82b784e4c7b472e5eb325819 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Wed, 25 Jun 2025 11:40:31 +0200 Subject: [PATCH 10/14] Lint fix --- test/SpokePoolClient.FindDeposits.ts | 1 - test/SpokeUtils.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/test/SpokePoolClient.FindDeposits.ts b/test/SpokePoolClient.FindDeposits.ts index 4764287f1..c289920d0 100644 --- a/test/SpokePoolClient.FindDeposits.ts +++ b/test/SpokePoolClient.FindDeposits.ts @@ -158,7 +158,6 @@ describe("SpokePoolClient: Find Deposits", function () { await spokePoolClient1.update(); const depositHash = getRelayEventKey(depositEvent); delete spokePoolClient1["depositHashes"][depositHash]; - const filter = spokePool_1.filters.FundsDeposited(); const fakeEvent = { args: { depositId: depositEvent.depositId, diff --git a/test/SpokeUtils.ts b/test/SpokeUtils.ts index ef1a54e04..16b505539 100644 --- a/test/SpokeUtils.ts +++ b/test/SpokeUtils.ts @@ -61,9 +61,7 @@ describe("SpokeUtils", function () { let deploymentBlock: number; beforeEach(async function () { - ({ spokePool, deploymentBlock } = await deploySpokePoolWithToken( - sampleData.originChainId - )); + ({ spokePool, deploymentBlock } = await deploySpokePoolWithToken(sampleData.originChainId)); }); it("getRelayEventKey correctly concatenates an event key", function () { From be74e90e53819575e2658d848b1e1756a3067fa2 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Wed, 25 Jun 2025 12:02:29 +0200 Subject: [PATCH 11/14] Use fixed value DepositIdNotFound for MultipleDepositSearchResult --- src/utils/DepositUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/DepositUtils.ts b/src/utils/DepositUtils.ts index 48bc7af44..d5a00380e 100644 --- a/src/utils/DepositUtils.ts +++ b/src/utils/DepositUtils.ts @@ -27,7 +27,7 @@ export type DepositSearchResult = export type MultipleDepositSearchResult = | { found: true; deposits: DepositWithBlock[] } - | { found: false; code: InvalidFill; reason: string }; + | { found: false; code: InvalidFill.DepositIdNotFound; reason: string }; /** * Attempts to resolve a deposit for a fill. If the fill's deposit Id is within the spoke pool client's search range, From 4f60a53982ee76e5075219402a0ecc3e548800c2 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Tue, 1 Jul 2025 12:45:21 +0200 Subject: [PATCH 12/14] Use Address class instead of strings for addresses --- .../SpokePoolClient/EVMSpokePoolClient.ts | 11 ++- .../SpokePoolClient/SVMSpokePoolClient.ts | 2 +- src/utils/SpokeUtils.ts | 5 +- test/SpokePoolClient.FindDeposits.ts | 83 +++++++++---------- test/SpokeUtils.ts | 38 +++++---- 5 files changed, 72 insertions(+), 67 deletions(-) diff --git a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts index eed690f00..26870cf45 100644 --- a/src/clients/SpokePoolClient/EVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/EVMSpokePoolClient.ts @@ -266,12 +266,17 @@ export class EVMSpokePoolClient extends SpokePoolClient { deposits = events.map((event) => { const deposit = { ...spreadEventWithBlockNumber(event), + inputToken: toAddressType(event.args.inputToken, event.args.originChainId), + outputToken: toAddressType(event.args.outputToken, event.args.destinationChainId), + depositor: toAddressType(event.args.depositor, this.chainId), + recipient: toAddressType(event.args.recipient, event.args.destinationChainId), + exclusiveRelayer: toAddressType(event.args.exclusiveRelayer, event.args.destinationChainId), originChainId: this.chainId, - fromLiteChain: true, - toLiteChain: true, + fromLiteChain: true, // To be updated immediately afterwards. + toLiteChain: true, // To be updated immediately afterwards. } as DepositWithBlock; - if (isZeroAddress(deposit.outputToken)) { + if (deposit.outputToken.isZeroAddress()) { deposit.outputToken = this.getDestinationTokenForDeposit(deposit); } deposit.fromLiteChain = this.isOriginLiteChain(deposit); diff --git a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts index 6162fb615..79b3674d4 100644 --- a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts @@ -257,7 +257,7 @@ export class SVMSpokePoolClient extends SpokePoolClient { originChainId: this.chainId, fromLiteChain: this.isOriginLiteChain(deposit), toLiteChain: this.isDestinationLiteChain(deposit), - outputToken: isZeroAddress(deposit.outputToken) + outputToken: deposit.outputToken.isZeroAddress() ? this.getDestinationTokenForDeposit(deposit) : deposit.outputToken, })) diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 8e5eabfa3..857de7977 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -1,8 +1,7 @@ import { encodeAbiParameters, Hex, keccak256 } from "viem"; import { fixedPointAdjustment as fixedPoint } from "./common"; -import { MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS, ZERO_BYTES } from "../constants"; -import { Deposit, DepositWithBlock, Fill, FillType, InvalidFill, RelayData, SlowFillLeaf } from "../interfaces"; -import { toBytes32 } from "./AddressUtils"; +import { MAX_SAFE_DEPOSIT_ID, ZERO_BYTES } from "../constants"; +import { DepositWithBlock, Fill, FillType, InvalidFill, RelayData, SlowFillLeaf } from "../interfaces"; import { BigNumber } from "./BigNumberUtils"; import { isMessageEmpty, validateFillForDeposit } from "./DepositUtils"; import { chainIsSvm, getNetworkName } from "./NetworkUtils"; diff --git a/test/SpokePoolClient.FindDeposits.ts b/test/SpokePoolClient.FindDeposits.ts index c289920d0..457ba398d 100644 --- a/test/SpokePoolClient.FindDeposits.ts +++ b/test/SpokePoolClient.FindDeposits.ts @@ -1,5 +1,5 @@ import { EVMSpokePoolClient, SpokePoolClient } from "../src/clients"; -import { bnOne, toBN, InvalidFill, deploy as deployMulticall, getRelayEventKey } from "../src/utils"; +import { bnOne, toBN, InvalidFill, deploy as deployMulticall, getRelayEventKey, toAddressType, Address } from "../src/utils"; import { CHAIN_ID_TEST_LIST, originChainId, destinationChainId, repaymentChainId } from "./constants"; import { expect, @@ -28,7 +28,7 @@ describe("SpokePoolClient: Find Deposits", function () { let l1Token: Contract, configStore: Contract; let spyLogger: winston.Logger; let spokePoolClient1: SpokePoolClient, configStoreClient: MockConfigStoreClient; - let inputToken: string, outputToken: string; + let inputToken: Address, outputToken: Address; let inputAmount: BigNumber, outputAmount: BigNumber; let hubPoolClient: MockHubPoolClient; @@ -69,9 +69,9 @@ describe("SpokePoolClient: Find Deposits", function () { await setupTokensForWallet(spokePool_1, depositor, [erc20_1], undefined, 10); await setupTokensForWallet(spokePool_2, relayer, [erc20_2], undefined, 10); await spokePool_1.setCurrentTime(await getLastBlockTime(spokePool_1.provider)); - inputToken = erc20_1.address; + inputToken = toAddressType(erc20_1.address, originChainId); inputAmount = toBNWei(1); - outputToken = erc20_2.address; + outputToken = toAddressType(erc20_2.address, destinationChainId); outputAmount = inputAmount.sub(bnOne); }); @@ -91,17 +91,16 @@ describe("SpokePoolClient: Find Deposits", function () { expect(result.found).to.be.true; if (result.found) { expect(result.deposits).to.have.lengthOf(1); - expect(result.deposits[0]).to.deep.include({ - depositId: depositEvent.depositId, - originChainId: depositEvent.originChainId, - destinationChainId: depositEvent.destinationChainId, - depositor: depositEvent.depositor, - recipient: depositEvent.recipient, - inputToken: depositEvent.inputToken, - outputToken: depositEvent.outputToken, - inputAmount: depositEvent.inputAmount, - outputAmount: depositEvent.outputAmount, - }); + const foundDeposit = result.deposits[0]; + expect(foundDeposit.depositId).to.equal(depositEvent.depositId); + expect(foundDeposit.originChainId).to.equal(depositEvent.originChainId); + expect(foundDeposit.destinationChainId).to.equal(depositEvent.destinationChainId); + expect(foundDeposit.depositor.eq(depositEvent.depositor)).to.be.true; + expect(foundDeposit.recipient.eq(depositEvent.recipient)).to.be.true; + expect(foundDeposit.inputToken.eq(depositEvent.inputToken)).to.be.true; + expect(foundDeposit.outputToken.eq(depositEvent.outputToken)).to.be.true; + expect(foundDeposit.inputAmount).to.equal(depositEvent.inputAmount); + expect(foundDeposit.outputAmount).to.equal(depositEvent.outputAmount); } }); @@ -131,17 +130,16 @@ describe("SpokePoolClient: Find Deposits", function () { expect(result.found).to.be.true; if (result.found) { expect(result.deposits).to.have.lengthOf(1); - expect(result.deposits[0]).to.deep.include({ - depositId: depositEvent.depositId, - originChainId: depositEvent.originChainId, - destinationChainId: depositEvent.destinationChainId, - depositor: depositEvent.depositor, - recipient: depositEvent.recipient, - inputToken: depositEvent.inputToken, - outputToken: depositEvent.outputToken, - inputAmount: depositEvent.inputAmount, - outputAmount: depositEvent.outputAmount, - }); + const foundDeposit = result.deposits[0]; + expect(foundDeposit.depositId).to.equal(depositEvent.depositId); + expect(foundDeposit.originChainId).to.equal(depositEvent.originChainId); + expect(foundDeposit.destinationChainId).to.equal(depositEvent.destinationChainId); + expect(foundDeposit.depositor.eq(depositEvent.depositor)).to.be.true; + expect(foundDeposit.recipient.eq(depositEvent.recipient)).to.be.true; + expect(foundDeposit.inputToken.eq(depositEvent.inputToken)).to.be.true; + expect(foundDeposit.outputToken.eq(depositEvent.outputToken)).to.be.true; + expect(foundDeposit.inputAmount).to.equal(depositEvent.inputAmount); + expect(foundDeposit.outputAmount).to.equal(depositEvent.outputAmount); } }); @@ -163,23 +161,25 @@ describe("SpokePoolClient: Find Deposits", function () { depositId: depositEvent.depositId, originChainId: depositEvent.originChainId, destinationChainId: depositEvent.destinationChainId, - depositor: depositEvent.depositor, - recipient: depositEvent.recipient, - inputToken: depositEvent.inputToken, + // These are bytes32 strings, as emitted by the contract event + depositor: depositEvent.depositor.toBytes32(), + recipient: depositEvent.recipient.toBytes32(), + inputToken: depositEvent.inputToken.toBytes32(), inputAmount: depositEvent.inputAmount, - outputToken: depositEvent.outputToken, + outputToken: depositEvent.outputToken.toBytes32(), outputAmount: depositEvent.outputAmount, quoteTimestamp: depositEvent.quoteTimestamp, message: depositEvent.message, fillDeadline: depositEvent.fillDeadline, exclusivityDeadline: depositEvent.exclusivityDeadline, - exclusiveRelayer: depositEvent.exclusiveRelayer, + exclusiveRelayer: depositEvent.exclusiveRelayer.toBytes32(), }, blockNumber: depositEvent.blockNumber, transactionHash: depositEvent.txnRef, transactionIndex: depositEvent.txnIndex, logIndex: depositEvent.logIndex, }; + // Note: This matches the contract event output, and the client will convert these to Address objects internally. const queryFilterStub = sinon.stub(spokePool_1, "queryFilter"); // eslint-disable-next-line @typescript-eslint/no-explicit-any queryFilterStub.resolves([fakeEvent as any]); @@ -188,17 +188,16 @@ describe("SpokePoolClient: Find Deposits", function () { expect(result.found).to.be.true; if (result.found) { expect(result.deposits).to.have.lengthOf(2); - expect(result.deposits[0]).to.deep.include({ - depositId: depositEvent.depositId, - originChainId: depositEvent.originChainId, - destinationChainId: depositEvent.destinationChainId, - depositor: depositEvent.depositor, - recipient: depositEvent.recipient, - inputToken: depositEvent.inputToken, - outputToken: depositEvent.outputToken, - inputAmount: depositEvent.inputAmount, - outputAmount: depositEvent.outputAmount, - }); + const foundDeposit = result.deposits[0]; + expect(foundDeposit.depositId).to.equal(depositEvent.depositId); + expect(foundDeposit.originChainId).to.equal(depositEvent.originChainId); + expect(foundDeposit.destinationChainId).to.equal(depositEvent.destinationChainId); + expect(foundDeposit.depositor.eq(depositEvent.depositor)).to.be.true; + expect(foundDeposit.recipient.eq(depositEvent.recipient)).to.be.true; + expect(foundDeposit.inputToken.eq(depositEvent.inputToken)).to.be.true; + expect(foundDeposit.outputToken.eq(depositEvent.outputToken)).to.be.true; + expect(foundDeposit.inputAmount).to.equal(depositEvent.inputAmount); + expect(foundDeposit.outputAmount).to.equal(depositEvent.outputAmount); } queryFilterStub.restore(); }); diff --git a/test/SpokeUtils.ts b/test/SpokeUtils.ts index 16b505539..b936b033a 100644 --- a/test/SpokeUtils.ts +++ b/test/SpokeUtils.ts @@ -8,6 +8,8 @@ import { randomAddress, toBN, validateFillForDeposit, + toAddressType, + InvalidFill, } from "../src/utils"; import { expect, deploySpokePoolWithToken, Contract } from "./utils"; import { MockSpokePoolClient } from "./mocks"; @@ -18,10 +20,10 @@ const randomBytes = () => `0x${ethersUtils.randomBytes(48).join("").slice(0, 64) const dummyLogger = winston.createLogger({ transports: [new winston.transports.Console()] }); const dummyFillProps = { - relayer: randomAddress(), + relayer: toAddressType(randomAddress(), 1), repaymentChainId: random(), relayExecutionInfo: { - updatedRecipient: randomAddress(), + updatedRecipient: toAddressType(randomAddress(), 1), updatedOutputAmount: toBN(random()), updatedMessageHash: ZERO_BYTES, fillType: 0, @@ -42,17 +44,17 @@ describe("SpokeUtils", function () { const sampleData = { originChainId: random(), destinationChainId: random(), - depositor: randomAddress(), - recipient: randomAddress(), - inputToken: randomAddress(), + depositor: toAddressType(randomAddress(), 1), + recipient: toAddressType(randomAddress(), 1), + inputToken: toAddressType(randomAddress(), 1), inputAmount: toBN(random()), - outputToken: randomAddress(), + outputToken: toAddressType(randomAddress(), 1), outputAmount: toBN(random()), message, messageHash, depositId: toBN(random()), fillDeadline: random(), - exclusiveRelayer: randomAddress(), + exclusiveRelayer: toAddressType(randomAddress(), 1), exclusivityDeadline: random(), ...dummyFillProps, }; @@ -137,7 +139,7 @@ describe("SpokeUtils", function () { mockSpokePoolClient.getFills = () => []; mockSpokePoolClient.findAllDeposits = async () => { await Promise.resolve(); - return { found: false, code: 0, reason: "Deposit not found" }; + return { found: false, code: InvalidFill.DepositIdNotFound, reason: "Deposit not found" }; }; mockSpokePoolClients = { @@ -192,10 +194,10 @@ describe("SpokeUtils", function () { quoteBlockNumber: random(), fromLiteChain: false, toLiteChain: false, - relayer: randomAddress(), + relayer: toAddressType(randomAddress(), 1), repaymentChainId: random(), relayExecutionInfo: { - updatedRecipient: randomAddress(), + updatedRecipient: toAddressType(randomAddress(), 1), updatedOutputAmount: sampleData.outputAmount, updatedMessageHash: sampleData.messageHash, fillType: 0, @@ -204,11 +206,11 @@ describe("SpokeUtils", function () { const fill = { ...deposit, - recipient: randomAddress(), - relayer: randomAddress(), + recipient: toAddressType(randomAddress(), 1), + relayer: toAddressType(randomAddress(), 1), repaymentChainId: random(), relayExecutionInfo: { - updatedRecipient: randomAddress(), + updatedRecipient: toAddressType(randomAddress(), 1), updatedOutputAmount: deposit.outputAmount, updatedMessageHash: deposit.messageHash, fillType: 0, @@ -241,7 +243,7 @@ describe("SpokeUtils", function () { quoteBlockNumber: random(), fromLiteChain: false, toLiteChain: false, - relayer: randomAddress(), + relayer: toAddressType(randomAddress(), 1), repaymentChainId: random(), relayExecutionInfo: { updatedRecipient: sampleData.recipient, @@ -253,7 +255,7 @@ describe("SpokeUtils", function () { const validFill = { ...validDeposit, - relayer: randomAddress(), + relayer: toAddressType(randomAddress(), 1), repaymentChainId: random(), relayExecutionInfo: { updatedRecipient: validDeposit.recipient, @@ -265,11 +267,11 @@ describe("SpokeUtils", function () { const invalidFill = { ...validDeposit, - recipient: randomAddress(), - relayer: randomAddress(), + recipient: toAddressType(randomAddress(), 1), + relayer: toAddressType(randomAddress(), 1), repaymentChainId: random(), relayExecutionInfo: { - updatedRecipient: randomAddress(), + updatedRecipient: toAddressType(randomAddress(), 1), updatedOutputAmount: validDeposit.outputAmount, updatedMessageHash: validDeposit.messageHash, fillType: 0, From 276d648275232d5b8f10de7052a01182c4a21ae5 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Tue, 1 Jul 2025 12:49:57 +0200 Subject: [PATCH 13/14] Lint fix and version increase --- package.json | 2 +- test/SpokePoolClient.FindDeposits.ts | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0e8418a1d..c3513a058 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@across-protocol/sdk", "author": "UMA Team", - "version": "4.3.3", + "version": "4.3.4", "license": "AGPL-3.0", "homepage": "https://docs.across.to/reference/sdk", "files": [ diff --git a/test/SpokePoolClient.FindDeposits.ts b/test/SpokePoolClient.FindDeposits.ts index 457ba398d..9ddc2ae78 100644 --- a/test/SpokePoolClient.FindDeposits.ts +++ b/test/SpokePoolClient.FindDeposits.ts @@ -1,5 +1,13 @@ import { EVMSpokePoolClient, SpokePoolClient } from "../src/clients"; -import { bnOne, toBN, InvalidFill, deploy as deployMulticall, getRelayEventKey, toAddressType, Address } from "../src/utils"; +import { + bnOne, + toBN, + InvalidFill, + deploy as deployMulticall, + getRelayEventKey, + toAddressType, + Address, +} from "../src/utils"; import { CHAIN_ID_TEST_LIST, originChainId, destinationChainId, repaymentChainId } from "./constants"; import { expect, From cc1dfadf03db9bc978c283ce4995dd138d33d7c7 Mon Sep 17 00:00:00 2001 From: Aleksa Colovic Date: Wed, 2 Jul 2025 17:08:12 +0200 Subject: [PATCH 14/14] Lint fix --- src/interfaces/SpokePool.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/interfaces/SpokePool.ts b/src/interfaces/SpokePool.ts index 5340628a8..220e5958c 100644 --- a/src/interfaces/SpokePool.ts +++ b/src/interfaces/SpokePool.ts @@ -78,7 +78,6 @@ export interface Fill extends Omit { relayExecutionInfo: RelayExecutionEventInfo; } - export interface InvalidFill { fill: FillWithBlock; validationResults: Array<{