Skip to content

Commit

Permalink
feat: refactor using metadata package (#57)
Browse files Browse the repository at this point in the history
# 🤖 Linear

Closes ZKS-206 ZKS-203 ZKS-222 ZKS-214

## Description

- Refactor `api` to use `MetadataProvider` for fetching chains metadata
- Refactor `L1 metrics service` to use `MetadataProvider` for fetching
tokens metadata
- Refactor `CoingeckoProvider` to remove strong dependency on it from
`L1 Metrics Service`
- `MetadataProviderFactory` to instantiate provider based on user
election from env config
  • Loading branch information
0xnigir1 authored Aug 28, 2024
1 parent 98b6036 commit 435d870
Show file tree
Hide file tree
Showing 30 changed files with 1,096 additions and 397 deletions.
9 changes: 8 additions & 1 deletion apps/api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,11 @@ COINGECKO_API_KEY='' # CoinGecko API key
COINGECKO_BASE_URL='' # CoinGecko API base URL for the API version you are using
COINGECKO_API_TYPE='' # CoinGecko API Type: 'demo' or 'pro'

CACHE_TTL=60 # Cache TTL in seconds
CACHE_TTL=60 # Cache TTL in seconds for Providers and Services

## Metadata Vars
METADATA_SOURCE='' # Metadata source: 'github' | 'local' | 'static'
METADATA_TOKEN_URL='' # Metadata token URL (required if METADATA_SOURCE is 'github')
METADATA_CHAIN_URL='' # Metadata chain URL (required if METADATA_SOURCE is 'github')
METADATA_TOKEN_JSON_PATH='' # Metadata token JSON file path (required if METADATA_SOURCE is 'local')
METADATA_CHAIN_JSON_PATH='' # Metadata chain JSON file path (required if METADATA_SOURCE is 'local')
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"dependencies": {
"@zkchainhub/chain-providers": "workspace:*",
"@zkchainhub/metadata": "workspace:*",
"@zkchainhub/metrics": "workspace:*",
"@zkchainhub/pricing": "workspace:*",
"@zkchainhub/shared": "workspace:*",
Expand Down
64 changes: 26 additions & 38 deletions apps/api/src/common/config/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,16 @@
import dotenv from "dotenv";
import { Address, isAddress } from "viem";
import { Address } from "viem";
import { mainnet, zksync } from "viem/chains";
import { z } from "zod";

import { MetadataConfig } from "@zkchainhub/metadata";
import { Logger } from "@zkchainhub/shared";

import { validationSchema } from "./schemas.js";

dotenv.config();

const logger = Logger.getInstance();

const addressArraySchema = z
.string()
.transform((str) => str.split(","))
.refine((addresses) => addresses.every((address) => isAddress(address)), {
message: "Must be a comma-separated list of valid Addresses",
});
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 validationSchema = z.object({
PORT: z.coerce.number().positive().default(3000),
BRIDGE_HUB_ADDRESS: addressSchema,
SHARED_BRIDGE_ADDRESS: addressSchema,
STATE_MANAGER_ADDRESSES: addressArraySchema,
L1_RPC_URLS: urlArraySchema,
L2_RPC_URLS: z
.union([z.literal(""), urlArraySchema])
.optional()
.transform((val) => {
if (val === undefined || val === "") return [];
return val;
}),
COINGECKO_API_KEY: z.string(),
COINGECKO_BASE_URL: z.string().url().default("https://api.coingecko.com/api/v3/"),
COINGECKO_API_TYPE: z.enum(["demo", "pro"]).default("demo"),
CACHE_TTL: z.coerce.number().positive().default(60),
});

const env = validationSchema.safeParse(process.env);

if (!env.success) {
Expand All @@ -54,6 +20,27 @@ if (!env.success) {

const { data: envData } = env;

const createMetadataConfig = (
env: typeof envData,
): MetadataConfig<typeof envData.METADATA_SOURCE> => {
switch (env.METADATA_SOURCE) {
case "github":
return {
source: "github",
tokenUrl: env.METADATA_TOKEN_URL,
chainUrl: env.METADATA_CHAIN_URL,
};
case "local":
return {
source: "local",
tokenJsonPath: env.METADATA_TOKEN_JSON_PATH,
chainJsonPath: env.METADATA_CHAIN_JSON_PATH,
};
case "static":
return { source: "static" };
}
};

export const config = {
port: envData.PORT,
l1: {
Expand All @@ -80,6 +67,7 @@ export const config = {
apiType: envData.COINGECKO_API_TYPE,
},
},
metadata: createMetadataConfig(envData),
} as const;

export type ConfigType = typeof config;
82 changes: 82 additions & 0 deletions apps/api/src/common/config/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { isAddress } from "viem";
import { z } from "zod";

const addressArraySchema = z
.string()
.transform((str) => str.split(","))
.refine((addresses) => addresses.every((address) => isAddress(address)), {
message: "Must be a comma-separated list of valid Addresses",
});
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 baseSchema = z.object({
PORT: z.coerce.number().positive().default(3000),
BRIDGE_HUB_ADDRESS: addressSchema,
SHARED_BRIDGE_ADDRESS: addressSchema,
STATE_MANAGER_ADDRESSES: addressArraySchema,
L1_RPC_URLS: urlArraySchema,
L2_RPC_URLS: z
.union([z.literal(""), urlArraySchema])
.optional()
.transform((val) => {
if (val === undefined || val === "") return [];
return val;
}),
COINGECKO_API_KEY: z.string(),
COINGECKO_BASE_URL: z.string().url().default("https://api.coingecko.com/api/v3/"),
COINGECKO_API_TYPE: z.enum(["demo", "pro"]).default("demo"),
CACHE_TTL: z.coerce.number().positive().default(60),
METADATA_SOURCE: z.enum(["github", "local", "static"] as const),
METADATA_TOKEN_URL: z.string().url().optional(),
METADATA_CHAIN_URL: z.string().url().optional(),
METADATA_TOKEN_JSON_PATH: z.string().optional(),
METADATA_CHAIN_JSON_PATH: z.string().optional(),
});

const githubSchema = baseSchema
.extend({
METADATA_SOURCE: z.literal("github"),
METADATA_TOKEN_URL: z.string().url(),
METADATA_CHAIN_URL: z.string().url(),
})
.omit({
METADATA_TOKEN_JSON_PATH: true,
METADATA_CHAIN_JSON_PATH: true,
});

const localSchema = baseSchema
.extend({
METADATA_SOURCE: z.literal("local"),
METADATA_TOKEN_JSON_PATH: z.string(),
METADATA_CHAIN_JSON_PATH: z.string(),
})
.omit({
METADATA_TOKEN_URL: true,
METADATA_CHAIN_URL: true,
});

const staticSchema = baseSchema
.extend({
METADATA_SOURCE: z.literal("static"),
})
.omit({
METADATA_TOKEN_URL: true,
METADATA_CHAIN_URL: true,
METADATA_TOKEN_JSON_PATH: true,
METADATA_CHAIN_JSON_PATH: true,
});

export const validationSchema = z.discriminatedUnion("METADATA_SOURCE", [
githubSchema,
localSchema,
staticSchema,
]);
10 changes: 9 additions & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { inspect } from "util";
import { caching } from "cache-manager";

import { EvmProvider } from "@zkchainhub/chain-providers";
import { MetadataProviderFactory } from "@zkchainhub/metadata";
import { L1MetricsService } from "@zkchainhub/metrics";
import { CoingeckoProvider } from "@zkchainhub/pricing";
import { Logger } from "@zkchainhub/shared";
Expand All @@ -28,15 +29,22 @@ const main = async (): Promise<void> => {
memoryCache,
logger,
);

const metadataProvider = MetadataProviderFactory.create(config.metadata, {
logger,
cache: memoryCache,
});

const l1MetricsService = new L1MetricsService(
config.bridgeHubAddress,
config.sharedBridgeAddress,
config.stateTransitionManagerAddresses,
evmProvider,
pricingProvider,
metadataProvider,
logger,
);
const metricsController = new MetricsController(l1MetricsService, logger);
const metricsController = new MetricsController(l1MetricsService, metadataProvider, logger);
const metricsRouter = new MetricsRouter(metricsController, logger);

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

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

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

export class MetricsController {
constructor(
private readonly l1MetricsService: L1MetricsService,
private readonly metadataProvider: IMetadataProvider,
private readonly logger: ILogger,
) {}

async getEcosystem(): Promise<EcosystemInfo> {
const [l1Tvl, gasInfo, chainIds] = await Promise.all([
const [l1Tvl, gasInfo, chainIds, chainsMetadata] = await Promise.all([
this.l1MetricsService.l1Tvl(),
this.l1MetricsService.ethGasInfo(),
this.l1MetricsService.getChainIds(),
this.metadataProvider.getChainsMetadata(),
]);

const zkChains = await Promise.all(
chainIds.map(async (chainId) => {
const metadata = zkChainsMetadata.get(chainId);
const metadata = chainsMetadata.get(chainId);
const tvl = (await this.l1MetricsService.tvl(chainId))
.reduce((acc, curr) => {
return acc.plus(BigNumber(curr.amountUsd));
return acc.plus(BigNumber(curr.amountUsd || 0));
}, new BigNumber(0))
.toString();
const chainIdStr = chainId.toString();
Expand Down Expand Up @@ -62,7 +65,8 @@ export class MetricsController {

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

if (!ecosystemChainIds.includes(chainIdBn)) {
Expand Down
Loading

0 comments on commit 435d870

Please sign in to comment.