Skip to content

Commit

Permalink
Merge branch 'main' into feat/add_script
Browse files Browse the repository at this point in the history
  • Loading branch information
Romsters authored Apr 16, 2024
2 parents f87a2aa + bc94960 commit fabf8ab
Show file tree
Hide file tree
Showing 78 changed files with 558 additions and 537 deletions.
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ zkSync Era Block Explorer is distributed under the terms of either
at your option.

## 🔗 Production links
- Testnet Goerli API: https://block-explorer-api.testnets.zksync.dev
- Testnet Sepolia API: https://block-explorer-api.sepolia.zksync.dev
- Mainnet API: https://block-explorer-api.mainnet.zksync.io
- Testnet Goerli App: https://goerli.explorer.zksync.io
- Testnet Sepolia App: https://sepolia.explorer.zksync.io
- Mainnet App: https://explorer.zksync.io
8 changes: 5 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
GRACEFUL_SHUTDOWN_TIMEOUT_MS=0
DATABASE_URL=postgres://postgres:postgres@localhost:5432/block-explorer
DATABASE_REPLICA_URL_0=
DATABASE_CONNECTION_POOL_SIZE=50
Expand All @@ -13,4 +14,4 @@ DISABLE_BFF_API_SCHEMA_DOCS=false
DISABLE_EXTERNAL_API=false
DATABASE_STATEMENT_TIMEOUT_MS=90000
CONTRACT_VERIFICATION_API_URL=http://127.0.0.1:3070
NETWORK_NAME=testnet-goerli
NETWORK_NAME=testnet-sepolia
25 changes: 21 additions & 4 deletions packages/api/src/address/address.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Token } from "../token/token.entity";
import { PagingOptionsWithMaxItemsLimitDto } from "../common/dtos";
import { AddressType } from "./dtos/baseAddress.dto";
import { TransferService } from "../transfer/transfer.service";
import { Transfer } from "../transfer/transfer.entity";
import { Transfer, TransferType } from "../transfer/transfer.entity";

jest.mock("../common/utils", () => ({
...jest.requireActual("../common/utils"),
Expand Down Expand Up @@ -286,8 +286,8 @@ describe("AddressController", () => {
(transferServiceMock.findAll as jest.Mock).mockResolvedValueOnce(transfers);
});

it("queries transfers with the specified options", async () => {
await controller.getAddressTransfers(address, listFilterOptions, pagingOptions);
it("queries transfers with the specified options when no filters provided", async () => {
await controller.getAddressTransfers(address, {}, listFilterOptions, pagingOptions);
expect(transferServiceMock.findAll).toHaveBeenCalledTimes(1);
expect(transferServiceMock.findAll).toHaveBeenCalledWith(
{
Expand All @@ -303,8 +303,25 @@ describe("AddressController", () => {
);
});

it("queries transfers with the specified options when filters are provided", async () => {
await controller.getAddressTransfers(address, { type: TransferType.Transfer }, listFilterOptions, pagingOptions);
expect(transferServiceMock.findAll).toHaveBeenCalledTimes(1);
expect(transferServiceMock.findAll).toHaveBeenCalledWith(
{
address,
type: TransferType.Transfer,
timestamp: "timestamp",
},
{
filterOptions: { type: TransferType.Transfer, ...listFilterOptions },
...pagingOptions,
route: `address/${address}/transfers`,
}
);
});

it("returns the transfers", async () => {
const result = await controller.getAddressTransfers(address, listFilterOptions, pagingOptions);
const result = await controller.getAddressTransfers(address, {}, listFilterOptions, pagingOptions);
expect(result).toBe(transfers);
});
});
Expand Down
17 changes: 12 additions & 5 deletions packages/api/src/address/address.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { AddressService } from "./address.service";
import { BlockService } from "../block/block.service";
import { TransactionService } from "../transaction/transaction.service";
import { BalanceService } from "../balance/balance.service";
import { AddressType, ContractDto, AccountDto, TokenAddressDto } from "./dtos";
import { AddressType, ContractDto, AccountDto, TokenAddressDto, FilterAddressTransfersOptionsDto } from "./dtos";
import { LogDto } from "../log/log.dto";
import { LogService } from "../log/log.service";
import { ParseAddressPipe, ADDRESS_REGEX_PATTERN } from "../common/pipes/parseAddress.pipe";
Expand Down Expand Up @@ -140,19 +140,26 @@ export class AddressController {
})
public async getAddressTransfers(
@Param("address", new ParseAddressPipe()) address: string,
@Query() filterAddressTransferOptions: FilterAddressTransfersOptionsDto,
@Query() listFilterOptions: ListFiltersDto,
@Query() pagingOptions: PagingOptionsWithMaxItemsLimitDto
): Promise<Pagination<TransferDto>> {
const filterTransactionsListOptions = buildDateFilter(listFilterOptions.fromDate, listFilterOptions.toDate);
const filterTransfersListOptions = buildDateFilter(listFilterOptions.fromDate, listFilterOptions.toDate);

return await this.transferService.findAll(
{
address,
isFeeOrRefund: false,
...filterTransactionsListOptions,
...filterTransfersListOptions,
...(filterAddressTransferOptions.type
? {
type: filterAddressTransferOptions.type,
}
: {
isFeeOrRefund: false,
}),
},
{
filterOptions: listFilterOptions,
filterOptions: { ...filterAddressTransferOptions, ...listFilterOptions },
...pagingOptions,
route: `${entityName}/${address}/transfers`,
}
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/address/dtos/filterAddressTransfersOptions.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsOptional } from "class-validator";
import { TransferType } from "../../transfer/transfer.entity";

export class FilterAddressTransfersOptionsDto {
@ApiPropertyOptional({
description: "Transfer type to filter transfers by",
example: TransferType.Transfer,
enum: TransferType,
})
@IsOptional()
public readonly type?: TransferType;
}
1 change: 1 addition & 0 deletions packages/api/src/address/dtos/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./account.dto";
export * from "./baseAddress.dto";
export * from "./contract.dto";
export * from "./filterAddressTransfersOptions.dto";
2 changes: 2 additions & 0 deletions packages/api/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IPaginationOptions as NestIPaginationOptions, IPaginationMeta } from "nestjs-typeorm-paginate";
import { TransferType } from "../transfer/transfer.entity";

interface IPaginationFilterOptions {
fromDate?: string;
Expand All @@ -7,6 +8,7 @@ interface IPaginationFilterOptions {
address?: string;
l1BatchNumber?: number;
minLiquidity?: number;
type?: TransferType;
}

export interface IPaginationOptions<CustomMetaType = IPaginationMeta> extends NestIPaginationOptions<CustomMetaType> {
Expand Down
13 changes: 0 additions & 13 deletions packages/api/src/config/docs/constants.testnet-goerli.json

This file was deleted.

1 change: 1 addition & 0 deletions packages/api/src/config/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe("config", () => {
feature1Enabled: true,
feature2Enabled: false,
},
gracefulShutdownTimeoutMs: 0,
});
});

Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default () => {
DATABASE_CONNECTION_IDLE_TIMEOUT_MS,
DATABASE_STATEMENT_TIMEOUT_MS,
CONTRACT_VERIFICATION_API_URL,
GRACEFUL_SHUTDOWN_TIMEOUT_MS,
} = process.env;

const MAX_NUMBER_OF_REPLICA = 100;
Expand Down Expand Up @@ -74,5 +75,6 @@ export default () => {
typeORM: getTypeOrmModuleOptions(),
contractVerificationApiUrl: CONTRACT_VERIFICATION_API_URL || "http://127.0.0.1:3070",
featureFlags,
gracefulShutdownTimeoutMs: parseInt(GRACEFUL_SHUTDOWN_TIMEOUT_MS, 10) || 0,
};
};
36 changes: 36 additions & 0 deletions packages/api/src/health/health.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { Test, TestingModule } from "@nestjs/testing";
import { HealthCheckService, TypeOrmHealthIndicator, HealthCheckResult } from "@nestjs/terminus";
import { mock } from "jest-mock-extended";
import { ConfigService } from "@nestjs/config";
import { setTimeout } from "node:timers/promises";
import { HealthController } from "./health.controller";

jest.mock("node:timers/promises", () => ({
setTimeout: jest.fn().mockResolvedValue(null),
}));

describe("HealthController", () => {
let healthCheckServiceMock: HealthCheckService;
let dbHealthCheckerMock: TypeOrmHealthIndicator;
let configServiceMock: ConfigService;
let healthController: HealthController;

beforeEach(async () => {
configServiceMock = mock<ConfigService>({
get: jest.fn().mockReturnValue(1),
});
healthCheckServiceMock = mock<HealthCheckService>({
check: jest.fn().mockImplementation((healthChecks) => {
for (const healthCheck of healthChecks) {
Expand All @@ -30,6 +40,10 @@ describe("HealthController", () => {
provide: TypeOrmHealthIndicator,
useValue: dbHealthCheckerMock,
},
{
provide: ConfigService,
useValue: configServiceMock,
},
],
}).compile();

Expand All @@ -50,4 +64,26 @@ describe("HealthController", () => {
expect(result).toBe(healthCheckResult);
});
});

describe("beforeApplicationShutdown", () => {
beforeEach(() => {
(setTimeout as jest.Mock).mockReset();
});

it("defined and returns void", async () => {
const result = await healthController.beforeApplicationShutdown();
expect(result).toBeUndefined();
});

it("awaits configured shutdown timeout", async () => {
await healthController.beforeApplicationShutdown("SIGTERM");
expect(setTimeout).toBeCalledTimes(1);
expect(setTimeout).toBeCalledWith(1);
});

it("does not await shutdown timeout if signal is not SIGTERM", async () => {
await healthController.beforeApplicationShutdown("SIGINT");
expect(setTimeout).toBeCalledTimes(0);
});
});
});
25 changes: 21 additions & 4 deletions packages/api/src/health/health.controller.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import { Controller, Get } from "@nestjs/common";
import { Logger, Controller, Get, BeforeApplicationShutdown } from "@nestjs/common";
import { HealthCheckService, TypeOrmHealthIndicator, HealthCheck, HealthCheckResult } from "@nestjs/terminus";
import { ApiExcludeController } from "@nestjs/swagger";
import { ConfigService } from "@nestjs/config";
import { setTimeout } from "node:timers/promises";

@ApiExcludeController()
@Controller(["health", "ready"])
export class HealthController {
export class HealthController implements BeforeApplicationShutdown {
private readonly logger: Logger;
private readonly gracefulShutdownTimeoutMs: number;

constructor(
private readonly healthCheckService: HealthCheckService,
private readonly dbHealthChecker: TypeOrmHealthIndicator
) {}
private readonly dbHealthChecker: TypeOrmHealthIndicator,
configService: ConfigService
) {
this.logger = new Logger(HealthController.name);
this.gracefulShutdownTimeoutMs = configService.get<number>("gracefulShutdownTimeoutMs");
}

@Get()
@HealthCheck()
public async check(): Promise<HealthCheckResult> {
return await this.healthCheckService.check([() => this.dbHealthChecker.pingCheck("database")]);
}

public async beforeApplicationShutdown(signal?: string): Promise<void> {
if (this.gracefulShutdownTimeoutMs && signal === "SIGTERM") {
this.logger.debug(`Awaiting ${this.gracefulShutdownTimeoutMs}ms before shutdown`);
await setTimeout(this.gracefulShutdownTimeoutMs);
this.logger.debug(`Timeout reached, shutting down now`);
}
}
}
8 changes: 5 additions & 3 deletions packages/api/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { format, transports, Logform } from "winston";
export const getLogger = (environment: string, logLevel: string): LoggerService => {
let defaultLogLevel = "debug";
const loggerFormatters: Logform.Format[] = [
format.timestamp({
format: "DD/MM/YYYY HH:mm:ss.SSS",
}),
environment === "production"
? format.timestamp()
: format.timestamp({
format: "DD/MM/YYYY HH:mm:ss.SSS",
}),
format.ms(),
utilities.format.nestLike("API", {}),
];
Expand Down
8 changes: 7 additions & 1 deletion packages/api/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import helmet from "helmet";
import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { ConfigService } from "@nestjs/config";
import { NestExpressApplication } from "@nestjs/platform-express";
import { configureApp } from "./configureApp";
import { getLogger } from "./logger";
import { AppModule } from "./app.module";
import { AppMetricsModule } from "./appMetrics.module";

const BODY_PARSER_SIZE_LIMIT = "10mb";

async function bootstrap() {
const logger = getLogger(process.env.NODE_ENV, process.env.LOG_LEVEL);

Expand All @@ -15,8 +18,9 @@ async function bootstrap() {
process.exit(1);
});

const app = await NestFactory.create(AppModule, {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger,
rawBody: true,
});
const configService = app.get(ConfigService);
const metricsApp = await NestFactory.create(AppMetricsModule);
Expand All @@ -32,6 +36,8 @@ async function bootstrap() {
SwaggerModule.setup("docs", app, document);
}

app.useBodyParser("json", { limit: BODY_PARSER_SIZE_LIMIT });
app.useBodyParser("urlencoded", { limit: BODY_PARSER_SIZE_LIMIT, extended: true });
app.enableCors();
app.use(helmet());
configureApp(app);
Expand Down
6 changes: 5 additions & 1 deletion packages/api/src/transfer/addressTransfer.entity.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Entity, Column, Index, ManyToOne, JoinColumn, PrimaryColumn } from "typeorm";
import { BaseEntity } from "../common/entities/base.entity";
import { Transfer } from "./transfer.entity";
import { Transfer, TransferType } from "./transfer.entity";
import { TokenType } from "../token/token.entity";
import { bigIntNumberTransformer } from "../common/transformers/bigIntNumber.transformer";
import { normalizeAddressTransformer } from "../common/transformers/normalizeAddress.transformer";

@Entity({ name: "addressTransfers" })
@Index(["address", "isFeeOrRefund", "timestamp", "logIndex"])
@Index(["address", "type", "timestamp", "logIndex"])
@Index(["address", "tokenType", "blockNumber", "logIndex"])
@Index(["address", "tokenAddress", "blockNumber", "logIndex"])
export class AddressTransfer extends BaseEntity {
Expand Down Expand Up @@ -34,6 +35,9 @@ export class AddressTransfer extends BaseEntity {
@Column({ type: "timestamp" })
public readonly timestamp: Date;

@Column({ type: "enum", enum: TransferType, default: TransferType.Transfer })
public readonly type: TransferType;

@Column({ type: "enum", enum: TokenType, default: TokenType.ETH })
public readonly tokenType: TokenType;

Expand Down
Loading

0 comments on commit fabf8ab

Please sign in to comment.