diff --git a/typescript/infra/src/config/chain.ts b/typescript/infra/src/config/chain.ts index 879f4276a..0b2bb911c 100644 --- a/typescript/infra/src/config/chain.ts +++ b/typescript/infra/src/config/chain.ts @@ -3,7 +3,7 @@ import { NonceManager } from '@ethersproject/experimental'; import { StaticCeloJsonRpcProvider } from 'celo-ethers-provider'; import { ethers } from 'ethers'; -import { ChainName } from '@abacus-network/sdk'; +import { ChainName, RetryJsonRpcProvider } from '@abacus-network/sdk'; import { getSecretDeployerKey, getSecretRpcEndpoint } from '../agents'; @@ -17,7 +17,10 @@ export async function fetchProvider( const celoChainNames = new Set(['alfajores', 'baklava', 'celo']); const provider = celoChainNames.has(chainName) ? new StaticCeloJsonRpcProvider(rpc) - : new ethers.providers.JsonRpcProvider(rpc); + : new RetryJsonRpcProvider(new ethers.providers.JsonRpcProvider(rpc), { + retryLimit: 2, + interval: 250, + }); return provider; } diff --git a/typescript/sdk/src/ethers/index.ts b/typescript/sdk/src/ethers/index.ts new file mode 100644 index 000000000..c3a246b54 --- /dev/null +++ b/typescript/sdk/src/ethers/index.ts @@ -0,0 +1 @@ +export { RetryProvider, RetryJsonRpcProvider } from './retry-provider'; diff --git a/typescript/sdk/src/ethers/retry-provider.ts b/typescript/sdk/src/ethers/retry-provider.ts new file mode 100644 index 000000000..9fbd20b01 --- /dev/null +++ b/typescript/sdk/src/ethers/retry-provider.ts @@ -0,0 +1,54 @@ +// RetryProvider +// +// Mostly taken from the removed version that was in ethers.js +// See: https://github.com/ethers-io/ethers.js/discussions/3006 +import { BaseProvider, JsonRpcProvider } from '@ethersproject/providers'; +import { ethers } from 'ethers'; + +import { retryAsync } from '@abacus-network/utils/dist/src/utils'; + +export type RetryOptions = { + // The wait interval in between + interval: number; + + // Maximum number of times to rety + retryLimit: number; +}; + +export class RetryProvider extends BaseProvider { + constructor( + readonly provider: BaseProvider, + readonly retryOptions: RetryOptions, + ) { + super(provider.getNetwork()); + ethers.utils.defineReadOnly(this, 'provider', provider); + ethers.utils.defineReadOnly(this, 'retryOptions', retryOptions); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + perform(method: string, params: any): Promise { + return retryAsync( + () => this.provider.perform(method, params), + this.retryOptions.retryLimit, + this.retryOptions.interval, + ); + } +} + +// Need this separate class for JsonRpcProvider to still expose `getSigner`, so will retry at the request level +export class RetryJsonRpcProvider extends JsonRpcProvider { + constructor( + readonly provider: JsonRpcProvider, + readonly retryOptions: RetryOptions, + ) { + super(provider.connection, provider.network); + } + + async send(method: string, params: Array): Promise { + return retryAsync( + async () => this.provider.send(method, params), + this.retryOptions.retryLimit, + this.retryOptions.interval, + ); + } +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index e229938c5..ed9c7c656 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -57,3 +57,4 @@ export { TestChainNames, } from './types'; export { objMap, objMapEntries, promiseObjAll, utils } from './utils'; +export { RetryProvider, RetryJsonRpcProvider } from './ethers'; diff --git a/typescript/utils/src/utils.ts b/typescript/utils/src/utils.ts index e2d9cc68a..c1aafa832 100644 --- a/typescript/utils/src/utils.ts +++ b/typescript/utils/src/utils.ts @@ -90,3 +90,26 @@ export function domainHash(domain: number): string { [domain, 'ABACUS'], ); } + +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// Retries an async function when it raises an exeption +// if all the tries fail it raises the last thrown exeption +export async function retryAsync( + runner: () => T, + attempts = 3, + delay = 500, +) { + let saveError; + for (let i = 0; i < attempts; i++) { + try { + return runner(); + } catch (error) { + saveError = error; + await sleep(delay * (i + 1)); + } + } + throw saveError; +}