Skip to content

Commit

Permalink
feat: add transfer type filter for address transfers BFF API (#195)
Browse files Browse the repository at this point in the history
# What ❔

Add filter by transfer type to the address transfers endpoint

## Why ❔

It is useful to be able to get a list of transfers of certain type for
address. For example get list of withdrawals.
  • Loading branch information
Romsters authored Mar 6, 2024
1 parent 19b0099 commit 20f49d6
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 12 deletions.
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
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
3 changes: 2 additions & 1 deletion packages/api/src/transfer/transfer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Repository, FindOperator, MoreThanOrEqual, LessThanOrEqual } from "type
import { Pagination } from "nestjs-typeorm-paginate";
import { paginate } from "../common/utils";
import { IPaginationOptions, SortingOrder } from "../common/types";
import { Transfer } from "./transfer.entity";
import { Transfer, TransferType } from "./transfer.entity";
import { TokenType } from "../token/token.entity";
import { AddressTransfer } from "./addressTransfer.entity";
import { normalizeAddressTransformer } from "../common/transformers/normalizeAddress.transformer";
Expand All @@ -15,6 +15,7 @@ export interface FilterTransfersOptions {
address?: string;
timestamp?: FindOperator<Date>;
isFeeOrRefund?: boolean;
type?: TransferType;
}

export interface FilterTokenTransfersOptions {
Expand Down
17 changes: 17 additions & 0 deletions packages/api/test/address.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ describe("AddressController (e2e)", () => {
tokenAddress: transferSpec.tokenAddress,
blockNumber: transferSpec.blockNumber,
timestamp: transferSpec.timestamp,
type: transferSpec.type,
tokenType: transferSpec.tokenType,
isFeeOrRefund: transferSpec.isFeeOrRefund,
logIndex: transferSpec.logIndex,
Expand Down Expand Up @@ -1167,6 +1168,22 @@ describe("AddressController (e2e)", () => {
);
});

it("returns HTTP 200 and address transfers for the specified transfer type", () => {
return request(app.getHttpServer())
.get("/address/0x91d0a23f34e535e44df8ba84c53a0945cf0eeb67/transfers?type=withdrawal")
.expect(200)
.expect((res) =>
expect(res.body.meta).toMatchObject({
currentPage: 1,
itemCount: 5,
itemsPerPage: 10,
totalItems: 5,
totalPages: 1,
})
)
.expect((res) => expect(res.body.items[0].type).toBe(TransferType.Withdrawal));
});

it("returns HTTP 200 and address transfers for the specified paging configuration", () => {
return request(app.getHttpServer())
.get(
Expand Down
6 changes: 5 additions & 1 deletion packages/worker/src/entities/addressTransfer.entity.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Entity, Column, ManyToOne, JoinColumn, Index, PrimaryColumn } from "typeorm";
import { BaseEntity } from "./base.entity";
import { Block } from "./block.entity";
import { Transfer } from "./transfer.entity";
import { Transfer, TransferType } from "./transfer.entity";
import { TokenType } from "./token.entity";
import { hexTransformer } from "../transformers/hex.transformer";
import { bigIntNumberTransformer } from "../transformers/bigIntNumber.transformer";
import { TransferFields } from "../dataFetcher/types";

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

@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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AddAddressTransferType1709722093204 implements MigrationInterface {
name = "AddAddressTransferType1709722093204";

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."addressTransfers_type_enum" AS ENUM('deposit', 'transfer', 'withdrawal', 'fee', 'mint', 'refund')`
);
await queryRunner.query(
`ALTER TABLE "addressTransfers" ADD "type" "public"."addressTransfers_type_enum" NOT NULL DEFAULT 'transfer'`
);
await queryRunner.query(
`CREATE INDEX "IDX_aa5a147f1f6a4acde1a13de594" ON "addressTransfers" ("address", "type", "timestamp", "logIndex") `
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "public"."IDX_aa5a147f1f6a4acde1a13de594"`);
await queryRunner.query(`ALTER TABLE "addressTransfers" DROP COLUMN "type"`);
await queryRunner.query(`DROP TYPE "public"."addressTransfers_type_enum"`);
}
}

0 comments on commit 20f49d6

Please sign in to comment.