diff --git a/packages/sdk/src/actions/getFillByDepositTx.ts b/packages/sdk/src/actions/getFillByDepositTx.ts index 875f7c9..eb78d37 100644 --- a/packages/sdk/src/actions/getFillByDepositTx.ts +++ b/packages/sdk/src/actions/getFillByDepositTx.ts @@ -9,7 +9,7 @@ import { TransactionReceipt, } from "viem"; import { MAINNET_INDEXER_API } from "../constants/index.js"; -import { NoFillLogError } from "../errors/index.js"; +import { NoFillLogError, WaitForFillTimeoutError } from "../errors/index.js"; import { FillEventLog, IndexerStatusResponse } from "../types/index.js"; import { parseFillLogs } from "./waitForFillTx.js"; @@ -127,26 +127,60 @@ export type FillStatus = { parsedFillEvent: FillEventLog; }; +// Default max wait for a fill before rejecting (5 minutes). +export const DEFAULT_WAIT_FOR_FILL_TIMEOUT_MS = 300_000; + export async function waitForFillByDepositTx( params: GetFillByDepositTxParams & { pollingInterval?: number; + timeout?: number; }, ): ReturnType { const interval = - params?.pollingInterval ?? params.destinationChainClient.pollingInterval; + params.pollingInterval ?? params.destinationChainClient.pollingInterval; + const timeoutMs = params.timeout ?? DEFAULT_WAIT_FOR_FILL_TIMEOUT_MS; + const deadline = Date.now() + timeoutMs; - return new Promise((res) => { + return new Promise((resolve, reject) => { const poll = () => { getFillByDepositTx(params) .then((response) => { if (response.fillTxReceipt) { - res(response); - } else { - setTimeout(poll, interval); + resolve(response); + return; + } + + if (Date.now() >= deadline) { + reject( + new WaitForFillTimeoutError( + BigInt(params.deposit.depositId), + params.deposit.destinationChainId, + timeoutMs, + params.deposit.depositTxHash, + ), + ); + return; } + + setTimeout(poll, interval); }) .catch((error) => { - params?.logger ? params.logger.error(error) : console.log(error); + params.logger ? params.logger.error(error) : console.log(error); + + if (Date.now() >= deadline) { + reject( + error instanceof NoFillLogError + ? new WaitForFillTimeoutError( + BigInt(params.deposit.depositId), + params.deposit.destinationChainId, + timeoutMs, + params.deposit.depositTxHash, + ) + : error, + ); + return; + } + setTimeout(poll, interval); }); }; diff --git a/packages/sdk/src/errors/index.ts b/packages/sdk/src/errors/index.ts index 4d23831..edc5995 100644 --- a/packages/sdk/src/errors/index.ts +++ b/packages/sdk/src/errors/index.ts @@ -136,3 +136,18 @@ export class NoFillLogError extends Error { this.name = "Fill Log Not Found"; } } + +export class WaitForFillTimeoutError extends Error { + constructor( + depositId: bigint, + chainId: number, + timeoutMs: number, + depositTxHash?: Hash, + ) { + super( + `Timed out after ${timeoutMs}ms waiting for fill on chain ${chainId} for deposit id #${depositId.toString()}${depositTxHash ? ` with depositTxHash ${depositTxHash}` : "."}`, + ); + this.name = "WaitForFillTimeout"; + } +} + diff --git a/packages/sdk/test/unit/actions/waitForFillByDepositTx.test.ts b/packages/sdk/test/unit/actions/waitForFillByDepositTx.test.ts new file mode 100644 index 0000000..4e34f53 --- /dev/null +++ b/packages/sdk/test/unit/actions/waitForFillByDepositTx.test.ts @@ -0,0 +1,57 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + waitForFillByDepositTx, + DEFAULT_WAIT_FOR_FILL_TIMEOUT_MS, +} from "../../../src/actions/getFillByDepositTx.js"; +import { WaitForFillTimeoutError } from "../../../src/errors/index.js"; +import type { PublicClient } from "viem"; + +describe("waitForFillByDepositTx", () => { + beforeEach(() => { + vi.stubGlobal( + "fetch", + vi.fn().mockRejectedValue(new Error("indexer unavailable")), + ); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + test("rejects with WaitForFillTimeoutError when no fill appears before timeout", async () => { + const destinationChainClient = { + pollingInterval: 20, + getLogs: vi.fn().mockResolvedValue([]), + getTransactionReceipt: vi.fn(), + getBlock: vi.fn(), + } as unknown as PublicClient; + + await expect( + waitForFillByDepositTx({ + deposit: { + depositId: 42n, + originChainId: 1, + destinationChainId: 10, + destinationSpokePoolAddress: + "0x0000000000000000000000000000000000000001", + message: "0x", + }, + destinationChainClient, + indexerUrl: "http://127.0.0.1:9", + timeout: 80, + pollingInterval: 20, + logger: { + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + }, + }), + ).rejects.toBeInstanceOf(WaitForFillTimeoutError); + }); + + test("exports a finite default timeout so callers are not left polling forever", () => { + expect(DEFAULT_WAIT_FOR_FILL_TIMEOUT_MS).toBeGreaterThan(0); + expect(DEFAULT_WAIT_FOR_FILL_TIMEOUT_MS).toBe(300_000); + }); +});