From 276767711918b97eba2d0f7f4703f50678e8353e Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Wed, 7 Aug 2024 21:15:07 -0300 Subject: [PATCH 1/3] 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; From e714d45254aaf8c4b8c6b428e5e9430536dfeae8 Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Wed, 7 Aug 2024 21:54:55 -0300 Subject: [PATCH 2/3] feat: l1 chainType metric --- libs/metrics/src/exceptions/index.ts | 1 + .../exceptions/invalidChainType.exception.ts | 11 ++ libs/metrics/src/l1/l1MetricsService.ts | 72 +++++++++---- .../test/unit/l1/l1MetricsService.spec.ts | 101 +++++++++++++++++- libs/shared/src/types/rollup.type.ts | 1 + 5 files changed, 160 insertions(+), 26 deletions(-) create mode 100644 libs/metrics/src/exceptions/invalidChainType.exception.ts diff --git a/libs/metrics/src/exceptions/index.ts b/libs/metrics/src/exceptions/index.ts index fb3a9a8..1da895c 100644 --- a/libs/metrics/src/exceptions/index.ts +++ b/libs/metrics/src/exceptions/index.ts @@ -1,2 +1,3 @@ export * from "./invalidChainId.exception"; export * from "./metricsService.exception"; +export * from "./invalidChainType.exception"; diff --git a/libs/metrics/src/exceptions/invalidChainType.exception.ts b/libs/metrics/src/exceptions/invalidChainType.exception.ts new file mode 100644 index 0000000..ae9c0bd --- /dev/null +++ b/libs/metrics/src/exceptions/invalidChainType.exception.ts @@ -0,0 +1,11 @@ +import { Chains } from "@zkchainhub/shared"; + +export class InvalidChainType extends Error { + constructor(index: number) { + super(`Current supported types are [${Chains.join( + ", ", + )}], but received index ${index}. Verify if supported chains are consistent with https://github.com/matter-labs/era-contracts/blob/8a70bbbc48125f5bde6189b4e3c6a3ee79631678/l1-contracts/contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol#L39 +`); + this.name = "InvalidChainType"; + } +} diff --git a/libs/metrics/src/l1/l1MetricsService.ts b/libs/metrics/src/l1/l1MetricsService.ts index dc69d6c..665bb31 100644 --- a/libs/metrics/src/l1/l1MetricsService.ts +++ b/libs/metrics/src/l1/l1MetricsService.ts @@ -14,7 +14,11 @@ import { zeroAddress, } from "viem"; -import { InvalidChainId, L1MetricsServiceException } from "@zkchainhub/metrics/exceptions"; +import { + InvalidChainId, + InvalidChainType, + L1MetricsServiceException, +} from "@zkchainhub/metrics/exceptions"; import { bridgeHubAbi, diamondProxyAbi, @@ -25,7 +29,14 @@ 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 { BatchesInfo, ChainId, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared"; +import { + BatchesInfo, + ChainId, + Chains, + ChainType, + L1_CONTRACTS, + vitalikAddress, +} from "@zkchainhub/shared"; import { ETH_TOKEN_ADDRESS } from "@zkchainhub/shared/constants"; import { erc20Tokens, @@ -156,22 +167,7 @@ export class L1MetricsService { * @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 diamondProxyAddress = await this.fetchDiamondProxyAddress(chainId); const [commited, verified, executed] = await this.evmProviderService.multicall({ contracts: [ { @@ -246,9 +242,43 @@ export class L1MetricsService { return { ethBalance: balances[addresses.length]!, addressesBalance: balances.slice(0, -1) }; } - //TODO: Implement chainType. - async chainType(_chainId: number): Promise<"validium" | "rollup"> { - return "rollup"; + async chainType(chainId: number): Promise { + const diamondProxyAddress = await this.fetchDiamondProxyAddress(chainId); + const chainTypeIndex = await this.evmProviderService.readContract( + diamondProxyAddress, + diamondProxyAbi, + "getPubdataPricingMode", + [], + ); + const chainType = Chains[chainTypeIndex]; + if (!chainType) { + throw new InvalidChainType(chainTypeIndex); + } + return chainType; + } + + /** + * Fetches the diamond proxy address for the given chain id and caches it for future use. + * @param chainId - The chain id for which to fetch the diamond proxy address. + * @returns Diamond proxy address. + */ + async fetchDiamondProxyAddress(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); + } + return diamondProxyAddress; } /** diff --git a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts index 85cca20..43b737e 100644 --- a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts +++ b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts @@ -4,7 +4,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { WINSTON_MODULE_PROVIDER } from "nest-winston"; import { encodeFunctionData, erc20Abi, parseEther, zeroAddress } from "viem"; -import { InvalidChainId, L1MetricsServiceException } from "@zkchainhub/metrics/exceptions"; +import { + InvalidChainId, + InvalidChainType, + L1MetricsServiceException, +} from "@zkchainhub/metrics/exceptions"; import { L1MetricsService } from "@zkchainhub/metrics/l1/"; import { bridgeHubAbi, @@ -15,7 +19,13 @@ import { import { tokenBalancesBytecode } from "@zkchainhub/metrics/l1/bytecode"; import { IPricingService, PRICING_PROVIDER } from "@zkchainhub/pricing"; import { EvmProviderService } from "@zkchainhub/providers"; -import { BatchesInfo, ETH_TOKEN_ADDRESS, L1_CONTRACTS, vitalikAddress } from "@zkchainhub/shared"; +import { + BatchesInfo, + ChainType, + ETH_TOKEN_ADDRESS, + L1_CONTRACTS, + vitalikAddress, +} from "@zkchainhub/shared"; import { nativeToken, WETH } from "@zkchainhub/shared/tokens/tokens"; // Mock implementations of the dependencies @@ -354,6 +364,54 @@ describe("L1MetricsService", () => { }); }); + describe("fetchDiamondProxyAddress", () => { + it("returns address if already exists in the map", async () => { + const chainId = 324; // this is ZKsyncEra chain id + const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890"; + l1MetricsService["diamondContracts"].clear(); + l1MetricsService["diamondContracts"].set(chainId, mockedDiamondProxyAddress); + + const readContractSpy = jest.spyOn(mockEvmProviderService, "readContract"); + const result = await l1MetricsService["fetchDiamondProxyAddress"](chainId); + + expect(result).toEqual(mockedDiamondProxyAddress); + expect(l1MetricsService["diamondContracts"].get(chainId)).toEqual( + mockedDiamondProxyAddress, + ); + expect(readContractSpy).toHaveBeenCalledTimes(0); + }); + it("throws if invalid chainId ", async () => { + const chainId = 324.123123; // this is ZKsyncEra chain id + + await expect( + l1MetricsService["fetchDiamondProxyAddress"](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(); + + jest.spyOn(mockEvmProviderService, "readContract").mockResolvedValue( + mockedDiamondProxyAddress, + ); + const result = await l1MetricsService["fetchDiamondProxyAddress"](chainId); + + expect(result).toEqual(mockedDiamondProxyAddress); + + expect(l1MetricsService["diamondContracts"].get(chainId)).toEqual( + mockedDiamondProxyAddress, + ); + expect(mockEvmProviderService.readContract).toHaveBeenCalledWith( + l1MetricsService["bridgeHub"].address, + l1MetricsService["bridgeHub"].abi, + "getHyperchain", + [BigInt(chainId)], + ); + }); + }); + describe("tvl", () => { it("return the TVL for chain id", async () => { const mockBalances = [60_841_657_140641n, 135_63005559n, 123_803_824374847279970609n]; // Mocked balances @@ -453,9 +511,42 @@ describe("L1MetricsService", () => { }); describe("chainType", () => { - it("return chainType", async () => { - const result = await l1MetricsService.chainType(1); - expect(result).toBe("rollup"); + it("returns chainType", async () => { + const chainId = 324; // this is ZKsyncEra chain id + const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890"; + + l1MetricsService["diamondContracts"].set(chainId, mockedDiamondProxyAddress); + const mockChainType: ChainType = "Rollup"; + + const readContractSpy = jest + .spyOn(mockEvmProviderService, "readContract") + .mockResolvedValue(0); + + const result = await l1MetricsService.chainType(chainId); + + expect(result).toEqual(mockChainType); + expect(readContractSpy).toHaveBeenCalledWith( + mockedDiamondProxyAddress, + diamondProxyAbi, + "getPubdataPricingMode", + [], + ); + }); + it("throws if blockchain returns an out of bounds index", async () => { + const chainId = 324; // this is ZKsyncEra chain id + const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890"; + + l1MetricsService["diamondContracts"].set(chainId, mockedDiamondProxyAddress); + + jest.spyOn(mockEvmProviderService, "readContract").mockResolvedValue(100); + + await expect(l1MetricsService.chainType(chainId)).rejects.toThrowError( + InvalidChainType, + ); + }); + it("throws if invalid chainId is passed", async () => { + const chainId = 324.123123; + await expect(l1MetricsService.chainType(chainId)).rejects.toThrowError(InvalidChainId); }); }); diff --git a/libs/shared/src/types/rollup.type.ts b/libs/shared/src/types/rollup.type.ts index 37e7549..664390f 100644 --- a/libs/shared/src/types/rollup.type.ts +++ b/libs/shared/src/types/rollup.type.ts @@ -1,3 +1,4 @@ +// Chains should not change since it is a direct mapping of https://github.com/matter-labs/era-contracts/blob/8a70bbbc48125f5bde6189b4e3c6a3ee79631678/l1-contracts/contracts/state-transition/chain-deps/ZkSyncHyperchainStorage.sol#L39 /** * An array of supported chain types. * @readonly From 084145814190a3027baf0ee782d68db947690723 Mon Sep 17 00:00:00 2001 From: 0xkenj1 Date: Thu, 8 Aug 2024 11:37:25 -0300 Subject: [PATCH 3/3] fix: pr comments --- libs/metrics/src/exceptions/index.ts | 2 +- .../exceptions/l1MetricsService.exception.ts | 6 +++++ .../exceptions/metricsService.exception.ts | 13 ---------- libs/metrics/src/l1/l1MetricsService.ts | 24 +++++++++---------- .../test/unit/l1/l1MetricsService.spec.ts | 23 +++++++++--------- libs/shared/src/types/utils.type.ts | 2 +- 6 files changed, 31 insertions(+), 39 deletions(-) create mode 100644 libs/metrics/src/exceptions/l1MetricsService.exception.ts delete mode 100644 libs/metrics/src/exceptions/metricsService.exception.ts diff --git a/libs/metrics/src/exceptions/index.ts b/libs/metrics/src/exceptions/index.ts index fb3a9a8..43d33e4 100644 --- a/libs/metrics/src/exceptions/index.ts +++ b/libs/metrics/src/exceptions/index.ts @@ -1,2 +1,2 @@ export * from "./invalidChainId.exception"; -export * from "./metricsService.exception"; +export * from "./l1MetricsService.exception"; diff --git a/libs/metrics/src/exceptions/l1MetricsService.exception.ts b/libs/metrics/src/exceptions/l1MetricsService.exception.ts new file mode 100644 index 0000000..40f1fb8 --- /dev/null +++ b/libs/metrics/src/exceptions/l1MetricsService.exception.ts @@ -0,0 +1,6 @@ +export class L1MetricsServiceException extends Error { + constructor(message: string) { + super(message); + this.name = "L1MetricsServiceException"; + } +} diff --git a/libs/metrics/src/exceptions/metricsService.exception.ts b/libs/metrics/src/exceptions/metricsService.exception.ts deleted file mode 100644 index 56c418d..0000000 --- a/libs/metrics/src/exceptions/metricsService.exception.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/l1/l1MetricsService.ts b/libs/metrics/src/l1/l1MetricsService.ts index dc69d6c..6db4d8c 100644 --- a/libs/metrics/src/l1/l1MetricsService.ts +++ b/libs/metrics/src/l1/l1MetricsService.ts @@ -155,11 +155,7 @@ export class L1MetricsService { * @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); + async getBatchesInfo(chainId: ChainId): Promise { let diamondProxyAddress: Address | undefined = this.diamondContracts.get(chainId); if (!diamondProxyAddress) { @@ -167,8 +163,11 @@ export class L1MetricsService { this.bridgeHub.address, this.bridgeHub.abi, "getHyperchain", - [chainIdBn], + [chainId], ); + if (diamondProxyAddress == zeroAddress) { + throw new InvalidChainId(`Chain ID ${chainId} doesn't exist on the ecosystem`); + } this.diamondContracts.set(chainId, diamondProxyAddress); } @@ -202,7 +201,7 @@ export class L1MetricsService { * 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 { + async tvl(chainId: ChainId): Promise { const erc20Addresses = erc20Tokens.map((token) => token.contractAddress); const balances = await this.fetchTokenBalancesByChain(chainId, erc20Addresses); @@ -221,8 +220,7 @@ 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) => { @@ -230,14 +228,14 @@ export class L1MetricsService { address: this.sharedBridge.address, abi: this.sharedBridge.abi, functionName: "chainBalance", - args: [chainIdBn, tokenAddress], + args: [chainId, tokenAddress], } as const; }), { address: this.sharedBridge.address, abi: this.sharedBridge.abi, functionName: "chainBalance", - args: [chainIdBn, ETH_TOKEN_ADDRESS], + args: [chainId, ETH_TOKEN_ADDRESS], } as const, ], allowFailure: false, @@ -247,7 +245,7 @@ export class L1MetricsService { } //TODO: Implement chainType. - async chainType(_chainId: number): Promise<"validium" | "rollup"> { + async chainType(_chainId: ChainId): Promise<"validium" | "rollup"> { return "rollup"; } @@ -304,7 +302,7 @@ export class L1MetricsService { } //TODO: Implement feeParams. - async feeParams(_chainId: number): Promise<{ + async feeParams(_chainId: ChainId): Promise<{ batchOverheadL1Gas: number; maxPubdataPerBatch: number; maxL2GasPerBatch: number; diff --git a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts index 85cca20..7773bdc 100644 --- a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts +++ b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts @@ -247,7 +247,7 @@ describe("L1MetricsService", () => { describe("getBatchesInfo", () => { it("returns batches info for chain id", async () => { - const chainId = 324; // this is ZKsyncEra chain id + const chainId = 324n; // this is ZKsyncEra chain id const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890"; l1MetricsService["diamondContracts"].set(chainId, mockedDiamondProxyAddress); @@ -289,15 +289,16 @@ describe("L1MetricsService", () => { 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("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 = 324; // this is ZKsyncEra chain id + const chainId = 324n; // this is ZKsyncEra chain id const mockedDiamondProxyAddress = "0x1234567890123456789012345678901234567890"; l1MetricsService["diamondContracts"].clear(); @@ -358,7 +359,7 @@ describe("L1MetricsService", () => { 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); @@ -435,7 +436,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, @@ -454,7 +455,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"); }); }); @@ -610,7 +611,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, diff --git a/libs/shared/src/types/utils.type.ts b/libs/shared/src/types/utils.type.ts index 31744f7..f74b627 100644 --- a/libs/shared/src/types/utils.type.ts +++ b/libs/shared/src/types/utils.type.ts @@ -3,4 +3,4 @@ import { AbiItem } from "viem"; export type AbiWithAddress = { abi: T; address: Address }; -export type ChainId = number; +export type ChainId = bigint;