Skip to content

Commit

Permalink
feat: l2 metrics api integration (#64)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes ZKS-229 ZKS-202

## Description

- integrate L2 Metrics info to `metrics/zkchain/:chainId` ep
- add JSON map env. var for configuring L2 chains services
  • Loading branch information
0xnigir1 authored Sep 4, 2024
1 parent d5b2901 commit 9a13844
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 31 deletions.
5 changes: 3 additions & 2 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ BRIDGE_HUB_ADDRESS=""
SHARED_BRIDGE_ADDRESS=""
STATE_MANAGER_ADDRESSES="" #CSV list of State managers addresses

L1_RPC_URLS="" #CSV list of L1 RPC URLs
L2_RPC_URLS="" #CSV list of L2 RPC URLs
L1_RPC_URLS=[] # array of L1 RPC URLs
# map from chain id to array of L2 RPC URLs
L2_RPC_URLS_MAP={}

PRICING_SOURCE="dummy" # Pricing source: 'dummy' | 'coingecko'

Expand Down
3 changes: 2 additions & 1 deletion apps/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ Available options:
| `BRIDGE_HUB_ADDRESS` | Bridge Hub address | N/A | Yes | |
| `SHARED_BRIDGE_ADDRESS` | Shared Bridge address | N/A | Yes | |
| `STATE_MANAGER_ADDRESSES` | CSV list of State manager addresses | N/A | Yes | |
| `L1_RPC_URLS` | CSV list of RPC URLs. For example, `https://eth.llamarpc.com,https://rpc.flashbots.net/fast` | N/A | Yes | You can check [Chainlist](https://chainlist.org/) for a list of public RPCs |
| `L1_RPC_URLS` | JSON array of RPC URLs. For example, ["https://eth.llamarpc.com","https://rpc.flashbots.net/fast"] | N/A | Yes | You can check [Chainlist](https://chainlist.org/) for a list of public RPCs |
| `L2_RPC_URLS_MAP` | JSON from chain id to CSV list of L2 RPC URLs. For example, {"324":"https://mainnet.era.zksync.io,https://zksync.drpc.org"} | N/A | No | You can check [Chainlist](https://chainlist.org/) for a list of public RPCs |
| `PRICING_SOURCE` | Pricing source to use (`'dummy'`, `'coingecko'`) | 'dummy' | No | |
| `DUMMY_PRICE` | Price for dummy pricing source | undefined | No | Only applicable if `PRICING_SOURCE` is `'dummy'` |
| `COINGECKO_API_KEY` | API key for CoinGecko | N/A | If `'coingecko'` is selected | You can get an API key by creating an account on [CoinGecko's site](https://www.coingecko.com/en/api) |
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/common/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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
24 changes: 17 additions & 7 deletions apps/api/src/common/config/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { isAddress } from "viem";
import { z } from "zod";

const stringToJSONSchema = z.string().transform((str, ctx): z.infer<ReturnType<typeof Object>> => {
try {
return JSON.parse(str);
} catch (e) {
ctx.addIssue({ code: "custom", message: "Invalid JSON" });
return z.NEVER;
}
});

const addressArraySchema = z
.string()
.transform((str) => str.split(","))
Expand All @@ -11,20 +20,21 @@ const addressSchema = z.string().refine((address) => isAddress(address), {
message: "Must be a valid Address",
});

const urlArraySchema = z
.string()
.transform((str) => str.split(","))
.refine((urls) => urls.every((url) => z.string().url().safeParse(url).success), {
message: "Must be a comma-separated list of valid URLs",
});
const urlArraySchema = z.array(z.string().url());

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,
L1_RPC_URLS: stringToJSONSchema.pipe(urlArraySchema),
L2_RPC_URLS_MAP: stringToJSONSchema.pipe(urlArrayMapSchema).default("{}"),
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
Loading

0 comments on commit 9a13844

Please sign in to comment.