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/metrics controller #23

Merged
merged 7 commits into from
Jul 18, 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
13 changes: 12 additions & 1 deletion apps/api/src/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ import { ProvidersModule } from "@packages/providers";

import { ApiController } from "./api.controller";
import { RequestLoggerMiddleware } from "./common/middleware/request.middleware";
import { MetricsController } from "./metrics/metrics.controller";

Copy link
Collaborator

Choose a reason for hiding this comment

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

natspec missing here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

on this .module you say we add the Natspec due to the consumer applied? because in general .module doesn't have anything to be documented

/**
* The main API module of the application.
* Here we import all required modules and register the controllers for the ZKchainHub API.
*/
@Module({
imports: [ProvidersModule],
controllers: [ApiController],
controllers: [ApiController, MetricsController],
providers: [],
})
export class ApiModule implements NestModule {
/**
* Configures middleware for the module.
* Applies RequestLoggerMiddleware to all routes except '/docs' and '/docs/(.*)'.
*
* @param {MiddlewareConsumer} consumer - The middleware consumer provided by NestJS.
*/
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestLoggerMiddleware).exclude("/docs", "/docs/(.*)").forRoutes("*");
}
Expand Down
26 changes: 26 additions & 0 deletions apps/api/src/common/pipes/parsePositiveInt.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from "@nestjs/common";

/**
* A pipe that transforms and validates input strings to positive integers.
* @implements {PipeTransform<string, number>}
*/
@Injectable()
export class ParsePositiveIntPipe implements PipeTransform<string, number> {
/**
* Transforms and validates the input value.
*
* @param {string} value - The input value to be transformed and validated.
* @param {ArgumentMetadata} metadata - Metadata about the transformed argument.
* @returns {number} The parsed positive integer.
* @throws {BadRequestException} If the input is not a valid positive integer.
*/
transform(value: string, metadata: ArgumentMetadata): number {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue) || parsedValue < 0) {
throw new BadRequestException(
`Validation failed: Parameter ${metadata.data} must be a positive integer`,
);
}
return parsedValue;
}
}
41 changes: 41 additions & 0 deletions apps/api/src/metrics/metrics.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Test, TestingModule } from "@nestjs/testing";

import { MetricsController } from "./metrics.controller";
import { getEcosystemInfo, getZKChainInfo } from "./mocks/metrics.mock";

describe("MetricsController", () => {
let controller: MetricsController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [MetricsController],
}).compile();

controller = module.get<MetricsController>(MetricsController);
});

it("should be defined", () => {
expect(controller).toBeDefined();
});

describe("getEcosystem", () => {
it("should return the ecosystem information", async () => {
const expectedInfo = getEcosystemInfo();

const result = await controller.getEcosystem();

expect(result).toEqual(expectedInfo);
});
});

describe("getChain", () => {
it("should return the chain information for the specified chain ID", async () => {
const chainId = 123;
const expectedInfo = getZKChainInfo(chainId);

const result = await controller.getChain(chainId);

expect(result).toEqual(expectedInfo);
});
});
});
34 changes: 34 additions & 0 deletions apps/api/src/metrics/metrics.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Controller, Get, Param } from "@nestjs/common";
import { ApiResponse, ApiTags } from "@nestjs/swagger";
import { ZKChainInfo } from "@shared/dtos/dto/chain.dto";

import { ParsePositiveIntPipe } from "../common/pipes/parsePositiveInt.pipe";
import { getEcosystemInfo, getZKChainInfo } from "./mocks/metrics.mock";

@ApiTags("metrics")
@Controller("metrics")
/**
* Controller for handling metrics related endpoints.
*/
export class MetricsController {
/**
* Retrieves the ecosystem information.
* @returns {Promise<EcosystemInfo>} The ecosystem information.
*/
@Get("/ecosystem")
public async getEcosystem() {
return getEcosystemInfo();
}

/**
* Retrieves the chain information for the specified chain ID.
* @param {number} chainId - The ID of the chain.
* @returns {Promise<ZKChainInfo>} The chain information.
*/

@ApiResponse({ status: 200, type: ZKChainInfo })
@Get("zkchain/:chainId")
public async getChain(@Param("chainId", new ParsePositiveIntPipe()) chainId: number) {
return getZKChainInfo(chainId);
}
}
104 changes: 104 additions & 0 deletions apps/api/src/metrics/mocks/metrics.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { EcosystemInfo } from "@shared/dtos";
import { ZKChainInfo } from "@shared/dtos/dto/chain.dto";
import { L2ChainInfo } from "@shared/dtos/dto/l2Metrics.dto";
import { Metadata } from "@shared/dtos/dto/metadata.dto";

export const getEcosystemInfo = () => {
const mock = new EcosystemInfo({
l1Tvl: { ETH: 1000000, USDC: 500000 },
ethGasInfo: {
gasPrice: 50,
ethTransfer: 21000,
erc20Transfer: 65000,
},
zkChains: [
{
chainId: 0,
chainType: "Rollup",
nativeToken: "ETH",
tvl: 1000000,
metadata: true,
rpc: true,
},
{
chainId: 1,
chainType: "Validium",
nativeToken: "ETH",
tvl: 500000,
metadata: true,
rpc: false,
},
{
chainId: 2,
chainType: "Rollup",
tvl: 300000,
metadata: false,
rpc: true,
},
{
chainId: 3,
chainType: "Rollup",
tvl: 10000,
metadata: false,
rpc: false,
},
],
});
return mock;
};

const mockMetadata: Metadata = {
iconUrl: "https://s2.coinmarketcap.com/static/img/coins/64x64/24091.png",
chainName: "ZKsyncERA",
publicRpcs: [
{ url: "https://mainnet.era.zksync.io", status: true },
{ url: "https://1rpc.io/zksync2-era", status: true },
{ url: "https://zksync.drpc.org", status: false },
],
explorerUrl: "https://explorer.zksync.io/",
launchDate: 1679626800,
environment: "mainnet",
nativeToken: "ETH",
};

const mockL2Info: L2ChainInfo = {
tps: 10000000,
avgBlockTime: 12,
lastBlock: 1000000,
lastBlockVerified: 999999,
};

export const getZKChainInfo = (chainId: number): ZKChainInfo => {
const mock = new ZKChainInfo({
chainType: "Rollup",
tvl: { ETH: 1000000, USDC: 500000 },
batchesInfo: {
commited: 100,
verified: 90,
proved: 80,
},
feeParams: {
batchOverheadL1Gas: 50000,
maxPubdataPerBatch: 120000,
maxL2GasPerBatch: 10000000,
priorityTxMaxPubdata: 15000,
minimalL2GasPrice: 0.25,
},
});
switch (chainId) {
case 0:
mock.metadata = mockMetadata;
mock.l2ChainInfo = mockL2Info;
break;
case 1:
mock.metadata = mockMetadata;
break;
case 2:
mock.l2ChainInfo = mockL2Info;
break;
default:
break;
}

return mock;
};
3 changes: 2 additions & 1 deletion apps/api/test/jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@packages/providers(|/.*)$": "<rootDir>/libs/providers/src/$1"
"^@packages/providers(|/.*)$": "<rootDir>/libs/providers/src/$1",
"^@shared/dtos(|/.*)$": "<rootDir>/libs/dtos/src/$1"
}
}
116 changes: 116 additions & 0 deletions apps/api/test/metrics.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { INestApplication } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import request from "supertest";

import { ApiModule } from "../src/api.module";

describe("MetricsController (e2e)", () => {
let app: INestApplication;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [ApiModule],
}).compile();

app = moduleFixture.createNestApplication();
await app.init();
});

afterEach(async () => {
await app.close();
});

describe("/ecosystem (GET)", () => {
it("/ecosystem (GET)", () => {
return request(app.getHttpServer())
.get("/metrics/ecosystem")
.expect(200)
.expect(({ body }) => {
expect(body.l1Tvl).toBeDefined();
expect(body.ethGasInfo).toBeDefined();
expect(body.zkChains).toBeDefined();
expect(body.zkChains.length).toBeGreaterThan(0);
});
});
});

describe("/chain/:chainId (GET)", () => {
it("correct request for RPC + METADATA", () => {
return request(app.getHttpServer())
.get("/metrics/zkchain/0")
.expect(200)
.expect(({ body }) => {
expect(body.chainType).toBeDefined();
expect(body.tvl).toBeDefined();
expect(body.batchesInfo).toBeDefined();
expect(body.feeParams).toBeDefined();
expect(body.metadata).toBeDefined();
expect(body.l2ChainInfo).toBeDefined();
});
});

it("correct request for METADATA", () => {
return request(app.getHttpServer())
.get("/metrics/zkchain/1")
.expect(200)
.expect(({ body }) => {
expect(body.chainType).toBeDefined();
expect(body.tvl).toBeDefined();
expect(body.batchesInfo).toBeDefined();
expect(body.feeParams).toBeDefined();
expect(body.metadata).toBeDefined();
expect(body.l2ChainInfo).toBeUndefined();
});
});

it("correct request for RPC", () => {
return request(app.getHttpServer())
.get("/metrics/zkchain/2")
.expect(200)
.expect(({ body }) => {
expect(body.chainType).toBeDefined();
expect(body.tvl).toBeDefined();
expect(body.batchesInfo).toBeDefined();
expect(body.feeParams).toBeDefined();
expect(body.metadata).toBeUndefined();
expect(body.l2ChainInfo).toBeDefined();
});
});

it("correct request for NO RPC + METADATA", () => {
return request(app.getHttpServer())
.get("/metrics/zkchain/3")
.expect(200)
.expect(({ body }) => {
expect(body.chainType).toBeDefined();
expect(body.tvl).toBeDefined();
expect(body.batchesInfo).toBeDefined();
expect(body.feeParams).toBeDefined();
expect(body.metadata).toBeUndefined();
expect(body.l2ChainInfo).toBeUndefined();
});
});

it("invalid negative number for chain id", () => {
return request(app.getHttpServer())
.get("/metrics/zkchain/-1")
.expect(400)
.expect(({ body }) => {
expect(body.message).toEqual(
"Validation failed: Parameter chainId must be a positive integer",
);
});
});

it("not a number for chain id", () => {
return request(app.getHttpServer())
.get("/metrics/zkchain/notanumber")
.expect(400)
.expect(({ body }) => {
expect(body.message).toEqual(
"Validation failed: Parameter chainId must be a positive integer",
);
});
});
});
});
Loading