diff --git a/libs/providers/src/exceptions/index.ts b/libs/providers/src/exceptions/index.ts index d5ef006..2935114 100644 --- a/libs/providers/src/exceptions/index.ts +++ b/libs/providers/src/exceptions/index.ts @@ -1,3 +1,4 @@ export * from "./invalidArgument.exception"; export * from "./dataDecode.exception"; export * from "./multicallNotFound.exception"; +export * from "./rpcUrlsEmpty.exception"; diff --git a/libs/providers/src/exceptions/rpcUrlsEmpty.exception.ts b/libs/providers/src/exceptions/rpcUrlsEmpty.exception.ts new file mode 100644 index 0000000..c57457f --- /dev/null +++ b/libs/providers/src/exceptions/rpcUrlsEmpty.exception.ts @@ -0,0 +1,6 @@ +export class RpcUrlsEmpty extends Error { + constructor() { + super("RPC URLs array cannot be empty"); + this.name = "RpcUrlsEmpty"; + } +} diff --git a/libs/providers/src/providers/evmProvider.service.ts b/libs/providers/src/providers/evmProvider.service.ts index f7e4c75..c2b3502 100644 --- a/libs/providers/src/providers/evmProvider.service.ts +++ b/libs/providers/src/providers/evmProvider.service.ts @@ -15,6 +15,8 @@ import { DecodeAbiParametersReturnType, encodeDeployData, EstimateGasParameters, + fallback, + FallbackTransport, GetBlockReturnType, Hex, http, @@ -28,6 +30,7 @@ import { DataDecodeException, InvalidArgumentException, MulticallNotFound, + RpcUrlsEmpty, } from "@zkchainhub/providers/exceptions"; import { AbiWithConstructor } from "@zkchainhub/providers/types"; @@ -36,16 +39,22 @@ import { AbiWithConstructor } from "@zkchainhub/providers/types"; */ @Injectable() export class EvmProviderService { - private client: ReturnType>; + private client: ReturnType< + typeof createPublicClient, Chain> + >; constructor( - rpcUrl: string, + rpcUrls: string[], readonly chain: Chain, @Inject(WINSTON_MODULE_NEST_PROVIDER) private readonly logger: LoggerService, ) { + if (rpcUrls.length === 0) { + throw new RpcUrlsEmpty(); + } + this.client = createPublicClient({ chain, - transport: http(rpcUrl), + transport: fallback(rpcUrls.map((rpcUrl) => http(rpcUrl))), }); } diff --git a/libs/providers/src/providers/zkChainProvider.service.ts b/libs/providers/src/providers/zkChainProvider.service.ts index 1fedede..6987ee2 100644 --- a/libs/providers/src/providers/zkChainProvider.service.ts +++ b/libs/providers/src/providers/zkChainProvider.service.ts @@ -1,6 +1,14 @@ import { Inject, Injectable, LoggerService } from "@nestjs/common"; import { WINSTON_MODULE_NEST_PROVIDER } from "nest-winston"; -import { Chain, Client, createClient, http, HttpTransport } from "viem"; +import { + Chain, + Client, + createClient, + fallback, + FallbackTransport, + http, + HttpTransport, +} from "viem"; import { GetL1BatchDetailsReturnType, PublicActionsL2, publicActionsL2 } from "viem/zksync"; import { InvalidArgumentException } from "@zkchainhub/providers/exceptions"; @@ -11,15 +19,24 @@ import { EvmProviderService } from "@zkchainhub/providers/providers/evmProvider. * Acts as a wrapper around Viem library to provide methods to interact with ZK chains. */ export class ZKChainProviderService extends EvmProviderService { - private zkClient: Client; + private zkClient: Client< + FallbackTransport, + Chain, + undefined, + undefined, + PublicActionsL2 + >; constructor( - rpcUrl: string, + rpcUrls: string[], chain: Chain, @Inject(WINSTON_MODULE_NEST_PROVIDER) logger: LoggerService, ) { - super(rpcUrl, chain, logger); - this.zkClient = createClient({ chain, transport: http(rpcUrl) }).extend(publicActionsL2()); + super(rpcUrls, chain, logger); + this.zkClient = createClient({ + chain, + transport: fallback(rpcUrls.map((rpcUrl) => http(rpcUrl))), + }).extend(publicActionsL2()); } /** diff --git a/libs/providers/test/unit/providers/evmProvider.service.spec.ts b/libs/providers/test/unit/providers/evmProvider.service.spec.ts index 0cbdf0b..0a020dc 100644 --- a/libs/providers/test/unit/providers/evmProvider.service.spec.ts +++ b/libs/providers/test/unit/providers/evmProvider.service.spec.ts @@ -7,7 +7,11 @@ import { localhost } from "viem/chains"; import { Logger } from "winston"; import { EvmProviderService } from "@zkchainhub/providers"; -import { DataDecodeException, MulticallNotFound } from "@zkchainhub/providers/exceptions"; +import { + DataDecodeException, + MulticallNotFound, + RpcUrlsEmpty, +} from "@zkchainhub/providers/exceptions"; import { arrayAbiFixture, structAbiFixture, @@ -48,8 +52,8 @@ describe("EvmProviderService", () => { { provide: EvmProviderService, useFactory: () => { - const rpcUrl = "http://localhost:8545"; - return new EvmProviderService(rpcUrl, mockChain, mockLogger as Logger); + const rpcUrls = ["http://localhost:8545"]; + return new EvmProviderService(rpcUrls, mockChain, mockLogger as Logger); }, }, ], @@ -63,6 +67,12 @@ describe("EvmProviderService", () => { jest.resetModules(); }); + it("throws RpcUrlsEmpty error if rpcUrls is empty", () => { + expect(() => { + new EvmProviderService([], mockChain, mockLogger as Logger); + }).toThrowError(RpcUrlsEmpty); + }); + describe("getBalance", () => { it("should return the balance of the specified address", async () => { const address = "0x123456789"; diff --git a/libs/providers/test/unit/providers/zkChainProvider.service.spec.ts b/libs/providers/test/unit/providers/zkChainProvider.service.spec.ts index 8b37b41..f1a2e0d 100644 --- a/libs/providers/test/unit/providers/zkChainProvider.service.spec.ts +++ b/libs/providers/test/unit/providers/zkChainProvider.service.spec.ts @@ -6,7 +6,7 @@ import { GetL1BatchDetailsReturnType } from "viem/zksync"; import { Logger } from "winston"; import { ZKChainProviderService } from "@zkchainhub/providers"; -import { InvalidArgumentException } from "@zkchainhub/providers/exceptions"; +import { InvalidArgumentException, RpcUrlsEmpty } from "@zkchainhub/providers/exceptions"; export const mockLogger: Partial = { log: jest.fn(), @@ -28,9 +28,9 @@ describe("ZKChainProviderService", () => { { provide: ZKChainProviderService, useFactory: () => { - const rpcUrl = "http://localhost:8545"; + const rpcUrls = ["http://localhost:8545"]; const chain = localhost; - return new ZKChainProviderService(rpcUrl, chain, mockLogger as Logger); + return new ZKChainProviderService(rpcUrls, chain, mockLogger as Logger); }, }, ], @@ -43,6 +43,12 @@ describe("ZKChainProviderService", () => { jest.clearAllMocks(); }); + it("throws RpcUrlsEmpty error if rpcUrls is empty", () => { + expect(() => { + new ZKChainProviderService([], localhost, mockLogger as Logger); + }).toThrowError(RpcUrlsEmpty); + }); + describe("avgBlockTime", () => { it("should return the average block time over the given range", async () => { const currentBlockNumber = 1000;