diff --git a/packages/api/src/address/address.controller.spec.ts b/packages/api/src/address/address.controller.spec.ts index e215f6c378..35586a0f8a 100644 --- a/packages/api/src/address/address.controller.spec.ts +++ b/packages/api/src/address/address.controller.spec.ts @@ -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"), @@ -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( { @@ -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); }); }); diff --git a/packages/api/src/address/address.controller.ts b/packages/api/src/address/address.controller.ts index 70f18a078c..015cb9280c 100644 --- a/packages/api/src/address/address.controller.ts +++ b/packages/api/src/address/address.controller.ts @@ -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"; @@ -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> { - 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`, } diff --git a/packages/api/src/address/dtos/filterAddressTransfersOptions.dto.ts b/packages/api/src/address/dtos/filterAddressTransfersOptions.dto.ts new file mode 100644 index 0000000000..c74c33c53b --- /dev/null +++ b/packages/api/src/address/dtos/filterAddressTransfersOptions.dto.ts @@ -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; +} diff --git a/packages/api/src/address/dtos/index.ts b/packages/api/src/address/dtos/index.ts index 9d9d6027c2..cbd3966dd4 100644 --- a/packages/api/src/address/dtos/index.ts +++ b/packages/api/src/address/dtos/index.ts @@ -1,3 +1,4 @@ export * from "./account.dto"; export * from "./baseAddress.dto"; export * from "./contract.dto"; +export * from "./filterAddressTransfersOptions.dto"; diff --git a/packages/api/src/common/types.ts b/packages/api/src/common/types.ts index 743cd580ca..4977f91f51 100644 --- a/packages/api/src/common/types.ts +++ b/packages/api/src/common/types.ts @@ -1,4 +1,5 @@ import { IPaginationOptions as NestIPaginationOptions, IPaginationMeta } from "nestjs-typeorm-paginate"; +import { TransferType } from "../transfer/transfer.entity"; interface IPaginationFilterOptions { fromDate?: string; @@ -7,6 +8,7 @@ interface IPaginationFilterOptions { address?: string; l1BatchNumber?: number; minLiquidity?: number; + type?: TransferType; } export interface IPaginationOptions extends NestIPaginationOptions { diff --git a/packages/api/src/transfer/addressTransfer.entity.ts b/packages/api/src/transfer/addressTransfer.entity.ts index 65e9677020..fad65ae22f 100644 --- a/packages/api/src/transfer/addressTransfer.entity.ts +++ b/packages/api/src/transfer/addressTransfer.entity.ts @@ -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 { @@ -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; diff --git a/packages/api/src/transfer/transfer.service.ts b/packages/api/src/transfer/transfer.service.ts index 98ce2a2b6b..5491954d99 100644 --- a/packages/api/src/transfer/transfer.service.ts +++ b/packages/api/src/transfer/transfer.service.ts @@ -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"; @@ -15,6 +15,7 @@ export interface FilterTransfersOptions { address?: string; timestamp?: FindOperator; isFeeOrRefund?: boolean; + type?: TransferType; } export interface FilterTokenTransfersOptions { diff --git a/packages/api/test/address.e2e-spec.ts b/packages/api/test/address.e2e-spec.ts index 0c51412dcc..ecb56ccaa5 100644 --- a/packages/api/test/address.e2e-spec.ts +++ b/packages/api/test/address.e2e-spec.ts @@ -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, @@ -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( diff --git a/packages/worker/src/entities/addressTransfer.entity.ts b/packages/worker/src/entities/addressTransfer.entity.ts index edf6fe5177..dcc5306329 100644 --- a/packages/worker/src/entities/addressTransfer.entity.ts +++ b/packages/worker/src/entities/addressTransfer.entity.ts @@ -1,7 +1,7 @@ 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"; @@ -10,6 +10,7 @@ import { TransferFields } from "../transfer/interfaces/transfer.interface"; @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"]) @@ -42,6 +43,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; diff --git a/packages/worker/src/migrations/1709722093204-AddAddressTransferType.ts b/packages/worker/src/migrations/1709722093204-AddAddressTransferType.ts new file mode 100644 index 0000000000..bb5f79b330 --- /dev/null +++ b/packages/worker/src/migrations/1709722093204-AddAddressTransferType.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAddressTransferType1709722093204 implements MigrationInterface { + name = "AddAddressTransferType1709722093204"; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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"`); + } +}