Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: l2 metrics service #63

Merged
merged 3 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
});
});
});
Comment on lines +128 to +143
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any non-happy paths worth testing?

});
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";
62 changes: 62 additions & 0 deletions packages/metrics/src/l2/l2Metrics.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { isNativeError } from "util/types";
import { BaseError } from "viem";

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> {
try {
const [, endBlock] = await this.provider.getL1BatchBlockRange(lastVerifiedBatch);
return endBlock;
} catch (error) {
if (error instanceof BaseError) {
this.logger.error(error.message);
} else if (isNativeError(error)) {
this.logger.error(error);
}
return undefined;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the way we are handling errors, for the rest of the methods. Should we throw here instead of returning undefined ? The lastVerifiedBlock is not an optional method on the front end

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this got me thinking tbh, should we actually return return value | undefined for all methods? like an error on L2 shouldn't break anything as it's like not having rpc urls wdyt?

}
}
}
Empty file.
92 changes: 92 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,92 @@
import { BaseError } from "viem";
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);
});
it("return undefined if an error occurs", async () => {
const lastVerifiedBatch = 500;
vi.spyOn(provider, "getL1BatchBlockRange").mockRejectedValue(
new BaseError("Invalid batch number"),
);

const result = await service.getLastVerifiedBlock(lastVerifiedBatch);

expect(result).toBeUndefined();
});
});
});
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