Skip to content

Commit

Permalink
feat: batches info l1 metric (#41)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes ZKS-74 ZKS-148

## Description

- Add `getBatchesInfo` logic on `L1MetricsService`
  • Loading branch information
0xkenj1 authored Aug 8, 2024
1 parent fd88b1f commit b154e12
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 54 deletions.
2 changes: 2 additions & 0 deletions libs/metrics/src/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./invalidChainId.exception";
export * from "./l1MetricsService.exception";
6 changes: 6 additions & 0 deletions libs/metrics/src/exceptions/invalidChainId.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class InvalidChainId extends Error {
constructor(message: string) {
super(message);
this.name = "InvalidChainId";
}
}
6 changes: 6 additions & 0 deletions libs/metrics/src/exceptions/l1MetricsService.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class L1MetricsServiceException extends Error {
constructor(message: string) {
super(message);
this.name = "L1MetricsServiceException";
}
}
13 changes: 0 additions & 13 deletions libs/metrics/src/exceptions/provider.exception.ts

This file was deleted.

83 changes: 61 additions & 22 deletions libs/metrics/src/l1/l1MetricsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import {
zeroAddress,
} from "viem";

import { L1ProviderException } from "@zkchainhub/metrics/exceptions/provider.exception";
import { bridgeHubAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis";
import { InvalidChainId, L1MetricsServiceException } from "@zkchainhub/metrics/exceptions";
import { bridgeHubAbi, diamondProxyAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis";
import { AssetTvl, GasInfo } from "@zkchainhub/metrics/types";
import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing";
import { EvmProviderService } from "@zkchainhub/providers";
import { AbiWithAddress, ChainId, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { ETH_TOKEN_ADDRESS } from "@zkchainhub/shared/constants/addresses";
import { BatchesInfo, ChainId, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { ETH_TOKEN_ADDRESS } from "@zkchainhub/shared/constants";
import {
erc20Tokens,
isNativeToken,
Expand All @@ -34,15 +34,15 @@ const ONE_ETHER = parseEther("1");
*/
@Injectable()
export class L1MetricsService {
private readonly bridgeHub: Readonly<AbiWithAddress> = {
private readonly bridgeHub = {
abi: bridgeHubAbi,
address: L1_CONTRACTS.BRIDGE_HUB,
};
private readonly sharedBridge: Readonly<AbiWithAddress> = {
private readonly sharedBridge = {
abi: sharedBridgeAbi,
address: L1_CONTRACTS.SHARED_BRIDGE,
};
private readonly diamondContracts: Map<ChainId, AbiWithAddress> = new Map();
private readonly diamondContracts: Map<ChainId, Address> = new Map();

constructor(
private readonly evmProviderService: EvmProviderService,
Expand Down Expand Up @@ -143,18 +143,58 @@ export class L1MetricsService {
return { ethBalance: ethBalance, addressesBalance: balances };
}

//TODO: Implement getBatchesInfo.
async getBatchesInfo(
_chainId: number,
): Promise<{ commited: number; verified: number; proved: number }> {
return { commited: 100, verified: 100, proved: 100 };
/**
* Retrieves the information about the batches from L2 chain
* @param chainId - The chain id for which to get the batches info
* @returns commits, verified and executed batches
*/
async getBatchesInfo(chainId: ChainId): Promise<BatchesInfo> {
let diamondProxyAddress: Address | undefined = this.diamondContracts.get(chainId);

if (!diamondProxyAddress) {
diamondProxyAddress = await this.evmProviderService.readContract(
this.bridgeHub.address,
this.bridgeHub.abi,
"getHyperchain",
[chainId],
);
if (diamondProxyAddress == zeroAddress) {
throw new InvalidChainId(`Chain ID ${chainId} doesn't exist on the ecosystem`);
}
this.diamondContracts.set(chainId, diamondProxyAddress);
}

const [commited, verified, executed] = await this.evmProviderService.multicall({
contracts: [
{
address: diamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesCommitted",
args: [],
} as const,
{
address: diamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesVerified",
args: [],
} as const,
{
address: diamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesExecuted",
args: [],
} as const,
],
allowFailure: false,
});
return { commited, verified, executed };
}

/**
* Retrieves the Total Value Locked for {chainId} by L1 token
* @returns A Promise that resolves to an array of AssetTvl objects representing the TVL for each asset.
*/
async tvl(chainId: number): Promise<AssetTvl[]> {
async tvl(chainId: ChainId): Promise<AssetTvl[]> {
const erc20Addresses = erc20Tokens.map((token) => token.contractAddress);

const balances = await this.fetchTokenBalancesByChain(chainId, erc20Addresses);
Expand All @@ -173,23 +213,22 @@ export class L1MetricsService {
* @param addresses - An array of addresses for which to fetch the token balances.
* @returns A promise that resolves to an object containing the ETH balance and an array of address balances.
*/
private async fetchTokenBalancesByChain(chainId: number, addresses: Address[]) {
const chainIdBn = BigInt(chainId);
private async fetchTokenBalancesByChain(chainId: ChainId, addresses: Address[]) {
const balances = await this.evmProviderService.multicall({
contracts: [
...addresses.map((tokenAddress) => {
return {
address: this.sharedBridge.address,
abi: sharedBridgeAbi,
abi: this.sharedBridge.abi,
functionName: "chainBalance",
args: [chainIdBn, tokenAddress],
args: [chainId, tokenAddress],
} as const;
}),
{
address: this.sharedBridge.address,
abi: sharedBridgeAbi,
abi: this.sharedBridge.abi,
functionName: "chainBalance",
args: [chainIdBn, ETH_TOKEN_ADDRESS],
args: [chainId, ETH_TOKEN_ADDRESS],
} as const,
],
allowFailure: false,
Expand All @@ -199,7 +238,7 @@ export class L1MetricsService {
}

//TODO: Implement chainType.
async chainType(_chainId: number): Promise<"validium" | "rollup"> {
async chainType(_chainId: ChainId): Promise<"validium" | "rollup"> {
return "rollup";
}

Expand Down Expand Up @@ -251,12 +290,12 @@ export class L1MetricsService {
if (isNativeError(e)) {
this.logger.error(`Failed to get gas information: ${e.message}`);
}
throw new L1ProviderException("Failed to get gas information from L1.");
throw new L1MetricsServiceException("Failed to get gas information from L1.");
}
}

//TODO: Implement feeParams.
async feeParams(_chainId: number): Promise<{
async feeParams(_chainId: ChainId): Promise<{
batchOverheadL1Gas: number;
maxPubdataPerBatch: number;
maxL2GasPerBatch: number;
Expand Down
135 changes: 119 additions & 16 deletions libs/metrics/test/unit/l1/l1MetricsService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { Test, TestingModule } from "@nestjs/testing";
import { WINSTON_MODULE_PROVIDER } from "nest-winston";
import { encodeFunctionData, erc20Abi, parseEther, zeroAddress } from "viem";

import { L1ProviderException } from "@zkchainhub/metrics/exceptions/provider.exception";
import { InvalidChainId, L1MetricsServiceException } from "@zkchainhub/metrics/exceptions";
import { L1MetricsService } from "@zkchainhub/metrics/l1/";
import { bridgeHubAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis";
import { bridgeHubAbi, diamondProxyAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis";
import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing";
import { EvmProviderService } from "@zkchainhub/providers";
import { ETH_TOKEN_ADDRESS, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { BatchesInfo, ETH_TOKEN_ADDRESS, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared";
import { nativeToken, WETH } from "@zkchainhub/shared/tokens/tokens";

// Mock implementations of the dependencies
Expand Down Expand Up @@ -252,17 +252,120 @@ describe("L1MetricsService", () => {
});

describe("getBatchesInfo", () => {
it("return getBatchesInfo", async () => {
const result = await l1MetricsService.getBatchesInfo(1);
expect(result).toEqual({ commited: 100, verified: 100, proved: 100 });
it("returns batches info for chain id", async () => {
const chainId = 324n; // this is ZKsyncEra chain id
const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890";

l1MetricsService["diamondContracts"].set(chainId, mockedDiamondProxyAddress);
const mockBatchesInfo: BatchesInfo = { commited: 300n, verified: 200n, executed: 100n };
const batchesInfoMulticallResponse = [
mockBatchesInfo.commited,
mockBatchesInfo.verified,
mockBatchesInfo.executed,
];

jest.spyOn(mockEvmProviderService, "multicall").mockResolvedValue(
batchesInfoMulticallResponse,
);

const result = await l1MetricsService.getBatchesInfo(chainId);

expect(result).toEqual(mockBatchesInfo);
expect(mockEvmProviderService.multicall).toHaveBeenCalledWith({
contracts: [
{
address: mockedDiamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesCommitted",
args: [],
},
{
address: mockedDiamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesVerified",
args: [],
},
{
address: mockedDiamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesExecuted",
args: [],
},
],
allowFailure: false,
});
});

it("throws if chainId doesn't exist on the ecosystem", async () => {
const chainId = 324n; // this is ZKsyncEra chain id
l1MetricsService["diamondContracts"].clear();
jest.spyOn(mockEvmProviderService, "readContract").mockResolvedValue(zeroAddress);
await expect(l1MetricsService.getBatchesInfo(chainId)).rejects.toThrow(InvalidChainId);
});

it("fetches and sets diamond proxy if chainId doesn't exists on map", async () => {
const chainId = 324n; // this is ZKsyncEra chain id
const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890";

l1MetricsService["diamondContracts"].clear();

const mockBatchesInfo: BatchesInfo = { commited: 300n, verified: 200n, executed: 100n };
const batchesInfoMulticallResponse = [
mockBatchesInfo.commited,
mockBatchesInfo.verified,
mockBatchesInfo.executed,
];

jest.spyOn(mockEvmProviderService, "readContract").mockResolvedValue(
mockedDiamondProxyAddress,
);
jest.spyOn(mockEvmProviderService, "multicall").mockResolvedValue(
batchesInfoMulticallResponse,
);
const result = await l1MetricsService.getBatchesInfo(chainId);

expect(result).toEqual(mockBatchesInfo);

expect(l1MetricsService["diamondContracts"].get(chainId)).toEqual(
mockedDiamondProxyAddress,
);
expect(mockEvmProviderService.readContract).toHaveBeenCalledWith(
l1MetricsService["bridgeHub"].address,
l1MetricsService["bridgeHub"].abi,
"getHyperchain",
[BigInt(chainId)],
);
expect(mockEvmProviderService.multicall).toHaveBeenCalledWith({
contracts: [
{
address: mockedDiamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesCommitted",
args: [],
},
{
address: mockedDiamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesVerified",
args: [],
},
{
address: mockedDiamondProxyAddress,
abi: diamondProxyAbi,
functionName: "getTotalBatchesExecuted",
args: [],
},
],
allowFailure: false,
});
});
});

describe("tvl", () => {
it("return the TVL for chain id", async () => {
const mockBalances = [60_841_657_140641n, 135_63005559n, 123_803_824374847279970609n]; // Mocked balances
const mockPrices = { "wrapped-bitcoin": 66_129, "usd-coin": 0.999, ethereum: 3_181.09 }; // Mocked prices
const chainId = 324; // this is ZKsyncEra chain id
const chainId = 324n; // this is ZKsyncEra chain id

jest.spyOn(mockEvmProviderService, "multicall").mockResolvedValue(mockBalances);
jest.spyOn(mockPricingService, "getTokenPrices").mockResolvedValue(mockPrices);
Expand Down Expand Up @@ -339,7 +442,7 @@ describe("L1MetricsService", () => {
});

it("throws an error if the prices length is invalid", async () => {
const chainId = 324;
const chainId = 324n;
jest.spyOn(mockEvmProviderService, "multicall").mockResolvedValue([
60_841_657_140641n,
135_63005559n,
Expand All @@ -358,7 +461,7 @@ describe("L1MetricsService", () => {

describe("chainType", () => {
it("return chainType", async () => {
const result = await l1MetricsService.chainType(1);
const result = await l1MetricsService.chainType(1n);
expect(result).toBe("rollup");
});
});
Expand Down Expand Up @@ -450,7 +553,7 @@ describe("L1MetricsService", () => {
expect(mockGetTokenPrices).toHaveBeenCalledWith([nativeToken.coingeckoId]);
});

it("throws L1ProviderException when estimateGas fails", async () => {
it("throws L1MetricsServiceException when estimateGas fails", async () => {
// Mock the necessary dependencies
const mockEstimateGas = jest.spyOn(mockEvmProviderService, "estimateGas");
mockEstimateGas.mockRejectedValueOnce(new Error("Failed to estimate gas"));
Expand All @@ -461,8 +564,8 @@ describe("L1MetricsService", () => {
const mockGetTokenPrices = jest.spyOn(mockPricingService, "getTokenPrices");
mockGetTokenPrices.mockResolvedValueOnce({ [nativeToken.coingeckoId]: 2000 }); // ethPriceInUsd

// Call the method and expect it to throw L1ProviderException
await expect(l1MetricsService.ethGasInfo()).rejects.toThrow(L1ProviderException);
// Call the method and expect it to throw L1MetricsServiceException
await expect(l1MetricsService.ethGasInfo()).rejects.toThrow(L1MetricsServiceException);

// Assertions
expect(mockEstimateGas).toHaveBeenCalledWith({
Expand All @@ -474,7 +577,7 @@ describe("L1MetricsService", () => {
expect(mockGetTokenPrices).not.toHaveBeenCalled();
});

it("throws L1ProviderException when getGasPrice fails", async () => {
it("throws L1MetricsServiceException when getGasPrice fails", async () => {
// Mock the necessary dependencies
const mockEstimateGas = jest.spyOn(mockEvmProviderService, "estimateGas");
mockEstimateGas.mockResolvedValueOnce(BigInt(21000)); // ethTransferGasCost
Expand All @@ -486,8 +589,8 @@ describe("L1MetricsService", () => {
const mockGetTokenPrices = jest.spyOn(mockPricingService, "getTokenPrices");
mockGetTokenPrices.mockResolvedValueOnce({ [nativeToken.coingeckoId]: 2000 }); // ethPriceInUsd

// Call the method and expect it to throw L1ProviderException
await expect(l1MetricsService.ethGasInfo()).rejects.toThrow(L1ProviderException);
// Call the method and expect it to throw L1MetricsServiceException
await expect(l1MetricsService.ethGasInfo()).rejects.toThrow(L1MetricsServiceException);

// Assertions
expect(mockEstimateGas).toHaveBeenCalledTimes(2);
Expand All @@ -514,7 +617,7 @@ describe("L1MetricsService", () => {

describe("feeParams", () => {
it("return feeParams", async () => {
const result = await l1MetricsService.feeParams(1);
const result = await l1MetricsService.feeParams(1n);
expect(result).toEqual({
batchOverheadL1Gas: 50000,
maxPubdataPerBatch: 120000,
Expand Down
1 change: 1 addition & 0 deletions libs/shared/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./rollup.type";
export * from "./utils.type";
export * from "./l1.type";
Loading

0 comments on commit b154e12

Please sign in to comment.