Skip to content

Commit

Permalink
feat: integrate l2 chain info to chain ep and env configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnigir1 committed Sep 3, 2024
1 parent d5b2901 commit ffc9f50
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 20 deletions.
6 changes: 5 additions & 1 deletion apps/api/src/common/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ dotenv.config();

const logger = Logger.getInstance();

const env = validationSchema.safeParse(process.env);
const env = validationSchema.safeParse({
...process.env,
L2_RPC_URLS_MAP: JSON.parse(process.env.L2_RPC_URLS_MAP || "{}"),
});

if (!env.success) {
logger.error(env.error.issues.map((issue) => JSON.stringify(issue)).join("\n"));
Expand Down Expand Up @@ -78,6 +81,7 @@ export const config = {
rpcUrls: envData.L1_RPC_URLS,
chain: getChain(envData.ENVIRONMENT),
},
l2: envData.L2_RPC_URLS_MAP,
bridgeHubAddress: envData.BRIDGE_HUB_ADDRESS as Address,
sharedBridgeAddress: envData.SHARED_BRIDGE_ADDRESS as Address,
stateTransitionManagerAddresses: envData.STATE_MANAGER_ADDRESSES as Address[],
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/common/config/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ const urlArraySchema = z
message: "Must be a comma-separated list of valid URLs",
});

const urlArrayMapSchema = z.record(
z.union([z.coerce.number().int(), z.string().regex(/^\d+$/)]), // key: number or string number
urlArraySchema,
);

const baseSchema = z.object({
PORT: z.coerce.number().positive().default(3000),
BRIDGE_HUB_ADDRESS: addressSchema,
SHARED_BRIDGE_ADDRESS: addressSchema,
STATE_MANAGER_ADDRESSES: addressArraySchema,
ENVIRONMENT: z.enum(["mainnet", "testnet", "local"]).default("mainnet"),
L1_RPC_URLS: urlArraySchema,
L2_RPC_URLS_MAP: urlArrayMapSchema,
PRICING_SOURCE: z.enum(["dummy", "coingecko"]).default("dummy"),
DUMMY_PRICE: z.coerce.number().optional(),
COINGECKO_API_KEY: z.string().optional(),
Expand Down
23 changes: 19 additions & 4 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { inspect } from "util";
import { caching } from "cache-manager";

import { EvmProvider } from "@zkchainhub/chain-providers";
import { EvmProvider, ZKChainProvider } from "@zkchainhub/chain-providers";
import { MetadataProviderFactory } from "@zkchainhub/metadata";
import { L1MetricsService } from "@zkchainhub/metrics";
import { L1MetricsService, L2MetricsService } from "@zkchainhub/metrics";
import { PricingProviderFactory } from "@zkchainhub/pricing";
import { Logger } from "@zkchainhub/shared";
import { ChainId, Logger } from "@zkchainhub/shared";

import { App } from "./app.js";
import { config } from "./common/config/index.js";
Expand Down Expand Up @@ -40,7 +40,22 @@ const main = async (): Promise<void> => {
metadataProvider,
logger,
);
const metricsController = new MetricsController(l1MetricsService, metadataProvider, logger);

const l2ChainsConfigMap = config.l2;
const l2MetricsMap = new Map<ChainId, L2MetricsService>();

for (const [chainId, rpcUrls] of Object.entries(l2ChainsConfigMap)) {
const provider = new ZKChainProvider(rpcUrls, logger);
const metricsService = new L2MetricsService(provider, logger);
l2MetricsMap.set(BigInt(chainId), metricsService);
}

const metricsController = new MetricsController(
l1MetricsService,
l2MetricsMap,
metadataProvider,
logger,
);
const metricsRouter = new MetricsRouter(metricsController, logger);

const app = new App(config, [metricsRouter], logger);
Expand Down
54 changes: 48 additions & 6 deletions apps/api/src/metrics/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { BigNumber } from "bignumber.js";

import { IMetadataProvider } from "@zkchainhub/metadata";
import { L1MetricsService } from "@zkchainhub/metrics";
import { ILogger } from "@zkchainhub/shared";
import { L1MetricsService, L2MetricsService } from "@zkchainhub/metrics";
import { ChainId, ILogger } from "@zkchainhub/shared";

import { EcosystemInfo, ZKChainInfo, ZkChainMetadata } from "../dto/response/index.js";
import { EcosystemInfo, L2ChainInfo, ZKChainInfo, ZkChainMetadata } from "../dto/response/index.js";
import { ChainNotFound } from "../exceptions/index.js";

export class MetricsController {
constructor(
private readonly l1MetricsService: L1MetricsService,
private readonly l2MetricsMap: Map<ChainId, L2MetricsService>,
private readonly metadataProvider: IMetadataProvider,
private readonly logger: ILogger,
) {}
Expand Down Expand Up @@ -65,20 +66,24 @@ export class MetricsController {

async getChain(chainId: number) {
const chainIdBn = BigInt(chainId);
const chainsMetadata = await this.metadataProvider.getChainsMetadata();
const metadata = chainsMetadata.get(chainIdBn);
const ecosystemChainIds = await this.l1MetricsService.getChainIds();

if (!ecosystemChainIds.includes(chainIdBn)) {
throw new ChainNotFound(chainIdBn);
}
const chainsMetadata = await this.metadataProvider.getChainsMetadata();
const metadata = chainsMetadata.get(chainIdBn);
const l2MetricsService = this.l2MetricsMap.get(chainIdBn);

const [tvl, batchesInfo, feeParams, baseTokenInfo] = await Promise.all([
this.l1MetricsService.tvl(chainIdBn),
this.l1MetricsService.getBatchesInfo(chainIdBn),
this.l1MetricsService.feeParams(chainIdBn),
this.l1MetricsService.getBaseTokens([chainIdBn]),
]);
let l2ChainInfo: L2ChainInfo | undefined;
if (l2MetricsService) {
l2ChainInfo = await this.getL2ChainInfo(l2MetricsService, Number(batchesInfo.verified));
}

const baseToken = baseTokenInfo[0];
const baseZkChainInfo = {
Expand All @@ -102,6 +107,7 @@ export class MetricsController {
...baseZkChainInfo,
chainType: await this.l1MetricsService.chainType(chainIdBn),
baseToken,
l2ChainInfo,
});
}

Expand All @@ -112,6 +118,42 @@ export class MetricsController {
chainType: metadataRest.chainType,
baseToken: metadataRest.baseToken,
metadata: new ZkChainMetadata(metadataRest),
l2ChainInfo,
});
}

private async getL2ChainInfo(
l2MetricsService: L2MetricsService,
verifiedBatches: number,
): Promise<L2ChainInfo | undefined> {
const [tpsResult, avgBlockTimeResult, lastBlockResult, lastBlockVerifiedResult] =
await Promise.allSettled([
l2MetricsService.tps(),
l2MetricsService.avgBlockTime(),
l2MetricsService.lastBlock(),
l2MetricsService.getLastVerifiedBlock(verifiedBatches),
]);

if (
tpsResult.status === "rejected" &&
avgBlockTimeResult.status === "rejected" &&
lastBlockResult.status === "rejected" &&
lastBlockVerifiedResult.status === "rejected"
)
return undefined;

return new L2ChainInfo({
tps: tpsResult.status === "fulfilled" ? tpsResult.value : undefined,
avgBlockTime:
avgBlockTimeResult.status === "fulfilled" ? avgBlockTimeResult.value : undefined,
lastBlock:
lastBlockResult.status === "fulfilled"
? lastBlockResult.value.toString()
: undefined,
lastBlockVerified:
lastBlockVerifiedResult.status === "fulfilled"
? lastBlockVerifiedResult.value
: undefined,
});
}
}
10 changes: 5 additions & 5 deletions apps/api/src/metrics/dto/response/l2Metrics.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,28 @@ export class L2ChainInfo {
* @type {number}
* @memberof L2ChainInfo
*/
tps: number;
tps?: number;

/**
* Average block time in seconds.
* @type {number}
* @memberof L2ChainInfo
*/
avgBlockTime: number;
avgBlockTime?: number;

/**
* The number of the last block.
* @type {number}
* @type {string}
* @memberof L2ChainInfo
*/
lastBlock: number;
lastBlock?: string;

/**
* The number of the last verified block.
* @type {number}
* @memberof L2ChainInfo
*/
lastBlockVerified: number;
lastBlockVerified?: number;

constructor(data: L2ChainInfo) {
this.tps = data.tps;
Expand Down
133 changes: 129 additions & 4 deletions apps/api/test/unit/metrics/metricsController.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import BigNumber from "bignumber.js";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { IMetadataProvider } from "@zkchainhub/metadata";
import { AssetTvl, GasInfo, L1MetricsService } from "@zkchainhub/metrics";
import { AssetTvl, GasInfo, L1MetricsService, L2MetricsService } from "@zkchainhub/metrics";
import {
ChainId,
ChainType,
Expand Down Expand Up @@ -31,6 +31,7 @@ const mockLogger: ILogger = {
describe("MetricsController", () => {
let service: MetricsController;
let l1MetricsService: L1MetricsService;
let l2MetricsMap: Map<ChainId, L2MetricsService>;
let metadataProvider: IMetadataProvider;

afterEach(() => {
Expand All @@ -52,7 +53,13 @@ describe("MetricsController", () => {
getChainsMetadata: vi.fn(),
getTokensMetadata: vi.fn(),
};
service = new MetricsController(l1MetricsService, metadataProvider, mockLogger);
l2MetricsMap = new Map<ChainId, L2MetricsService>();
service = new MetricsController(
l1MetricsService,
l2MetricsMap,
metadataProvider,
mockLogger,
);
});

it("should be defined", () => {
Expand Down Expand Up @@ -84,7 +91,6 @@ describe("MetricsController", () => {
symbol: "zkCRO",
name: "zkCRO",
contractAddress: "0x28Ff2E4dD1B58efEB0fC138602A28D5aE81e44e2",
coingeckoId: "unknown",
type: "erc20",
imageUrl: "https://zkevm.cronos.org/images/chains/zkevm.svg",
decimals: 18,
Expand Down Expand Up @@ -259,7 +265,6 @@ describe("MetricsController", () => {
symbol: "zkCRO",
name: "zkCRO",
contractAddress: "0x28Ff2E4dD1B58efEB0fC138602A28D5aE81e44e2",
coingeckoId: "unknown",
type: "erc20",
imageUrl: "https://zkevm.cronos.org/images/chains/zkevm.svg",
decimals: 18,
Expand Down Expand Up @@ -679,6 +684,126 @@ describe("MetricsController", () => {
);
});

it("returns the chain information with L2 Metrics for the specified chain ID", async () => {
const l2MetricsService = {
tps: vi.fn(),
avgBlockTime: vi.fn(),
lastBlock: vi.fn(),
getLastVerifiedBlock: vi.fn(),
} as unknown as L2MetricsService;
l2MetricsMap.set(324n, l2MetricsService);

const chainId = 324;
const chainIdBn = BigInt(chainId);
const mockZkTvl: AssetTvl[] = [
{
amount: "118306.55998740125385395",
amountUsd: "300833115.01308356813367811665",
price: "2542.827",
name: "Ethereum",
symbol: "ETH",
contractAddress: null,
type: "native",
imageUrl:
"https://coin-images.coingecko.com/coins/images/279/large/ethereum.png?1696501628",
decimals: 18,
},
{
amount: "56310048.030096",
amountUsd: "56366358.078126096",
price: "1.001",
name: "USDC",
symbol: "USDC",
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
imageUrl:
"https://coin-images.coingecko.com/coins/images/6319/large/usdc.png?1696506694",
type: "erc20",
decimals: 6,
},
{
amount: "998459864.823799773445941598",
amountUsd: "11981518.377885597281351299176",
price: "0.012",
name: "Koi",
symbol: "KOI",
contractAddress: "0x9D14BcE1dADdf408d77295BB1be9b343814f44DE",
imageUrl:
"https://coin-images.coingecko.com/coins/images/35766/large/Koi_logo.png?1709782399",
type: "erc20",
decimals: 18,
},
];
const mockBatchesInfo = { commited: 1n, verified: 1n, executed: 1n };
const mockFeeParams = {
pubdataPricingMode: 1,
batchOverheadL1Gas: 50000,
maxL2GasPerBatch: 100000,
maxPubdataPerBatch: 8000,
minimalL2GasPrice: 2000000000n,
priorityTxMaxPubdata: 1000,
};
const zkChainsMetadata = new Map<bigint, ZKChainMetadataItem>();
zkChainsMetadata.set(chainIdBn, {
chainId: 324n,
chainType: "Rollup",
baseToken: nativeToken,
iconUrl: "https://s2.coinmarketcap.com/static/img/coins/64x64/24091.png",
name: "ZKsyncERA",
publicRpcs: [
"https://mainnet.era.zksync.io",
"https://zksync.drpc.org",
"https://zksync.meowrpc.com",
],
explorerUrl: "https://explorer.zksync.io/",
launchDate: 1679626800,
});

vi.spyOn(metadataProvider, "getChainsMetadata").mockResolvedValue(zkChainsMetadata);
vi.spyOn(l1MetricsService, "getChainIds").mockResolvedValue([chainIdBn]);
vi.spyOn(l1MetricsService, "tvl").mockResolvedValue(mockZkTvl);
vi.spyOn(l1MetricsService, "getBatchesInfo").mockResolvedValue(mockBatchesInfo);
vi.spyOn(l1MetricsService, "feeParams").mockResolvedValue(mockFeeParams);
vi.spyOn(l1MetricsService, "getBaseTokens").mockResolvedValue([nativeToken]);
vi.spyOn(l2MetricsService, "tps").mockResolvedValue(1.5);
vi.spyOn(l2MetricsService, "avgBlockTime").mockResolvedValue(10.5);
vi.spyOn(l2MetricsService, "lastBlock").mockResolvedValue(5000n);
vi.spyOn(l2MetricsService, "getLastVerifiedBlock").mockResolvedValue(4950);

const result = await service.getChain(chainId);

expect(result).toEqual(
new ZKChainInfo({
batchesInfo: {
commited: mockBatchesInfo.commited.toString(),
verified: mockBatchesInfo.verified.toString(),
executed: mockBatchesInfo.executed.toString(),
},
tvl: mockZkTvl,
feeParams: {
batchOverheadL1Gas: mockFeeParams.batchOverheadL1Gas,
maxL2GasPerBatch: mockFeeParams.maxL2GasPerBatch,
maxPubdataPerBatch: mockFeeParams.maxPubdataPerBatch,
minimalL2GasPrice: mockFeeParams.minimalL2GasPrice.toString(),
priorityTxMaxPubdata: mockFeeParams.priorityTxMaxPubdata,
},
chainType: zkChainsMetadata.get(chainIdBn)?.chainType as ChainType,
baseToken: zkChainsMetadata.get(chainIdBn)?.baseToken as Token<
"erc20" | "native"
>,
metadata: new ZkChainMetadata(
zkChainsMetadata.get(chainIdBn) as ZkChainMetadata,
),
l2ChainInfo: {
tps: 1.5,
avgBlockTime: 10.5,
lastBlock: "5000",
lastBlockVerified: 4950,
},
}),
);
expect(l2MetricsService.getLastVerifiedBlock).toHaveBeenCalledWith(1);
});

it("should throw ChainNotFound exception when chain ID is not found", async () => {
const chainId = 999;
vi.spyOn(metadataProvider, "getChainsMetadata").mockResolvedValue(new Map());
Expand Down

0 comments on commit ffc9f50

Please sign in to comment.