How to initialize a RetryProvider for multiple RPC endpoints in ethers.js

58 views Asked by At

I'm trying to create a RetryProvider function that will initialize a connection, send a request to an RPC endpoint, and if the response is not valid (due to rate limits, bad url, etc), increment the URL index of rpcUrls by 1 and tries again with the next URL.

import { ethers } from "ethers";

const rpcUrls = {
  1: {
    0: "https://badurl.eth.com/rpc",
    1: "https://rpc.ankr.com/eth/5b280...",
    2: "https://go.getblock.io/01941...",
    3: "https://lb.nodies.app/v1/5e9daed36...",
    4: "https://eth-mainnet.public.blastapi.io",
    5: "https://ethereum.publicnode.com",
    6: "https://eth-mainnet.g.alchemy.com/v2/SoDlWkD...",
  },
};

class RetryJsonRpcProvider extends ethers.JsonRpcProvider {
  constructor(rpcUrls, chainId) {
    super(rpcUrls[0], chainId);
    this.rpcUrls = rpcUrls;
    this.currentIndex = 0;
    this.chainId = chainId;
  }

  async send(method, params) {
    try {
      console.log(
        `Attempting request with URL: ${this.rpcUrls[this.currentIndex]}`
      );
      return await super.send(method, params);
    } catch (error) {
      console.log(
        `Request failed for URL: ${this.rpcUrls[this.currentIndex]}. Reason: ${
          error.message
        }`
      );
      this.currentIndex++;
      if (this.currentIndex >= this.rpcUrls.length) {
        console.log("All RPC URLs failed.");
        throw new Error("All RPC URLs failed.");
      } else {
        console.log(
          `Retrying with next URL: ${this.rpcUrls[this.currentIndex]}`
        );
        // Recreate the provider with the next URL
        const nextProvider = new ethers.JsonRpcProvider(
          this.rpcUrls[this.currentIndex],
          this.chainId
        );
        return nextProvider.send(method, params);
      }
    }
  }
}

function createRetryProvider(chainId) {
  const urls = Object.values(rpcUrls[chainId]);
  return new RetryJsonRpcProvider(urls, chainId);
}

export const providers = {
  1: createRetryProvider(1),
  56: createRetryProvider(56),
  137: createRetryProvider(137),
  42161: createRetryProvider(42161),
  10: createRetryProvider(10),
  8453: createRetryProvider(8453),
};

and I'm getting this response.

Attempting request with URL: https://badurl.eth.com/rpc
Attempting request with URL: https://badurl.eth.com/rpc
Request failed for URL: https://badurl.eth.com/rpc. Reason: getaddrinfo ENOTFOUND badurl.eth.com
Retrying with next URL: https://rpc.ankr.com/eth/5b280c7544a...
Request failed for URL: https://rpc.ankr.com/eth/5b280c7544a... Reason: getaddrinfo ENOTFOUND badurl.eth.com
Retrying with next URL: https://go.getblock.io/019412da1d9...
Attempting request with URL: https://go.getblock.io/019412da1d9...
Attempting request with URL: https://go.getblock.io/019412da1d9...
Request failed for URL: https://go.getblock.io/019412da1d9... Reason: getaddrinfo ENOTFOUND badurl.eth.com
Retrying with next URL: https://lb.nodies.app/v1/5e9daed367d145...
Request failed for URL: https://lb.nodies.app/v1/5e9daed367d145.... Reason: getaddrinfo ENOTFOUND badurl.eth.com
Retrying with next URL: https://eth-mainnet.public.blastapi.io
Attempting request with URL: https://eth-mainnet.public.blastapi.io
Attempting request with URL: https://eth-mainnet.public.blastapi.io
Request failed for URL: https://eth-mainnet.public.blastapi.io. Reason: getaddrinfo ENOTFOUND badurl.eth.com
Retrying with next URL: https://ethereum.publicnode.com
Request failed for URL: https://ethereum.publicnode.com. Reason: getaddrinfo ENOTFOUND badurl.eth.com
Retrying with next URL: https://eth-mainnet.g.alchemy.com/v2/SoD...

It appears that once the initial URL fails, it tries to increment the URL by 1, but still shows an error for the failing URL.

  • The end goal is that when the first URL fails, it correctly checks the next URL in the index
  • each URL DOES work when used by itself, so I know its not an RPC connection error
  • It seems like its not incrementing the URLs appropriately because each URL fails with an error related to the badurl.eth.com address.
  • I know this is more of a general JS question, but any help would be really appreciated
  • oh, and I'm calling providers externally via provider = providers[chainId]
1

There are 1 answers

0
Juan Manuel Villarraza On

Sharing a simple approach I made that achieve:

Giving a list of rpcs

  • spread the load between them
  • retriable, if a call fail, then try with different rpc, if all of them faild, then throw last error (can be optimized and even toggled)
  • it works for all kind of rpc calls
import { providers } from "ethers"; // v.5.7

export class RetriableStaticJsonRpcProvider extends providers.StaticJsonRpcProvider {
  providerList: providers.StaticJsonRpcProvider[];
  currentIndex = 0;
  error: any;

  constructor(rpcs: string[], chainId: number) {
    super({ url: rpcs[0] }, chainId);

    this.providerList = rpcs.map(url => new providers.StaticJsonRpcProvider({ url }, chainId));
  }

  async send(method: string, params: Array<any>, retries?: number): Promise<any> {
    let _retries = retries || 0;

    /**
     * validate retries before continue
     * base case of recursivity (throw if already try all rpcs)
     */
    this.validateRetries(_retries);

    try {
      // select properly provider
      const provider = this.selectProvider();

      // send rpc call
      return await provider.send(method, params);
    } catch (error) {
      // store error internally
      this.error = error;

      // increase retries
      _retries = _retries + 1;

      return this.send(method, params, _retries);
    }
  }

  private selectProvider() {
    // last rpc from the list
    if (this.currentIndex === this.providerList.length) {
      // set currentIndex to the seconds element
      this.currentIndex = 1;
      return this.providerList[0];
    }

    // select current provider
    const provider = this.providerList[this.currentIndex];
    // increase counter
    this.currentIndex = this.currentIndex + 1;

    return provider;
  }

  /**
   * validate that retries is equal to the length of rpc
   * to ensure rpc are called at least one time
   *
   * if that's the case, and we fail in all the calls
   * then throw the internal saved error
   */
  private validateRetries(retries: number) {
    if (retries === this.providerList.length) {
      const error = this.error;
      this.error = undefined;
      throw new Error(error);
    }
  }
}