Skip to content

Commit

Permalink
feat: add api to get validated blocks by address
Browse files Browse the repository at this point in the history
  • Loading branch information
Romsters committed Oct 10, 2023
1 parent d0aa5a4 commit c2499e2
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 29 deletions.
39 changes: 39 additions & 0 deletions packages/api/src/api/account/account.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { mock } from "jest-mock-extended";
import { BadRequestException, Logger } from "@nestjs/common";
import { L2_ETH_TOKEN_ADDRESS } from "../../common/constants";
import { BlockService } from "../../block/block.service";
import { BlockDetail } from "../../block/blockDetail.entity";
import { TransactionService } from "../../transaction/transaction.service";
import { BalanceService } from "../../balance/balance.service";
import { TransactionStatus } from "../../transaction/entities/transaction.entity";
Expand Down Expand Up @@ -104,6 +105,7 @@ describe("AccountController", () => {
beforeEach(async () => {
blockServiceMock = mock<BlockService>({
getLastBlockNumber: jest.fn().mockResolvedValue(100),
findMany: jest.fn().mockResolvedValue([]),
});
transactionServiceMock = mock<TransactionService>({
findByAddress: jest.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -603,4 +605,41 @@ describe("AccountController", () => {
expect(parseAddressListPipeExceptionFactory()).toEqual(new BadRequestException("Error! Missing address"));
});
});

describe("getAccountMinedBlocks", () => {
it("returns not ok response when no blocks by miner found", async () => {
const response = await controller.getAccountMinedBlocks(address, {
page: 1,
offset: 10,
maxLimit: 100,
});
expect(response).toEqual({
status: ResponseStatus.NOTOK,
message: ResponseMessage.NO_TRANSACTIONS_FOUND,
result: [],
});
});

it("returns blocks list response when block by miner are found", async () => {
jest
.spyOn(blockServiceMock, "findMany")
.mockResolvedValue([{ number: 1, timestamp: new Date("2023-03-03") } as BlockDetail]);
const response = await controller.getAccountMinedBlocks(address, {
page: 1,
offset: 10,
maxLimit: 100,
});
expect(response).toEqual({
status: ResponseStatus.OK,
message: ResponseMessage.OK,
result: [
{
blockNumber: "1",
timeStamp: "1677801600",
blockReward: "0",
},
],
});
});
});
});
19 changes: 7 additions & 12 deletions packages/api/src/api/account/account.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,27 +233,22 @@ export class AccountController {
@Query("address", new ParseAddressPipe()) address: string,
@Query() pagingOptions: PagingOptionsWithMaxItemsLimitDto
): Promise<AccountMinedBlocksResponseDto> {
// Atm all the blocks are validated by zero address
if (address !== "0x0000000000000000000000000000000000000000") {
const blocks = await this.blockService.findMany({
miner: address,
...pagingOptions,
selectFields: ["number", "timestamp"],
});
if (!blocks.length) {
return {
status: ResponseStatus.NOTOK,
message: ResponseMessage.NO_TRANSACTIONS_FOUND,
result: [],
};
}
const blocks = await this.blockService.findAll(
{},
{
page: pagingOptions.page,
limit: pagingOptions.offset,
maxLimit: pagingOptions.maxLimit,
canUseNumberFilterAsOffset: true,
}
);
return {
status: ResponseStatus.OK,
message: ResponseMessage.OK,
result: blocks.items.map((block) => ({
result: blocks.map((block) => ({
blockNumber: block.number.toString(),
timeStamp: dateToTimestamp(block.timestamp).toString(),
blockReward: "0",
Expand Down
7 changes: 7 additions & 0 deletions packages/api/src/api/api.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ describe("ApiController", () => {
});
});

describe("getAccountMinedBlocks", () => {
it("returns null as it is defined only to appear in docs and cannot be called", async () => {
const result = await controller.getAccountMinedBlocks({ page: 1, offset: 10, maxLimit: 1000 });
expect(result).toBe(null);
});
});

describe("getBlockNumberByTimestamp", () => {
it("returns null as it is defined only to appear in docs and cannot be called", async () => {
const result = await controller.getBlockNumberByTimestamp();
Expand Down
8 changes: 4 additions & 4 deletions packages/api/src/api/log/log.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe("LogController", () => {
const address = "address";
beforeEach(async () => {
logServiceMock = mock<LogService>({
findLogs: jest.fn().mockResolvedValue([
findMany: jest.fn().mockResolvedValue([
{
logIndex: 1,
},
Expand Down Expand Up @@ -52,8 +52,8 @@ describe("LogController", () => {
0,
10
);
expect(logServiceMock.findLogs).toBeCalledTimes(1);
expect(logServiceMock.findLogs).toBeCalledWith({
expect(logServiceMock.findMany).toBeCalledTimes(1);
expect(logServiceMock.findMany).toBeCalledWith({
address,
fromBlock: 0,
toBlock: 10,
Expand Down Expand Up @@ -89,7 +89,7 @@ describe("LogController", () => {
});

it("returns not ok response and empty logs list when logs are not found", async () => {
(logServiceMock.findLogs as jest.Mock).mockResolvedValueOnce([]);
(logServiceMock.findMany as jest.Mock).mockResolvedValueOnce([]);
const response = await controller.getLogs(
address,
{
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/api/log/log.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class LogController {
@Query("fromBlock", new ParseLimitedIntPipe({ min: 0, isOptional: true })) fromBlock?: number,
@Query("toBlock", new ParseLimitedIntPipe({ min: 0, isOptional: true })) toBlock?: number
): Promise<LogsResponseDto> {
const logs = await this.logService.findLogs({
const logs = await this.logService.findMany({
address,
fromBlock,
toBlock,
Expand Down
79 changes: 78 additions & 1 deletion packages/api/src/block/block.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getRepositoryToken } from "@nestjs/typeorm";
import { Repository, SelectQueryBuilder, FindOptionsOrder } from "typeorm";
import { Pagination, IPaginationMeta } from "nestjs-typeorm-paginate";
import * as utils from "../common/utils";
import { BlockService } from "./block.service";
import { BlockService, FindManyOptions } from "./block.service";
import { Block } from "./block.entity";
import { BlockDetail } from "./blockDetail.entity";

Expand Down Expand Up @@ -299,4 +299,81 @@ describe("BlockService", () => {
expect(number).toBe(1000);
});
});

describe("findMany", () => {
let queryBuilderMock;
let filterOptions: FindManyOptions;

beforeEach(() => {
queryBuilderMock = mock<SelectQueryBuilder<BlockDetail>>({
getMany: jest.fn().mockResolvedValue([
{
number: 1,
timestamp: new Date("2023-03-03"),
},
]),
});
(blockDetailRepositoryMock.createQueryBuilder as jest.Mock).mockReturnValue(queryBuilderMock);

filterOptions = {
miner: "address",
};
});

it("creates query builder with proper params", async () => {
await service.findMany(filterOptions);
expect(blockDetailRepositoryMock.createQueryBuilder).toHaveBeenCalledTimes(1);
expect(blockDetailRepositoryMock.createQueryBuilder).toHaveBeenCalledWith("block");
});

it("selects specified fields", async () => {
await service.findMany({
...filterOptions,
selectFields: ["number", "timestamp"],
});
expect(queryBuilderMock.addSelect).toHaveBeenCalledTimes(1);
expect(queryBuilderMock.addSelect).toHaveBeenCalledWith(["number", "timestamp"]);
});

it("adds where condition for miner when specified", async () => {
await service.findMany(filterOptions);
expect(queryBuilderMock.where).toHaveBeenCalledTimes(1);
expect(queryBuilderMock.where).toHaveBeenCalledWith({
miner: "address",
});
});

it("does not add where condition for miner when not specified", async () => {
await service.findMany({});
expect(queryBuilderMock.where).not.toBeCalled();
});

it("sets offset and limit", async () => {
await service.findMany({
page: 10,
offset: 100,
});
expect(queryBuilderMock.offset).toHaveBeenCalledTimes(1);
expect(queryBuilderMock.offset).toHaveBeenCalledWith(900);
expect(queryBuilderMock.limit).toHaveBeenCalledTimes(1);
expect(queryBuilderMock.limit).toHaveBeenCalledWith(100);
});

it("orders by block number DESC", async () => {
await service.findMany({});
expect(queryBuilderMock.orderBy).toHaveBeenCalledTimes(1);
expect(queryBuilderMock.orderBy).toHaveBeenCalledWith("block.number", "DESC");
});

it("returns block list", async () => {
const result = await service.findMany({});
expect(queryBuilderMock.getMany).toHaveBeenCalledTimes(1);
expect(result).toEqual([
{
number: 1,
timestamp: new Date("2023-03-03"),
},
]);
});
});
});
21 changes: 21 additions & 0 deletions packages/api/src/block/block.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import { IPaginationOptions } from "../common/types";
import { Block } from "./block.entity";
import { BlockDetail } from "./blockDetail.entity";

export interface FindManyOptions {
miner?: string;
page?: number;
offset?: number;
selectFields?: (keyof BlockDetail)[];
}

@Injectable()
export class BlockService {
public constructor(
Expand Down Expand Up @@ -82,4 +89,18 @@ export class BlockService {

return await paginate<Block>(queryBuilder, paginationOptions, () => this.count(filterOptions));
}

public async findMany({ miner, page = 1, offset = 10, selectFields }: FindManyOptions): Promise<BlockDetail[]> {
const queryBuilder = this.blockDetailsRepository.createQueryBuilder("block");
queryBuilder.addSelect(selectFields);
if (miner) {
queryBuilder.where({
miner,
});
}
queryBuilder.offset((page - 1) * offset);
queryBuilder.limit(offset);
queryBuilder.orderBy("block.number", "DESC");
return await queryBuilder.getMany();
}
}
20 changes: 10 additions & 10 deletions packages/api/src/log/log.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe("LogService", () => {
});
});

describe("findLogs", () => {
describe("findMany", () => {
let queryBuilderMock;
let filterOptions: FilterLogsByAddressOptions;

Expand All @@ -127,26 +127,26 @@ describe("LogService", () => {
});

it("creates query builder with proper params", async () => {
await service.findLogs(filterOptions);
await service.findMany(filterOptions);
expect(repositoryMock.createQueryBuilder).toHaveBeenCalledTimes(1);
expect(repositoryMock.createQueryBuilder).toHaveBeenCalledWith("log");
});

it("joins transaction and transactionReceipt records to the logs", async () => {
await service.findLogs(filterOptions);
await service.findMany(filterOptions);
expect(queryBuilderMock.leftJoin).toBeCalledTimes(2);
expect(queryBuilderMock.leftJoin).toHaveBeenCalledWith("log.transaction", "transaction");
expect(queryBuilderMock.leftJoin).toHaveBeenCalledWith("transaction.transactionReceipt", "transactionReceipt");
});

it("selects only needed fields from joined records", async () => {
await service.findLogs(filterOptions);
await service.findMany(filterOptions);
expect(queryBuilderMock.addSelect).toBeCalledTimes(1);
expect(queryBuilderMock.addSelect).toHaveBeenCalledWith(["transaction.gasPrice", "transactionReceipt.gasUsed"]);
});

it("filters logs by address", async () => {
await service.findLogs(filterOptions);
await service.findMany(filterOptions);
expect(queryBuilderMock.where).toBeCalledTimes(1);
expect(queryBuilderMock.where).toHaveBeenCalledWith({
address: filterOptions.address,
Expand All @@ -155,7 +155,7 @@ describe("LogService", () => {

describe("when fromBlock filter is specified", () => {
it("adds blockNumber filter", async () => {
await service.findLogs({
await service.findMany({
...filterOptions,
fromBlock: 10,
});
Expand All @@ -168,7 +168,7 @@ describe("LogService", () => {

describe("when toBlock filter is specified", () => {
it("adds toBlock filter", async () => {
await service.findLogs({
await service.findMany({
...filterOptions,
toBlock: 10,
});
Expand All @@ -180,7 +180,7 @@ describe("LogService", () => {
});

it("sets offset and limit", async () => {
await service.findLogs({
await service.findMany({
...filterOptions,
page: 2,
offset: 100,
Expand All @@ -192,15 +192,15 @@ describe("LogService", () => {
});

it("sorts by blockNumber asc and logIndex asc", async () => {
await service.findLogs(filterOptions);
await service.findMany(filterOptions);
expect(queryBuilderMock.orderBy).toBeCalledTimes(1);
expect(queryBuilderMock.orderBy).toHaveBeenCalledWith("log.blockNumber", "ASC");
expect(queryBuilderMock.addOrderBy).toBeCalledTimes(1);
expect(queryBuilderMock.addOrderBy).toHaveBeenCalledWith("log.logIndex", "ASC");
});

it("executes query and returns transfers list", async () => {
const result = await service.findLogs(filterOptions);
const result = await service.findMany(filterOptions);
expect(result).toEqual([
{
logIndex: 1,
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/log/log.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class LogService {
return await paginate<Log>(queryBuilder, paginationOptions);
}

public async findLogs({
public async findMany({
address,
fromBlock,
toBlock,
Expand Down
21 changes: 21 additions & 0 deletions packages/api/test/account-api.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,25 @@ describe("Account API (e2e)", () => {
);
});
});

describe("/api?module=account&action=getminedblocks GET", () => {
it("returns HTTP 200 and list of mined blocks by address", () => {
return request(app.getHttpServer())
.get(`/api?module=account&action=getminedblocks&address=0x0000000000000000000000000000000000000000`)
.expect(200)
.expect((res) =>
expect(res.body).toStrictEqual({
status: "1",
message: "OK",
result: [
{
blockNumber: "1",
timeStamp: "1668091448",
blockReward: "0",
},
],
})
);
});
});
});
1 change: 1 addition & 0 deletions packages/worker/src/entities/block.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { BaseEntity } from "./base.entity";

@Entity({ name: "blocks" })
@Index(["timestamp", "number"])
@Index(["miner", "number"])
export class Block extends BaseEntity {
@PrimaryColumn({ type: "bigint", transformer: bigIntNumberTransformer })
public readonly number: number;
Expand Down
Loading

0 comments on commit c2499e2

Please sign in to comment.