diff --git a/packages/chain-providers/src/providers/evmProvider.service.ts b/packages/chain-providers/src/providers/evmProvider.service.ts index 74dad52..248843c 100644 --- a/packages/chain-providers/src/providers/evmProvider.service.ts +++ b/packages/chain-providers/src/providers/evmProvider.service.ts @@ -39,12 +39,12 @@ import { */ export class EvmProvider { private client: ReturnType< - typeof createPublicClient, Chain> + typeof createPublicClient, Chain | undefined> >; constructor( rpcUrls: string[], - readonly chain: Chain, + readonly chain: Chain | undefined, private readonly logger: ILogger, ) { if (rpcUrls.length === 0) { @@ -62,7 +62,7 @@ export class EvmProvider { * @returns {Address | undefined} The address of the Multicall3 contract, or undefined if not found. */ getMulticall3Address(): Address | undefined { - return this.chain.contracts?.multicall3?.address; + return this.chain?.contracts?.multicall3?.address; } /** @@ -201,7 +201,7 @@ export class EvmProvider { >( args: MulticallParameters, ): Promise> { - if (!this.chain.contracts?.multicall3?.address) throw new MulticallNotFound(); + if (!this.chain?.contracts?.multicall3?.address) throw new MulticallNotFound(); return this.client.multicall(args); } diff --git a/packages/chain-providers/src/providers/zkChainProvider.service.ts b/packages/chain-providers/src/providers/zkChainProvider.service.ts index 1392f34..b9360f5 100644 --- a/packages/chain-providers/src/providers/zkChainProvider.service.ts +++ b/packages/chain-providers/src/providers/zkChainProvider.service.ts @@ -7,7 +7,12 @@ import { http, HttpTransport, } from "viem"; -import { GetL1BatchDetailsReturnType, PublicActionsL2, publicActionsL2 } from "viem/zksync"; +import { + GetL1BatchBlockRangeReturnParameters, + GetL1BatchDetailsReturnType, + PublicActionsL2, + publicActionsL2, +} from "viem/zksync"; import { ILogger } from "@zkchainhub/shared"; @@ -20,13 +25,13 @@ import { EvmProvider } from "./evmProvider.service.js"; export class ZKChainProvider extends EvmProvider { private zkClient: Client< FallbackTransport, - Chain, + Chain | undefined, undefined, undefined, PublicActionsL2 >; - constructor(rpcUrls: string[], chain: Chain, logger: ILogger) { + constructor(rpcUrls: string[], logger: ILogger, chain: Chain | undefined = undefined) { super(rpcUrls, chain, logger); this.zkClient = createClient({ chain, @@ -51,6 +56,18 @@ export class ZKChainProvider extends EvmProvider { return parseInt((await this.zkClient.getL1BatchNumber()).toString(), 16); } + /** + * Retrieves the block range for a given L1 batch number. + * + * @param l1BatchNumber - The L1 batch number. + * @returns A promise that resolves to the block range for the specified L1 batch number. + */ + async getL1BatchBlockRange( + l1BatchNumber: number, + ): Promise { + return this.zkClient.getL1BatchBlockRange({ l1BatchNumber }); + } + /** * Calculates the average block time over a specified range. * @param range The number of blocks to consider for calculating the average block time. Default is 1000. diff --git a/packages/chain-providers/test/unit/providers/zkChainProvider.service.spec.ts b/packages/chain-providers/test/unit/providers/zkChainProvider.service.spec.ts index 0420adc..a356d79 100644 --- a/packages/chain-providers/test/unit/providers/zkChainProvider.service.spec.ts +++ b/packages/chain-providers/test/unit/providers/zkChainProvider.service.spec.ts @@ -24,19 +24,19 @@ describe("ZKChainProvider", () => { }); it("has a zkclient property defined", () => { - zkProvider = new ZKChainProvider(defaultRpcUrls, defaultMockChain, mockLogger); + zkProvider = new ZKChainProvider(defaultRpcUrls, mockLogger, defaultMockChain); expect(zkProvider["zkClient"]).toBeDefined(); }); it("throws RpcUrlsEmpty error if rpcUrls is empty", () => { expect(() => { - new ZKChainProvider([], localhost, mockLogger); + new ZKChainProvider([], mockLogger, localhost); }).toThrowError(RpcUrlsEmpty); }); describe("avgBlockTime", () => { it("should return the average block time over the given range", async () => { - zkProvider = new ZKChainProvider(defaultRpcUrls, defaultMockChain, mockLogger); + zkProvider = new ZKChainProvider(defaultRpcUrls, mockLogger, defaultMockChain); const currentBlockNumber = 1000; const range = 100; const currentBlockTimestamp = { timestamp: BigInt(123234345) }; @@ -64,7 +64,7 @@ describe("ZKChainProvider", () => { }); it("should throw an InvalidArgumentException if the range is less than 1", async () => { - zkProvider = new ZKChainProvider(defaultRpcUrls, defaultMockChain, mockLogger); + zkProvider = new ZKChainProvider(defaultRpcUrls, mockLogger, defaultMockChain); await expect(zkProvider.avgBlockTime(0)).rejects.toThrowError( new InvalidArgumentException("range for avgBlockTime should be >= 1"), ); @@ -73,7 +73,7 @@ describe("ZKChainProvider", () => { describe("tps", () => { it("should return the transactions per second (TPS)", async () => { - zkProvider = new ZKChainProvider(defaultRpcUrls, defaultMockChain, mockLogger); + zkProvider = new ZKChainProvider(defaultRpcUrls, mockLogger, defaultMockChain); const currentBatchNumber = 1000; // 1000 in hexadecimal const currentBatchDetails = { l2TxCount: 200, timestamp: 123234345 }; const prevBatchDetails = { timestamp: 123123123 }; @@ -99,7 +99,7 @@ describe("ZKChainProvider", () => { }); it("should handle the case when there are no transactions", async () => { - zkProvider = new ZKChainProvider(defaultRpcUrls, defaultMockChain, mockLogger); + zkProvider = new ZKChainProvider(defaultRpcUrls, mockLogger, defaultMockChain); const currentBatchNumber = 1000; // 1000 in hexadecimal const currentBatchDetails = { l2TxCount: 0, timestamp: 123234345 }; const prevBatchDetails = { timestamp: 123123123 }; @@ -124,4 +124,21 @@ describe("ZKChainProvider", () => { expect(zkProvider.getL1BatchDetails).toHaveBeenCalledWith(999); }); }); + + describe("getL1BatchBlockRange", () => { + it("should return the block range for the specified L1 batch number", async () => { + zkProvider = new ZKChainProvider(defaultRpcUrls, mockLogger, defaultMockChain); + const l1BatchNumber = 1000; + const blockRange: [number, number] = [5000, 6000]; + + vi.spyOn(zkProvider["zkClient"], "getL1BatchBlockRange").mockResolvedValue(blockRange); + + const result = await zkProvider.getL1BatchBlockRange(l1BatchNumber); + + expect(result).toEqual(blockRange); + expect(zkProvider["zkClient"].getL1BatchBlockRange).toHaveBeenCalledWith({ + l1BatchNumber, + }); + }); + }); }); diff --git a/packages/chain-providers/vitest.config.ts b/packages/chain-providers/vitest.config.ts index 3aa3262..7f9df03 100644 --- a/packages/chain-providers/vitest.config.ts +++ b/packages/chain-providers/vitest.config.ts @@ -1,5 +1,5 @@ import path from "path"; -import { defineConfig } from "vitest/config"; +import { configDefaults, defineConfig } from "vitest/config"; export default defineConfig({ test: { @@ -10,7 +10,13 @@ export default defineConfig({ coverage: { provider: "v8", reporter: ["text", "json", "html"], // Coverage reporters - exclude: ["node_modules", "dist"], // Files to exclude from coverage + exclude: [ + "node_modules", + "dist", + "src/index.ts", + "**/external.ts", + ...configDefaults.exclude, + ], // Files to exclude from coverage }, }, resolve: { diff --git a/packages/metrics/src/external.ts b/packages/metrics/src/external.ts index e250302..7284d63 100644 --- a/packages/metrics/src/external.ts +++ b/packages/metrics/src/external.ts @@ -2,4 +2,4 @@ export type { FeeParams, GasInfo, AssetTvl } from "./internal.js"; export { InvalidChainId, InvalidChainType, L1MetricsServiceException } from "./internal.js"; -export { L1MetricsService } from "./internal.js"; +export { L1MetricsService, L2MetricsService } from "./internal.js"; diff --git a/packages/metrics/src/internal.ts b/packages/metrics/src/internal.ts index a802462..8d60e4b 100644 --- a/packages/metrics/src/internal.ts +++ b/packages/metrics/src/internal.ts @@ -2,3 +2,4 @@ export * from "./types/index.js"; export * from "./exceptions/index.js"; export * from "./l1/abis/index.js"; export * from "./l1/index.js"; +export * from "./l2/index.js"; diff --git a/packages/metrics/src/l2/index.ts b/packages/metrics/src/l2/index.ts index e69de29..2217291 100644 --- a/packages/metrics/src/l2/index.ts +++ b/packages/metrics/src/l2/index.ts @@ -0,0 +1 @@ +export * from "./l2Metrics.service.js"; diff --git a/packages/metrics/src/l2/l2Metrics.service.ts b/packages/metrics/src/l2/l2Metrics.service.ts new file mode 100644 index 0000000..24dd87b --- /dev/null +++ b/packages/metrics/src/l2/l2Metrics.service.ts @@ -0,0 +1,50 @@ +import { ZKChainProvider } from "@zkchainhub/chain-providers"; +import { ILogger } from "@zkchainhub/shared"; + +/** + * Acts as a wrapper around Viem library to provide methods to interact with zkSync chains. + */ +export class L2MetricsService { + constructor( + private readonly provider: ZKChainProvider, + private readonly logger: ILogger, + ) {} + + /** + * Retrieves the transactions per second (TPS) from the provider. + * + * @returns A promise that resolves to the number of transactions per second. + */ + async tps(): Promise { + return this.provider.tps(); + } + + /** + * Retrieves the average block time from the provider. + * + * @returns A promise that resolves to the average block time as a number. + */ + async avgBlockTime(): Promise { + return this.provider.avgBlockTime(); + } + + /** + * Retrieves the number of the last block in the chain. + * + * @returns A promise that resolves to a bigint representing the number of the last block. + */ + async lastBlock(): Promise { + return this.provider.getBlockNumber(); + } + + /** + * Retrieves the last verified block based on the given lastVerifiedBatch. + * + * @param lastVerifiedBatch The number representing the last verified batch. + * @returns A Promise that resolves to the number of the last verified block, or undefined if an error occurs. + */ + async getLastVerifiedBlock(lastVerifiedBatch: number): Promise { + const [, endBlock] = await this.provider.getL1BatchBlockRange(lastVerifiedBatch); + return endBlock; + } +} diff --git a/packages/metrics/src/l2/l2MetricsService.ts b/packages/metrics/src/l2/l2MetricsService.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/metrics/test/unit/l2/l2Metrics.service.spec.ts b/packages/metrics/test/unit/l2/l2Metrics.service.spec.ts new file mode 100644 index 0000000..07a83eb --- /dev/null +++ b/packages/metrics/test/unit/l2/l2Metrics.service.spec.ts @@ -0,0 +1,81 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { ZKChainProvider } from "@zkchainhub/chain-providers"; +import { ILogger } from "@zkchainhub/shared"; + +import { L2MetricsService } from "../../../src/l2/l2Metrics.service"; + +describe("L2MetricsService", () => { + let service: L2MetricsService; + let provider: ZKChainProvider; + let logger: ILogger; + + beforeEach(() => { + provider = { + tps: vi.fn(), + avgBlockTime: vi.fn(), + getBlockNumber: vi.fn(), + getL1BatchBlockRange: vi.fn(), + } as unknown as ZKChainProvider; + logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + } as unknown as ILogger; + service = new L2MetricsService(provider, logger); + }); + + it("should create an instance of L2MetricsService", () => { + expect(service).toBeDefined(); + }); + + describe("tps", () => { + it("should return the TPS value", async () => { + const expectedTps = 100; + vi.spyOn(provider, "tps").mockResolvedValue(expectedTps); + + const result = await service.tps(); + + expect(result).toBe(expectedTps); + expect(provider.tps).toHaveBeenCalled(); + }); + }); + + describe("avgBlockTime", () => { + it("return the average block time", async () => { + const expectedAvgBlockTime = 10; + vi.spyOn(provider, "avgBlockTime").mockResolvedValue(expectedAvgBlockTime); + + const result = await service.avgBlockTime(); + + expect(result).toBe(expectedAvgBlockTime); + expect(provider.avgBlockTime).toHaveBeenCalled(); + }); + }); + + describe("lastBlock", () => { + it("return the last block number", async () => { + const expectedLastBlock = 1000n; + vi.spyOn(provider, "getBlockNumber").mockResolvedValue(expectedLastBlock); + + const result = await service.lastBlock(); + + expect(result).toBe(expectedLastBlock); + expect(provider.getBlockNumber).toHaveBeenCalled(); + }); + }); + + describe("getLastVerifiedBlock", () => { + it("return the end block of the last verified batch", async () => { + const lastVerifiedBatch = 5; + const expectedEndBlock = 100; + vi.spyOn(provider, "getL1BatchBlockRange").mockResolvedValue([0, expectedEndBlock]); + + const result = await service.getLastVerifiedBlock(lastVerifiedBatch); + + expect(result).toBe(expectedEndBlock); + expect(provider.getL1BatchBlockRange).toHaveBeenCalledWith(lastVerifiedBatch); + }); + }); +}); diff --git a/packages/metrics/vitest.config.ts b/packages/metrics/vitest.config.ts index 3aa3262..7f9df03 100644 --- a/packages/metrics/vitest.config.ts +++ b/packages/metrics/vitest.config.ts @@ -1,5 +1,5 @@ import path from "path"; -import { defineConfig } from "vitest/config"; +import { configDefaults, defineConfig } from "vitest/config"; export default defineConfig({ test: { @@ -10,7 +10,13 @@ export default defineConfig({ coverage: { provider: "v8", reporter: ["text", "json", "html"], // Coverage reporters - exclude: ["node_modules", "dist"], // Files to exclude from coverage + exclude: [ + "node_modules", + "dist", + "src/index.ts", + "**/external.ts", + ...configDefaults.exclude, + ], // Files to exclude from coverage }, }, resolve: {