From cad03dfba8f9fd0bc3a7cddec7405b547c9035d4 Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Mon, 10 Jun 2024 12:44:25 -0700 Subject: [PATCH] Adds retry on watchContractEvents --- .changeset/giant-frogs-collect.md | 5 ++ .../src/event/actions/watch-events.ts | 22 +++++--- .../utils/any-evm/is-eip155-enforced.test.ts | 36 +++++++++++-- packages/thirdweb/src/utils/retry.test.ts | 51 +++++++++++++++++++ packages/thirdweb/src/utils/retry.ts | 29 +++++++++++ 5 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 .changeset/giant-frogs-collect.md create mode 100644 packages/thirdweb/src/utils/retry.test.ts create mode 100644 packages/thirdweb/src/utils/retry.ts diff --git a/.changeset/giant-frogs-collect.md b/.changeset/giant-frogs-collect.md new file mode 100644 index 00000000000..122aa164eb4 --- /dev/null +++ b/.changeset/giant-frogs-collect.md @@ -0,0 +1,5 @@ +--- +"thirdweb": patch +--- + +Automatic retries on watchContractEvents diff --git a/packages/thirdweb/src/event/actions/watch-events.ts b/packages/thirdweb/src/event/actions/watch-events.ts index ea77933b258..b12010a1863 100644 --- a/packages/thirdweb/src/event/actions/watch-events.ts +++ b/packages/thirdweb/src/event/actions/watch-events.ts @@ -5,6 +5,7 @@ import { } from "./get-events.js"; import { watchBlockNumber } from "../../rpc/watchBlockNumber.js"; +import { retry } from "../../utils/retry.js"; import type { Prettify } from "../../utils/type-utils.js"; import type { PreparedEvent } from "../prepare-event.js"; import type { ParseEventLogsResult } from "./parse-logs.js"; @@ -68,13 +69,20 @@ export function watchContractEvents< * @internal */ onNewBlockNumber: async (blockNumber) => { - const logs = await getContractEvents({ - ...options, - // fromBlock is inclusive - fromBlock: blockNumber, - // toBlock is inclusive - toBlock: blockNumber, - }); + const logs = await retry( + async () => + getContractEvents({ + ...options, + // fromBlock is inclusive + fromBlock: blockNumber, + // toBlock is inclusive + toBlock: blockNumber, + }), + { + retries: 3, + delay: 500, + }, + ); // if there were any logs associated with our event(s) if (logs.length) { options.onEvents(logs); diff --git a/packages/thirdweb/src/utils/any-evm/is-eip155-enforced.test.ts b/packages/thirdweb/src/utils/any-evm/is-eip155-enforced.test.ts index 0655dcc5bc7..558bc5216ef 100644 --- a/packages/thirdweb/src/utils/any-evm/is-eip155-enforced.test.ts +++ b/packages/thirdweb/src/utils/any-evm/is-eip155-enforced.test.ts @@ -1,17 +1,38 @@ -import { describe, expect, it } from "vitest"; -import { ANVIL_CHAIN } from "../../../test/src/chains.js"; +import { beforeEach } from "node:test"; +import { afterAll, describe, expect, it, vi } from "vitest"; +import { + ANVIL_CHAIN, + FORKED_ETHEREUM_CHAIN, +} from "../../../test/src/chains.js"; import { TEST_CLIENT } from "../../../test/src/test-clients.js"; -import { base } from "../../chains/chain-definitions/base.js"; +import * as ethSendRawTransaction from "../../rpc/actions/eth_sendRawTransaction.js"; import { isEIP155Enforced } from "./is-eip155-enforced.js"; +const ethSendRawTransactionSpy = vi.spyOn( + ethSendRawTransaction, + "eth_sendRawTransaction", +); + // skip this test suite if there is no secret key available to test with // TODO: remove reliance on secret key during unit tests entirely describe.runIf(process.env.TW_SECRET_KEY)("isEIP155Enforced", () => { + afterAll(() => { + vi.restoreAllMocks(); + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + it("should return true if EIP-155 is enforced", async () => { + ethSendRawTransactionSpy.mockRejectedValueOnce({ + code: -32003, + message: "eip155", + }); + // Call the isEIP155Enforced function with a chain that enforces EIP-155 const result = await isEIP155Enforced({ - // optimism enforce eip155 - chain: base, + chain: FORKED_ETHEREUM_CHAIN, client: TEST_CLIENT, }); @@ -20,6 +41,11 @@ describe.runIf(process.env.TW_SECRET_KEY)("isEIP155Enforced", () => { }); it("should return false if EIP-155 is not enforced", async () => { + ethSendRawTransactionSpy.mockRejectedValueOnce({ + code: -32003, + message: "Insufficient funds for gas * price + value", + }); + // Call the isEIP155Enforced function with a chain that does not enforce EIP-155 const result = await isEIP155Enforced({ // localhost does not enforce eip155 diff --git a/packages/thirdweb/src/utils/retry.test.ts b/packages/thirdweb/src/utils/retry.test.ts new file mode 100644 index 00000000000..a6774e309a7 --- /dev/null +++ b/packages/thirdweb/src/utils/retry.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from "vitest"; +import { retry } from "./retry.js"; + +describe("retry", () => { + it("should successfully resolve the promise without retries if no error is thrown", async () => { + const mockFn = vi.fn().mockResolvedValue("success"); + await expect(retry(mockFn, { retries: 1, delay: 100 })).resolves.toBe( + "success", + ); + expect(mockFn).toHaveBeenCalledTimes(1); + }); + + it("should retry the specified number of times on failure", async () => { + const error = new Error("Test error"); + const mockFn = vi + .fn() + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error) + .mockResolvedValue("success"); + + await expect(retry(mockFn, { retries: 3, delay: 0 })).resolves.toBe( + "success", + ); + expect(mockFn).toHaveBeenCalledTimes(3); + }); + + it("should fail after exceeding the retry limit", async () => { + const error = new Error("Persistent error"); + const mockFn = vi.fn().mockRejectedValue(error); + + await expect(retry(mockFn, { retries: 2, delay: 0 })).rejects.toThrow(); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it("should respect the delay between retries", async () => { + const error = new Error("Test error with delay"); + const mockFn = vi + .fn() + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error) + .mockResolvedValue("success"); + + const delay = 100; + const startTime = Date.now(); + await retry(mockFn, { retries: 3, delay }); + const endTime = Date.now(); + + expect(endTime - startTime).toBeGreaterThanOrEqual(2 * delay); + expect(mockFn).toHaveBeenCalledTimes(3); + }); +}); diff --git a/packages/thirdweb/src/utils/retry.ts b/packages/thirdweb/src/utils/retry.ts new file mode 100644 index 00000000000..104050c2621 --- /dev/null +++ b/packages/thirdweb/src/utils/retry.ts @@ -0,0 +1,29 @@ +/** + * Attempts to execute a function that returns a promise and retries if the function throws an error. + * + * @param {Function} fn - A function that returns a promise to be executed. + * @param {Object} options - Configuration options for the retry behavior. + * @param {number} [options.retries=1] - The number of times to retry the function before failing. + * @param {number} [options.delay=0] - The delay in milliseconds between retries. + * @returns {Promise} The result of the function execution if successful. + */ + +export async function retry( + fn: () => Promise, + options: { retries?: number; delay?: number }, +): Promise { + const retries = options.retries ?? 1; + const delay = options.delay ?? 0; + let lastError: Error | null = null; + for (let i = 0; i < retries; i++) { + try { + return await fn(); + } catch (error) { + lastError = error as Error; + if (delay > 0) { + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + throw lastError; +}