Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: l2 metrics api integration #64

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we are already parsing a json don't you think having an array of rpcs is better ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replaced also L1_RPC_URLS to also be a json array since it's easier to read

| `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());
Comment on lines -14 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noice


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super edge case but would the key "04" be validated with this ruleset? Should we invalidate a chain id starting with a 0?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it's fine, chain ids are numbers actually but JSON doesn't allow numbers as key type so if you write, "04" to us it's the same as "4"

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);
}

Comment on lines +47 to +52
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was wondering if it would be better to encapsulate this logic inside the L2MetricsService and you just pass the map as parameter. But this approach seems to be more decoupled. 👍

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