diff --git a/apps/api/.env.example b/apps/api/.env.example index d11a207..f25f87d 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,6 +1,11 @@ PORT=3000 # Port to run the API server on -L1_RPC_URLS="" #CSV list of L2 RPC URLs -L2_RPC_URLS="" #CSV list of L1 RPC URLs + +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 COINGECKO_API_KEY='' # CoinGecko API key COINGECKO_BASE_URL='' # CoinGecko API base URL for the API version you are using diff --git a/apps/api/package.json b/apps/api/package.json index 08bc78e..72e241a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -17,15 +17,16 @@ "test:cov": "vitest run --config vitest.config.ts --coverage" }, "dependencies": { + "@zkchainhub/chain-providers": "workspace:*", "@zkchainhub/metrics": "workspace:*", "@zkchainhub/pricing": "workspace:*", - "@zkchainhub/chain-providers": "workspace:*", "@zkchainhub/shared": "workspace:*", "bignumber.js": "9.1.2", "cache-manager": "5.7.6", "cors": "2.8.5", "dotenv": "16.4.5", "express": "4.19.2", + "node-cache": "5.1.2", "swagger-ui-express": "5.0.1", "viem": "2.19.6", "yaml": "2.5.0", diff --git a/apps/api/src/common/config/index.ts b/apps/api/src/common/config/index.ts index a104055..ac603eb 100644 --- a/apps/api/src/common/config/index.ts +++ b/apps/api/src/common/config/index.ts @@ -1,5 +1,5 @@ import dotenv from "dotenv"; -import { Address } from "viem"; +import { Address, isAddress } from "viem"; import { mainnet, zksync } from "viem/chains"; import { z } from "zod"; @@ -9,6 +9,16 @@ 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(",")) @@ -18,6 +28,9 @@ const urlArraySchema = z 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]) @@ -54,9 +67,9 @@ export const config = { chain: zksync, } : undefined, - bridgeHubAddress: "0x303a465B659cBB0ab36eE643eA362c509EEb5213" as Address, - sharedBridgeAddress: "0xD7f9f54194C633F36CCD5F3da84ad4a1c38cB2cB" as Address, - stateTransitionManagerAddresses: ["0xc2eE6b6af7d616f6e27ce7F4A451Aedc2b0F5f5C"] as Address[], + bridgeHubAddress: envData.BRIDGE_HUB_ADDRESS as Address, + sharedBridgeAddress: envData.SHARED_BRIDGE_ADDRESS as Address, + stateTransitionManagerAddresses: envData.STATE_MANAGER_ADDRESSES as Address[], pricing: { cacheOptions: { ttl: envData.CACHE_TTL, diff --git a/apps/api/src/common/middleware/cache.middleware.ts b/apps/api/src/common/middleware/cache.middleware.ts new file mode 100644 index 0000000..c6fec22 --- /dev/null +++ b/apps/api/src/common/middleware/cache.middleware.ts @@ -0,0 +1,36 @@ +import { NextFunction, Request, Response } from "express"; +import NodeCache from "node-cache"; + +const DEFAULT_TTL = 60; // 1 minute +const cache = new NodeCache(); + +//FIXME: This is a temporary cache implementation. It is not recommended for production use. +// might be replaced with a more robust solution in the future. +/** + * A middleware that caches responses for a given time to live (TTL). + * @param args - The time to live (TTL) in seconds for the cached response. + * @returns A middleware function that caches responses for a given time to live (TTL). + */ +export function cacheMiddleware(args: { ttl: number } = { ttl: DEFAULT_TTL }) { + return async (req: Request, res: Response, next: NextFunction): Promise => { + const key = req.originalUrl || req.url; + const cachedResponse = await cache.get(key); + if (cachedResponse) { + // Check if the cached response is a JSON object or plain text + res.json(cachedResponse); + } else { + // Store the original send and json functions + const originalJson = res.json.bind(res); + // Override the json function + + res.json = (body): Response => { + // Cache the response body + cache.set(key, body, args.ttl); + // Call the original json function with the response body + return originalJson(body); + }; + + next(); + } + }; +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 32b0a2c..056ec4a 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -1,11 +1,11 @@ import { inspect } from "util"; import { caching } from "cache-manager"; +import { EvmProvider } from "@zkchainhub/chain-providers/dist/src/index.js"; import { L1MetricsService } from "@zkchainhub/metrics"; import { CoingeckoProvider } from "@zkchainhub/pricing"; import { Logger } from "@zkchainhub/shared"; -import { EvmProvider } from "../../../packages/chain-providers/dist/src/index.js"; import { App } from "./app.js"; import { config } from "./common/config/index.js"; import { MetricsController, MetricsRouter } from "./metrics/index.js"; diff --git a/apps/api/src/metrics/routes/index.ts b/apps/api/src/metrics/routes/index.ts index a9d6026..5f13414 100644 --- a/apps/api/src/metrics/routes/index.ts +++ b/apps/api/src/metrics/routes/index.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { ILogger } from "@zkchainhub/shared"; +import { cacheMiddleware } from "../../common/middleware/cache.middleware.js"; import { BaseRouter } from "../../common/routes/baseRouter.js"; import { ChainNotFound, MetricsController } from "../index.js"; @@ -27,7 +28,7 @@ export class MetricsRouter extends BaseRouter { * Retrieves the ecosystem information. * @returns {Promise} The ecosystem information. */ - this.router.get("/ecosystem", async (_req, res, next) => { + this.router.get("/ecosystem", cacheMiddleware(), async (_req, res, next) => { try { const data = await this.metricsController.getEcosystem(); res.json(data); @@ -42,7 +43,7 @@ export class MetricsRouter extends BaseRouter { * @param {number} chainId - The ID of the chain. * @returns {Promise} The chain information. */ - this.router.get("/zkchain/:chainId", async (req, res, next) => { + this.router.get("/zkchain/:chainId", cacheMiddleware(), async (req, res, next) => { try { const { params } = ChainIdSchema.parse(req); diff --git a/packages/metrics/src/l1/l1MetricsService.ts b/packages/metrics/src/l1/l1MetricsService.ts index d867a32..5e3bfcb 100644 --- a/packages/metrics/src/l1/l1MetricsService.ts +++ b/packages/metrics/src/l1/l1MetricsService.ts @@ -11,6 +11,7 @@ import { zeroAddress, } from "viem"; +import { EvmProvider } from "@zkchainhub/chain-providers/dist/src/index.js"; import { IPricingProvider } from "@zkchainhub/pricing"; import { BatchesInfo, @@ -27,7 +28,6 @@ import { WETH, } from "@zkchainhub/shared"; -import { EvmProvider } from "../../../chain-providers/dist/src/index.js"; import { AssetTvl, bridgeHubAbi, diff --git a/packages/metrics/test/unit/l1/l1MetricsService.spec.ts b/packages/metrics/test/unit/l1/l1MetricsService.spec.ts index 1ae4965..72be3fa 100644 --- a/packages/metrics/test/unit/l1/l1MetricsService.spec.ts +++ b/packages/metrics/test/unit/l1/l1MetricsService.spec.ts @@ -1,6 +1,7 @@ import { Address, encodeFunctionData, erc20Abi, parseEther, zeroAddress } from "viem"; import { afterEach, describe, expect, it, Mocked, vi } from "vitest"; +import { EvmProvider, MulticallNotFound } from "@zkchainhub/chain-providers/dist/src/index.js"; import { IPricingProvider } from "@zkchainhub/pricing"; import { BatchesInfo, @@ -15,7 +16,6 @@ import { WETH, } from "@zkchainhub/shared"; -import { EvmProvider, MulticallNotFound } from "../../../../chain-providers/dist/src/index.js"; import { bridgeHubAbi, diamondProxyAbi, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f8fc392..13eec1e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,9 @@ importers: express: specifier: 4.19.2 version: 4.19.2 + node-cache: + specifier: 5.1.2 + version: 5.1.2 swagger-ui-express: specifier: 5.0.1 version: 5.0.1(express@4.19.2) @@ -2087,6 +2090,13 @@ packages: } engines: { node: ">=12" } + clone@2.1.2: + resolution: + { + integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==, + } + engines: { node: ">=0.8" } + color-convert@1.9.3: resolution: { @@ -3679,6 +3689,13 @@ packages: integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==, } + node-cache@5.1.2: + resolution: + { + integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==, + } + engines: { node: ">= 8.0.0" } + node-releases@2.0.14: resolution: { @@ -6102,6 +6119,8 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + clone@2.1.2: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -7006,6 +7025,10 @@ snapshots: neo-async@2.6.2: {} + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + node-releases@2.0.14: {} npm-run-path@5.3.0: