Skip to content

Commit

Permalink
feat: l2 metrics service (#63)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes ZKS-228 

## Description

- `L2MetricsService` implementation on `metrics` package
- add zk provider method for fetching blocks range for a given L1 Batch
  • Loading branch information
0xnigir1 authored Sep 3, 2024
1 parent adf8217 commit d5b2901
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 18 deletions.
8 changes: 4 additions & 4 deletions packages/chain-providers/src/providers/evmProvider.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ import {
*/
export class EvmProvider {
private client: ReturnType<
typeof createPublicClient<FallbackTransport<HttpTransport[]>, Chain>
typeof createPublicClient<FallbackTransport<HttpTransport[]>, Chain | undefined>
>;

constructor(
rpcUrls: string[],
readonly chain: Chain,
readonly chain: Chain | undefined,
private readonly logger: ILogger,
) {
if (rpcUrls.length === 0) {
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -201,7 +201,7 @@ export class EvmProvider {
>(
args: MulticallParameters<contracts, allowFailure>,
): Promise<MulticallReturnType<contracts, allowFailure>> {
if (!this.chain.contracts?.multicall3?.address) throw new MulticallNotFound();
if (!this.chain?.contracts?.multicall3?.address) throw new MulticallNotFound();

return this.client.multicall<contracts, allowFailure>(args);
}
Expand Down
23 changes: 20 additions & 3 deletions packages/chain-providers/src/providers/zkChainProvider.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -20,13 +25,13 @@ import { EvmProvider } from "./evmProvider.service.js";
export class ZKChainProvider extends EvmProvider {
private zkClient: Client<
FallbackTransport<HttpTransport[]>,
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,
Expand All @@ -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<GetL1BatchBlockRangeReturnParameters> {
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) };
Expand Down Expand Up @@ -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"),
);
Expand All @@ -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 };
Expand All @@ -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 };
Expand All @@ -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,
});
});
});
});
10 changes: 8 additions & 2 deletions packages/chain-providers/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "path";
import { defineConfig } from "vitest/config";
import { configDefaults, defineConfig } from "vitest/config";

export default defineConfig({
test: {
Expand All @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion packages/metrics/src/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
1 change: 1 addition & 0 deletions packages/metrics/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
1 change: 1 addition & 0 deletions packages/metrics/src/l2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./l2Metrics.service.js";
50 changes: 50 additions & 0 deletions packages/metrics/src/l2/l2Metrics.service.ts
Original file line number Diff line number Diff line change
@@ -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<number> {
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<number> {
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<bigint> {
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<number | undefined> {
const [, endBlock] = await this.provider.getL1BatchBlockRange(lastVerifiedBatch);
return endBlock;
}
}
Empty file.
81 changes: 81 additions & 0 deletions packages/metrics/test/unit/l2/l2Metrics.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
10 changes: 8 additions & 2 deletions packages/metrics/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "path";
import { defineConfig } from "vitest/config";
import { configDefaults, defineConfig } from "vitest/config";

export default defineConfig({
test: {
Expand All @@ -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: {
Expand Down

0 comments on commit d5b2901

Please sign in to comment.