diff --git a/libs/metrics/src/exceptions/index.ts b/libs/metrics/src/exceptions/index.ts new file mode 100644 index 0000000..43d33e4 --- /dev/null +++ b/libs/metrics/src/exceptions/index.ts @@ -0,0 +1,2 @@ +export * from "./invalidChainId.exception"; +export * from "./l1MetricsService.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/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/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/l1MetricsService.ts b/libs/metrics/src/l1/l1MetricsService.ts index 4c9f112..67f6100 100644 --- a/libs/metrics/src/l1/l1MetricsService.ts +++ b/libs/metrics/src/l1/l1MetricsService.ts @@ -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, @@ -34,15 +34,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, @@ -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 { + 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 { + async tvl(chainId: ChainId): Promise { const erc20Addresses = erc20Tokens.map((token) => token.contractAddress); const balances = await this.fetchTokenBalancesByChain(chainId, erc20Addresses); @@ -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, @@ -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"; } @@ -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; diff --git a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts index 9d06c19..aa58646 100644 --- a/libs/metrics/test/unit/l1/l1MetricsService.spec.ts +++ b/libs/metrics/test/unit/l1/l1MetricsService.spec.ts @@ -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 @@ -252,9 +252,112 @@ 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, + }); }); }); @@ -262,7 +365,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); @@ -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, @@ -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"); }); }); @@ -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")); @@ -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({ @@ -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 @@ -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); @@ -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, 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..f74b627 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; +export type ChainId = bigint;