From 276767711918b97eba2d0f7f4703f50678e8353e Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Wed, 7 Aug 2024 21:15:07 -0300 Subject: [PATCH] feat: batches info l1 metric --- libs/metrics/src/exceptions/index.ts | 2 + .../exceptions/invalidChainId.exception.ts | 6 + .../exceptions/metricsService.exception.ts | 13 ++ .../src/exceptions/provider.exception.ts | 13 -- libs/metrics/src/l1/abis/index.ts | 1 + libs/metrics/src/l1/l1MetricsService.ts | 77 +++++++--- .../test/unit/l1/l1MetricsService.spec.ts | 132 ++++++++++++++++-- libs/shared/src/types/index.ts | 1 + libs/shared/src/types/l1.type.ts | 17 +++ libs/shared/src/types/utils.type.ts | 5 +- 10 files changed, 223 insertions(+), 44 deletions(-) create mode 100644 libs/metrics/src/exceptions/index.ts create mode 100644 libs/metrics/src/exceptions/invalidChainId.exception.ts create mode 100644 libs/metrics/src/exceptions/metricsService.exception.ts delete mode 100644 libs/metrics/src/exceptions/provider.exception.ts create mode 100644 libs/shared/src/types/l1.type.ts diff --git a/libs/metrics/src/exceptions/index.ts b/libs/metrics/src/exceptions/index.ts new file mode 100644 index 0000000..fb3a9a8 --- /dev/null +++ b/libs/metrics/src/exceptions/index.ts @@ -0,0 +1,2 @@ +export * from "./invalidChainId.exception"; +export * from "./metricsService.exception"; diff --git a/libs/metrics/src/exceptions/invalidChainId.exception.ts b/libs/metrics/src/exceptions/invalidChainId.exception.ts new file mode 100644 index 0000000..6eb4827 --- /dev/null +++ b/libs/metrics/src/exceptions/invalidChainId.exception.ts @@ -0,0 +1,6 @@ +export class InvalidChainId extends Error { + constructor(message: string) { + super(message); + this.name = "InvalidChainId"; + } +} diff --git a/libs/metrics/src/exceptions/metricsService.exception.ts b/libs/metrics/src/exceptions/metricsService.exception.ts new file mode 100644 index 0000000..56c418d --- /dev/null +++ b/libs/metrics/src/exceptions/metricsService.exception.ts @@ -0,0 +1,13 @@ +export class MetricsServiceException extends Error { + constructor(message: string) { + super(message); + this.name = "MetricsServiceException"; + } +} + +export class L1MetricsServiceException extends MetricsServiceException { + constructor(message: string) { + super(message); + this.name = "L1MetricsServiceException"; + } +} diff --git a/libs/metrics/src/exceptions/provider.exception.ts b/libs/metrics/src/exceptions/provider.exception.ts deleted file mode 100644 index d596bac..0000000 --- a/libs/metrics/src/exceptions/provider.exception.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class ProviderException extends Error { - constructor(message: string) { - super(message); - this.name = "ProviderException"; - } -} - -export class L1ProviderException extends ProviderException { - constructor(message: string) { - super(message); - this.name = "L1ProviderException"; - } -} diff --git a/libs/metrics/src/l1/abis/index.ts b/libs/metrics/src/l1/abis/index.ts index a8c48ff..df61add 100644 --- a/libs/metrics/src/l1/abis/index.ts +++ b/libs/metrics/src/l1/abis/index.ts @@ -1,3 +1,4 @@ export * from "./bridgeHub.abi"; export * from "./diamondProxy.abi"; export * from "./sharedBridge.abi"; +export * from "./tokenBalances.abi"; diff --git a/libs/metrics/src/l1/l1MetricsService.ts b/libs/metrics/src/l1/l1MetricsService.ts index cc051d8..dc69d6c 100644 --- a/libs/metrics/src/l1/l1MetricsService.ts +++ b/libs/metrics/src/l1/l1MetricsService.ts @@ -14,15 +14,19 @@ import { zeroAddress, } from "viem"; -import { L1ProviderException } from "@zkchainhub/metrics/exceptions/provider.exception"; -import { bridgeHubAbi, sharedBridgeAbi } from "@zkchainhub/metrics/l1/abis"; -import { tokenBalancesAbi } from "@zkchainhub/metrics/l1/abis/tokenBalances.abi"; +import { InvalidChainId, L1MetricsServiceException } from "@zkchainhub/metrics/exceptions"; +import { + bridgeHubAbi, + diamondProxyAbi, + sharedBridgeAbi, + tokenBalancesAbi, +} from "@zkchainhub/metrics/l1/abis"; import { tokenBalancesBytecode } from "@zkchainhub/metrics/l1/bytecode"; 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, @@ -38,15 +42,15 @@ const ONE_ETHER = parseEther("1"); */ @Injectable() export class L1MetricsService { - private readonly bridgeHub: Readonly = { + private readonly bridgeHub = { abi: bridgeHubAbi, address: L1_CONTRACTS.BRIDGE_HUB, }; - private readonly sharedBridge: Readonly = { + private readonly sharedBridge = { abi: sharedBridgeAbi, address: L1_CONTRACTS.SHARED_BRIDGE, }; - private readonly diamondContracts: Map = new Map(); + private readonly diamondContracts: Map = new Map(); constructor( private readonly evmProviderService: EvmProviderService, @@ -146,11 +150,52 @@ export class L1MetricsService { return { ethBalance: balances[addresses.length]!, addressesBalance: balances.slice(0, -1) }; } - //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: number): Promise { + if (!Number.isInteger(chainId)) { + throw new InvalidChainId("chain id must be an integer"); + } + const chainIdBn = BigInt(chainId); + let diamondProxyAddress: Address | undefined = this.diamondContracts.get(chainId); + + if (!diamondProxyAddress) { + diamondProxyAddress = await this.evmProviderService.readContract( + this.bridgeHub.address, + this.bridgeHub.abi, + "getHyperchain", + [chainIdBn], + ); + 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 }; } /** @@ -183,14 +228,14 @@ export class L1MetricsService { ...addresses.map((tokenAddress) => { return { address: this.sharedBridge.address, - abi: sharedBridgeAbi, + abi: this.sharedBridge.abi, functionName: "chainBalance", args: [chainIdBn, tokenAddress], } as const; }), { address: this.sharedBridge.address, - abi: sharedBridgeAbi, + abi: this.sharedBridge.abi, functionName: "chainBalance", args: [chainIdBn, ETH_TOKEN_ADDRESS], } as const, @@ -254,7 +299,7 @@ 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."); } } diff --git a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts index 18def4b..85cca20 100644 --- a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts +++ b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts @@ -4,14 +4,18 @@ 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 { tokenBalancesAbi } from "@zkchainhub/metrics/l1/abis/tokenBalances.abi"; +import { + bridgeHubAbi, + diamondProxyAbi, + sharedBridgeAbi, + tokenBalancesAbi, +} from "@zkchainhub/metrics/l1/abis"; import { tokenBalancesBytecode } from "@zkchainhub/metrics/l1/bytecode"; 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 @@ -242,9 +246,111 @@ 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 = 324; // 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 invalid chainId ", async () => { + const chainId = 324.123123; // this is ZKsyncEra chain id + + await expect(l1MetricsService.getBatchesInfo(chainId)).rejects.toThrowError( + InvalidChainId, + ); + }); + it("fetches and sets diamond proxy if chainId doesn't exists on map", async () => { + const chainId = 324; // 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, + }); }); }); @@ -440,7 +546,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")); @@ -451,8 +557,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({ @@ -464,7 +570,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 @@ -476,8 +582,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); diff --git a/libs/shared/src/types/index.ts b/libs/shared/src/types/index.ts index 1540e93..4a63843 100644 --- a/libs/shared/src/types/index.ts +++ b/libs/shared/src/types/index.ts @@ -1,2 +1,3 @@ export * from "./rollup.type"; export * from "./utils.type"; +export * from "./l1.type"; diff --git a/libs/shared/src/types/l1.type.ts b/libs/shared/src/types/l1.type.ts new file mode 100644 index 0000000..a195237 --- /dev/null +++ b/libs/shared/src/types/l1.type.ts @@ -0,0 +1,17 @@ +/** + * Represents the information about the batches from L2 chain + */ +export interface BatchesInfo { + /** + * The total number of batches that were committed + */ + commited: bigint; + /** + * The total number of batches that were committed & verified + */ + verified: bigint; + /** + * The total number of batches that were committed & verified & executed + */ + executed: bigint; +} diff --git a/libs/shared/src/types/utils.type.ts b/libs/shared/src/types/utils.type.ts index 8489ff4..31744f7 100644 --- a/libs/shared/src/types/utils.type.ts +++ b/libs/shared/src/types/utils.type.ts @@ -1,5 +1,6 @@ -import { Abi, Address } from "abitype"; +import { Address } from "abitype"; +import { AbiItem } from "viem"; -export type AbiWithAddress = { abi: Abi; address: Address }; +export type AbiWithAddress = { abi: T; address: Address }; export type ChainId = number;