From e50eff55ad6a2e18bb16adf30d89e09e07965540 Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 13:14:21 +0800 Subject: [PATCH 1/9] feat: add position api --- src/app.module.ts | 2 + src/entities/balanceOfLp.entity.ts | 3 +- src/positions/positions.controller.ts | 96 ++++++++++++++++++++++ src/positions/positions.dto.ts | 50 +++++++++++ src/repositories/balanceOfLp.repository.ts | 89 +++++++++++++++++++- 5 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 src/positions/positions.controller.ts create mode 100644 src/positions/positions.dto.ts diff --git a/src/app.module.ts b/src/app.module.ts index e00dd80..3ce479d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -48,6 +48,7 @@ import { RedistributeBalanceRepository } from "./repositories/redistributeBalanc import { SwethController } from "./sweth/sweth.controller"; import { SwethService } from "./sweth/sweth.service"; import { SwethApiService } from "./sweth/sweth.api.service"; +import { PositionController } from "./positions/positions.controller"; import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index"; @Module({ @@ -90,6 +91,7 @@ import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index"; NovaPagingController, CacheController, SwethController, + PositionController, ], providers: [ { diff --git a/src/entities/balanceOfLp.entity.ts b/src/entities/balanceOfLp.entity.ts index aac02fd..3f009bb 100644 --- a/src/entities/balanceOfLp.entity.ts +++ b/src/entities/balanceOfLp.entity.ts @@ -4,7 +4,8 @@ import { bigIntNumberTransformer } from "../transformers/bigIntNumber.transforme import { hexTransformer } from "../transformers/hex.transformer"; @Entity({ name: "balancesOfLp" }) -@Index(["blockNumber", "balance"]) +@Index(["blockNumber", "pairAddress", "tokenAddress"]) +@Index(["blockNumber", "pairAddress", "address", "tokenAddress"]) export class BalanceOfLp extends BaseEntity { @PrimaryColumn({ type: "bytea", transformer: hexTransformer }) public readonly address: string; diff --git a/src/positions/positions.controller.ts b/src/positions/positions.controller.ts new file mode 100644 index 0000000..76340e3 --- /dev/null +++ b/src/positions/positions.controller.ts @@ -0,0 +1,96 @@ +import { Controller, Get, Param, Query } from "@nestjs/common"; +import { + ApiBadRequestResponse, + ApiExcludeController, + ApiNotFoundResponse, + ApiParam, + ApiTags, +} from "@nestjs/swagger"; +import { BalanceOfLpRepository } from "src/repositories/balanceOfLp.repository"; +import { GetUserPositionsDto, UserPositionsResponseDto } from "./positions.dto"; + +@ApiTags("positions") +@ApiExcludeController(false) +@Controller("positions") +export class PositionController { + constructor(private balanceOfRepository: BalanceOfLpRepository) { } + + @Get(":projectName/tokens") + @ApiParam({ + name: "projectName", + required: true, + description: "Project name", + }) + @ApiBadRequestResponse({ + description: '{ "errno": 1, "errmsg": "Service exception" }', + }) + @ApiNotFoundResponse({ + description: '{ "errno": 1, "errmsg": "not found" }', + }) + async getUserPositionsByProjectAndTokens( + @Param("projectName") projectName: string, + @Query() queryParams: GetUserPositionsDto, + ): Promise { + const { tokenAddresses, page, limit, blockNumber } = queryParams; + const balances = + await this.balanceOfRepository.getUserPositionsByProjectAndTokens({ + projectName, + tokenAddresses, + page, + limit, + blockNumber, + }); + + return { + errmsg: "no error", + errno: 0, + data: balances, + }; + } + + @Get("agx/etherfi") + @ApiBadRequestResponse({ + description: '{ "errno": 1, "errmsg": "Service exception" }', + }) + @ApiNotFoundResponse({ + description: '{ "errno": 1, "errmsg": "not found" }', + }) + async getAgxUserEtherFiPositions( + @Query("blockNumber") blockNumber?: string, + ): Promise<{ Result: { address: string; effective_balance: string }[] }> { + const page = 1; + const limit = 100; + const tokenAddresses = [ + // "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", + // "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", + "0x8280a4e7D5B3B658ec4580d3Bc30f5e50454F169", + ].join(","); + + let result: Array<{ address: string; effective_balance: string }> = []; + let hasNextPage = true; + + while (hasNextPage) { + const balances = + await this.balanceOfRepository.getUserPositionsByProjectAndTokens({ + projectName: "agx", + tokenAddresses, + page, + limit, + blockNumber, + }); + if (result.length < limit) { + hasNextPage = false; + } + result = result.concat( + balances.map((i) => ({ + address: i.userAddress, + effective_balance: i.balance, + })), + ); + } + + return { + Result: result, + }; + } +} diff --git a/src/positions/positions.dto.ts b/src/positions/positions.dto.ts new file mode 100644 index 0000000..ea48e0e --- /dev/null +++ b/src/positions/positions.dto.ts @@ -0,0 +1,50 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { PagingOptionsDto } from "src/common/pagingOptionsDto.dto"; + +export class GetUserPositionsDto extends PagingOptionsDto { + @ApiProperty({ + required: false, + description: "Comma separated list of token addresses", + }) + tokenAddresses?: string; + + @ApiProperty({ + required: false, + description: "query positions at the blockNumber", + }) + blockNumber?: string; +} + +export class UserPositionsDto { + @ApiProperty() + userAddress: string; + + @ApiProperty() + tokenAddress: string; + + @ApiProperty() + balance: string; +} + +export class UserPositionsResponseDto { + @ApiProperty({ + type: Number, + description: "error code", + example: 0, + }) + public readonly errno: number; + + @ApiProperty({ + type: String, + description: "error message", + example: "no error", + }) + public readonly errmsg: string; + + @ApiProperty({ + type: UserPositionsDto, + description: "user points data", + nullable: true, + }) + public readonly data?: UserPositionsDto[]; +} diff --git a/src/repositories/balanceOfLp.repository.ts b/src/repositories/balanceOfLp.repository.ts index 14beb7f..644c748 100644 --- a/src/repositories/balanceOfLp.repository.ts +++ b/src/repositories/balanceOfLp.repository.ts @@ -2,10 +2,14 @@ import { Injectable } from "@nestjs/common"; import { UnitOfWork } from "../unitOfWork"; import { BaseRepository } from "./base.repository"; import { BalanceOfLp } from "../entities/balanceOfLp.entity"; - +import { ProjectRepository } from "./project.repository"; +import { GetUserPositionsDto } from "src/positions/positions.dto"; @Injectable() export class BalanceOfLpRepository extends BaseRepository { - public constructor(unitOfWork: UnitOfWork) { + public constructor( + unitOfWork: UnitOfWork, + readonly projectRepository: ProjectRepository, + ) { super(BalanceOfLp, unitOfWork); } @@ -71,4 +75,85 @@ export class BalanceOfLpRepository extends BaseRepository { return row; }); } + + async getUserPositionsByProjectAndTokens({ + projectName, + tokenAddresses, + page = 1, + limit = 10, + blockNumber, + }: GetUserPositionsDto & { projectName: string }) { + const tokenAddressList = tokenAddresses ? tokenAddresses.split(",") : []; + const pairAddressBuffers = + await this.projectRepository.getPairAddresses(projectName); + + const entityManager = this.unitOfWork.getTransactionManager(); + console.log(entityManager.connection.options); + + if (blockNumber === undefined) { + const latestBlock = await entityManager + .createQueryBuilder(BalanceOfLp, "b") + .select("MAX(b.blockNumber)", "max") + .getRawOne(); + + blockNumber = latestBlock.max; + if (!blockNumber) { + return []; + } + } else { + const closestBlock = await entityManager + .createQueryBuilder(BalanceOfLp, "b") + .select("b.blockNumber") + .where("b.blockNumber <= :blockNumber", { blockNumber }) + .andWhere("b.pairAddress IN (:...pairAddresses)", { + pairAddresses: pairAddressBuffers, + }) + .orderBy("b.blockNumber", "DESC") + .getOne(); + + if (!closestBlock) { + return []; + } + + blockNumber = closestBlock.blockNumber.toString(); + } + + let queryBuilder = entityManager + .createQueryBuilder(BalanceOfLp, "b") + .select([ + 'b.address AS "userAddress"', + 'b.tokenAddress AS "tokenAddress"', + 'SUM(CAST(b.balance AS numeric)) AS "balance"', + ]) + .where("b.blockNumber = :blockNumber", { blockNumber }) + .andWhere("b.pairAddress IN (:...pairAddresses)", { + pairAddresses: pairAddressBuffers, + }) + .groupBy("b.address, b.tokenAddress") + .skip((page - 1) * limit) + .take(limit); + + if (tokenAddressList.length > 0) { + const tokenAddressBuffers = tokenAddressList.map((addr) => + Buffer.from(addr.slice(2), "hex"), + ); + + queryBuilder = queryBuilder.andWhere( + "b.tokenAddress IN (:...tokenAddressList)", + { tokenAddressList: tokenAddressBuffers }, + ); + } + + const balances = await queryBuilder.getRawMany<{ + userAddress: Buffer; + tokenAddress: Buffer; + balance: string; + }>(); + + return balances.map((item) => ({ + ...item, + userAddress: "0x" + item.tokenAddress.toString("hex"), + tokenAddress: "0x" + item.tokenAddress.toString("hex"), + })); + } } From b1bb5dc3c765ccb34b77560dafc21257882084a8 Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 14:12:28 +0800 Subject: [PATCH 2/9] fix: etherfi api --- src/positions/positions.controller.ts | 14 +++++++------- src/repositories/balanceOfLp.repository.ts | 6 ++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/positions/positions.controller.ts b/src/positions/positions.controller.ts index 76340e3..5c0b68d 100644 --- a/src/positions/positions.controller.ts +++ b/src/positions/positions.controller.ts @@ -8,12 +8,13 @@ import { } from "@nestjs/swagger"; import { BalanceOfLpRepository } from "src/repositories/balanceOfLp.repository"; import { GetUserPositionsDto, UserPositionsResponseDto } from "./positions.dto"; +import { ethers } from "ethers"; @ApiTags("positions") @ApiExcludeController(false) @Controller("positions") export class PositionController { - constructor(private balanceOfRepository: BalanceOfLpRepository) { } + constructor(private balanceOfRepository: BalanceOfLpRepository) {} @Get(":projectName/tokens") @ApiParam({ @@ -57,16 +58,15 @@ export class PositionController { }) async getAgxUserEtherFiPositions( @Query("blockNumber") blockNumber?: string, - ): Promise<{ Result: { address: string; effective_balance: string }[] }> { + ): Promise<{ Result: { address: string; effective_balance: number }[] }> { const page = 1; const limit = 100; const tokenAddresses = [ - // "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", - // "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", - "0x8280a4e7D5B3B658ec4580d3Bc30f5e50454F169", + "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", + "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", ].join(","); - let result: Array<{ address: string; effective_balance: string }> = []; + let result: Array<{ address: string; effective_balance: number }> = []; let hasNextPage = true; while (hasNextPage) { @@ -84,7 +84,7 @@ export class PositionController { result = result.concat( balances.map((i) => ({ address: i.userAddress, - effective_balance: i.balance, + effective_balance: Number(ethers.formatUnits(i.balance)), })), ); } diff --git a/src/repositories/balanceOfLp.repository.ts b/src/repositories/balanceOfLp.repository.ts index 644c748..fca141e 100644 --- a/src/repositories/balanceOfLp.repository.ts +++ b/src/repositories/balanceOfLp.repository.ts @@ -88,12 +88,14 @@ export class BalanceOfLpRepository extends BaseRepository { await this.projectRepository.getPairAddresses(projectName); const entityManager = this.unitOfWork.getTransactionManager(); - console.log(entityManager.connection.options); if (blockNumber === undefined) { const latestBlock = await entityManager .createQueryBuilder(BalanceOfLp, "b") .select("MAX(b.blockNumber)", "max") + .where("b.pairAddress IN (:...pairAddresses)", { + pairAddresses: pairAddressBuffers, + }) .getRawOne(); blockNumber = latestBlock.max; @@ -152,7 +154,7 @@ export class BalanceOfLpRepository extends BaseRepository { return balances.map((item) => ({ ...item, - userAddress: "0x" + item.tokenAddress.toString("hex"), + userAddress: "0x" + item.userAddress.toString("hex"), tokenAddress: "0x" + item.tokenAddress.toString("hex"), })); } From 0bb0246af986cd4265825a0b2d73af16d5948c67 Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 14:18:56 +0800 Subject: [PATCH 3/9] fix: api doc --- src/positions/positions.dto.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/positions/positions.dto.ts b/src/positions/positions.dto.ts index ea48e0e..9c7e20d 100644 --- a/src/positions/positions.dto.ts +++ b/src/positions/positions.dto.ts @@ -16,13 +16,25 @@ export class GetUserPositionsDto extends PagingOptionsDto { } export class UserPositionsDto { - @ApiProperty() + @ApiProperty({ + type: String, + description: "user address", + example: "0xc48F99afe872c2541f530C6c87E3A6427e0C40d5", + }) userAddress: string; - @ApiProperty() + @ApiProperty({ + type: String, + description: "token address", + example: "0x8280a4e7D5B3B658ec4580d3Bc30f5e50454F169", + }) tokenAddress: string; - @ApiProperty() + @ApiProperty({ + type: String, + description: "token balance", + example: "10000000000000000", + }) balance: string; } @@ -43,7 +55,7 @@ export class UserPositionsResponseDto { @ApiProperty({ type: UserPositionsDto, - description: "user points data", + description: "user position list", nullable: true, }) public readonly data?: UserPositionsDto[]; From 9c551e3cb6c949b70dda327bc16a95c7dee38295 Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 14:43:41 +0800 Subject: [PATCH 4/9] fix: file structure --- src/app.module.ts | 6 ++- src/positions/positions.controller.ts | 54 ++++----------------------- src/positions/positions.service.ts | 52 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 48 deletions(-) create mode 100644 src/positions/positions.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 3ce479d..76a6409 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -48,8 +48,9 @@ import { RedistributeBalanceRepository } from "./repositories/redistributeBalanc import { SwethController } from "./sweth/sweth.controller"; import { SwethService } from "./sweth/sweth.service"; import { SwethApiService } from "./sweth/sweth.api.service"; -import { PositionController } from "./positions/positions.controller"; import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index"; +import { PositionsService } from "./positions/positions.service"; +import { PositionsController } from "./positions/positions.controller"; @Module({ imports: [ @@ -91,7 +92,7 @@ import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index"; NovaPagingController, CacheController, SwethController, - PositionController, + PositionsController, ], providers: [ { @@ -125,6 +126,7 @@ import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index"; RedistributeBalanceRepository, SwethService, SwethApiService, + PositionsService, ], }) export class AppModule {} diff --git a/src/positions/positions.controller.ts b/src/positions/positions.controller.ts index 5c0b68d..9081baf 100644 --- a/src/positions/positions.controller.ts +++ b/src/positions/positions.controller.ts @@ -6,15 +6,14 @@ import { ApiParam, ApiTags, } from "@nestjs/swagger"; -import { BalanceOfLpRepository } from "src/repositories/balanceOfLp.repository"; import { GetUserPositionsDto, UserPositionsResponseDto } from "./positions.dto"; -import { ethers } from "ethers"; +import { PositionsService } from "./positions.service"; @ApiTags("positions") @ApiExcludeController(false) @Controller("positions") -export class PositionController { - constructor(private balanceOfRepository: BalanceOfLpRepository) {} +export class PositionsController { + constructor(private positionsService: PositionsService) {} @Get(":projectName/tokens") @ApiParam({ @@ -34,7 +33,7 @@ export class PositionController { ): Promise { const { tokenAddresses, page, limit, blockNumber } = queryParams; const balances = - await this.balanceOfRepository.getUserPositionsByProjectAndTokens({ + await this.positionsService.getUserPositionsByProjectAndTokens({ projectName, tokenAddresses, page, @@ -50,47 +49,10 @@ export class PositionController { } @Get("agx/etherfi") - @ApiBadRequestResponse({ - description: '{ "errno": 1, "errmsg": "Service exception" }', - }) - @ApiNotFoundResponse({ - description: '{ "errno": 1, "errmsg": "not found" }', - }) - async getAgxUserEtherFiPositions( - @Query("blockNumber") blockNumber?: string, - ): Promise<{ Result: { address: string; effective_balance: number }[] }> { - const page = 1; - const limit = 100; - const tokenAddresses = [ - "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", - "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", - ].join(","); - - let result: Array<{ address: string; effective_balance: number }> = []; - let hasNextPage = true; - - while (hasNextPage) { - const balances = - await this.balanceOfRepository.getUserPositionsByProjectAndTokens({ - projectName: "agx", - tokenAddresses, - page, - limit, - blockNumber, - }); - if (result.length < limit) { - hasNextPage = false; - } - result = result.concat( - balances.map((i) => ({ - address: i.userAddress, - effective_balance: Number(ethers.formatUnits(i.balance)), - })), - ); - } + async getAgxUserEtherFiPositions(@Query("blockNumber") blockNumber?: string) { + const data = + await this.positionsService.getAgxEtherfiPositionsByBlock(blockNumber); - return { - Result: result, - }; + return data; } } diff --git a/src/positions/positions.service.ts b/src/positions/positions.service.ts new file mode 100644 index 0000000..ab4d515 --- /dev/null +++ b/src/positions/positions.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from "@nestjs/common"; +import { BalanceOfLpRepository } from "src/repositories/balanceOfLp.repository"; +import { GetUserPositionsDto } from "./positions.dto"; +import { ethers } from "ethers"; + +@Injectable() +export class PositionsService { + constructor(private balanceOfRepository: BalanceOfLpRepository) {} + + async getUserPositionsByProjectAndTokens( + params: GetUserPositionsDto & { projectName: string }, + ) { + const data = + await this.balanceOfRepository.getUserPositionsByProjectAndTokens(params); + return data; + } + + async getAgxEtherfiPositionsByBlock(blockNumber: string) { + const page = 1; + const limit = 100; + const tokenAddresses = [ + "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", + "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", + ].join(","); + + let result: Array<{ address: string; effective_balance: number }> = []; + let hasNextPage = true; + + while (hasNextPage) { + const balances = await this.getUserPositionsByProjectAndTokens({ + projectName: "agx", + tokenAddresses, + page, + limit, + blockNumber, + }); + if (result.length < limit) { + hasNextPage = false; + } + result = result.concat( + balances.map((i) => ({ + address: i.userAddress, + effective_balance: Number(ethers.formatUnits(i.balance)), + })), + ); + } + + return { + Result: result, + }; + } +} From 5bbef1380d622e10a10b29fb1cd9bdec60bc04ab Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 15:02:22 +0800 Subject: [PATCH 5/9] feat: support query by userAddress --- src/positions/positions.controller.ts | 6 +----- src/positions/positions.dto.ts | 6 ++++++ src/repositories/balanceOfLp.repository.ts | 9 +++++++++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/positions/positions.controller.ts b/src/positions/positions.controller.ts index 9081baf..d388c79 100644 --- a/src/positions/positions.controller.ts +++ b/src/positions/positions.controller.ts @@ -31,14 +31,10 @@ export class PositionsController { @Param("projectName") projectName: string, @Query() queryParams: GetUserPositionsDto, ): Promise { - const { tokenAddresses, page, limit, blockNumber } = queryParams; const balances = await this.positionsService.getUserPositionsByProjectAndTokens({ projectName, - tokenAddresses, - page, - limit, - blockNumber, + ...queryParams, }); return { diff --git a/src/positions/positions.dto.ts b/src/positions/positions.dto.ts index 9c7e20d..4f13534 100644 --- a/src/positions/positions.dto.ts +++ b/src/positions/positions.dto.ts @@ -13,6 +13,12 @@ export class GetUserPositionsDto extends PagingOptionsDto { description: "query positions at the blockNumber", }) blockNumber?: string; + + @ApiProperty({ + required: false, + description: "user Address", + }) + userAddress?: string; } export class UserPositionsDto { diff --git a/src/repositories/balanceOfLp.repository.ts b/src/repositories/balanceOfLp.repository.ts index fca141e..38d8bc9 100644 --- a/src/repositories/balanceOfLp.repository.ts +++ b/src/repositories/balanceOfLp.repository.ts @@ -82,6 +82,7 @@ export class BalanceOfLpRepository extends BaseRepository { page = 1, limit = 10, blockNumber, + userAddress, }: GetUserPositionsDto & { projectName: string }) { const tokenAddressList = tokenAddresses ? tokenAddresses.split(",") : []; const pairAddressBuffers = @@ -146,6 +147,14 @@ export class BalanceOfLpRepository extends BaseRepository { ); } + if (userAddress) { + const userAddressBuffer = Buffer.from(userAddress.slice(2), "hex"); + + queryBuilder = queryBuilder.andWhere("b.address = :userAddress", { + userAddress: userAddressBuffer, + }); + } + const balances = await queryBuilder.getRawMany<{ userAddress: Buffer; tokenAddress: Buffer; From 3b5c47eb36da7d8e06929cb23c2bfd484704cfbb Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 22:40:58 +0800 Subject: [PATCH 6/9] fix: review issue --- src/common/pagingOptionsDto.dto.ts | 4 +- src/positions/positions.controller.ts | 11 ++- src/positions/positions.dto.ts | 17 ++++- src/positions/positions.service.ts | 41 ++++++----- src/repositories/balanceOfLp.repository.ts | 79 ++++++++++++++-------- 5 files changed, 98 insertions(+), 54 deletions(-) diff --git a/src/common/pagingOptionsDto.dto.ts b/src/common/pagingOptionsDto.dto.ts index b1db88d..b84fcbf 100644 --- a/src/common/pagingOptionsDto.dto.ts +++ b/src/common/pagingOptionsDto.dto.ts @@ -14,7 +14,7 @@ export class PagingOptionsDto { @IsInt() @Min(1) @IsOptional() - public readonly page: number = 1; + public readonly page?: number = 1; @ApiPropertyOptional({ type: "integer", @@ -29,5 +29,5 @@ export class PagingOptionsDto { @Min(1) @Max(500) @IsOptional() - public readonly limit: number = 100; + public readonly limit?: number = 100; } diff --git a/src/positions/positions.controller.ts b/src/positions/positions.controller.ts index d388c79..2cce6c6 100644 --- a/src/positions/positions.controller.ts +++ b/src/positions/positions.controller.ts @@ -6,7 +6,11 @@ import { ApiParam, ApiTags, } from "@nestjs/swagger"; -import { GetUserPositionsDto, UserPositionsResponseDto } from "./positions.dto"; +import { + GetAGXPositionDto, + GetUserPositionsDto, + UserPositionsResponseDto, +} from "./positions.dto"; import { PositionsService } from "./positions.service"; @ApiTags("positions") @@ -15,7 +19,7 @@ import { PositionsService } from "./positions.service"; export class PositionsController { constructor(private positionsService: PositionsService) {} - @Get(":projectName/tokens") + @Get(":projectName") @ApiParam({ name: "projectName", required: true, @@ -45,7 +49,8 @@ export class PositionsController { } @Get("agx/etherfi") - async getAgxUserEtherFiPositions(@Query("blockNumber") blockNumber?: string) { + async getAgxUserEtherFiPositions(@Query() queryString: GetAGXPositionDto) { + const { blockNumber } = queryString; const data = await this.positionsService.getAgxEtherfiPositionsByBlock(blockNumber); diff --git a/src/positions/positions.dto.ts b/src/positions/positions.dto.ts index 4f13534..31e4e0a 100644 --- a/src/positions/positions.dto.ts +++ b/src/positions/positions.dto.ts @@ -1,23 +1,38 @@ import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsInt, IsOptional, Min } from "class-validator"; import { PagingOptionsDto } from "src/common/pagingOptionsDto.dto"; +export class GetAGXPositionDto { + @IsInt() + @Min(1) + @Type(() => Number) + blockNumber: number; +} + export class GetUserPositionsDto extends PagingOptionsDto { @ApiProperty({ required: false, description: "Comma separated list of token addresses", }) + @IsOptional() tokenAddresses?: string; @ApiProperty({ required: false, description: "query positions at the blockNumber", }) - blockNumber?: string; + @IsInt() + @Min(1) + @IsOptional() + @Type(() => Number) + blockNumber?: number; @ApiProperty({ required: false, description: "user Address", }) + @IsOptional() userAddress?: string; } diff --git a/src/positions/positions.service.ts b/src/positions/positions.service.ts index ab4d515..e59194d 100644 --- a/src/positions/positions.service.ts +++ b/src/positions/positions.service.ts @@ -10,40 +10,39 @@ export class PositionsService { async getUserPositionsByProjectAndTokens( params: GetUserPositionsDto & { projectName: string }, ) { + const { tokenAddresses, limit = 100, page = 1 } = params; + const formattedParams = { + ...params, + tokenAddresses: tokenAddresses?.split(",") ?? [], + limit, + page, + }; const data = - await this.balanceOfRepository.getUserPositionsByProjectAndTokens(params); + await this.balanceOfRepository.getUserPositionsByProjectAndTokens( + formattedParams, + ); return data; } - async getAgxEtherfiPositionsByBlock(blockNumber: string) { - const page = 1; - const limit = 100; + async getAgxEtherfiPositionsByBlock(blockNumber: number) { const tokenAddresses = [ "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", - ].join(","); + ]; let result: Array<{ address: string; effective_balance: number }> = []; - let hasNextPage = true; - while (hasNextPage) { - const balances = await this.getUserPositionsByProjectAndTokens({ + const balances = + await this.balanceOfRepository.getUserPositionsByProjectAndTokens({ projectName: "agx", tokenAddresses, - page, - limit, - blockNumber, + blockNumber: blockNumber, }); - if (result.length < limit) { - hasNextPage = false; - } - result = result.concat( - balances.map((i) => ({ - address: i.userAddress, - effective_balance: Number(ethers.formatUnits(i.balance)), - })), - ); - } + + result = balances.map((i) => ({ + address: i.userAddress, + effective_balance: Number(ethers.formatUnits(i.balance)), + })); return { Result: result, diff --git a/src/repositories/balanceOfLp.repository.ts b/src/repositories/balanceOfLp.repository.ts index 38d8bc9..c9e0a73 100644 --- a/src/repositories/balanceOfLp.repository.ts +++ b/src/repositories/balanceOfLp.repository.ts @@ -76,20 +76,12 @@ export class BalanceOfLpRepository extends BaseRepository { }); } - async getUserPositionsByProjectAndTokens({ - projectName, - tokenAddresses, - page = 1, - limit = 10, - blockNumber, - userAddress, - }: GetUserPositionsDto & { projectName: string }) { - const tokenAddressList = tokenAddresses ? tokenAddresses.split(",") : []; - const pairAddressBuffers = - await this.projectRepository.getPairAddresses(projectName); - + async getClosestBlockNumber( + blockNumber: number, + pairAddressBuffers: Buffer[], + ) { + let result: number | undefined; const entityManager = this.unitOfWork.getTransactionManager(); - if (blockNumber === undefined) { const latestBlock = await entityManager .createQueryBuilder(BalanceOfLp, "b") @@ -99,10 +91,7 @@ export class BalanceOfLpRepository extends BaseRepository { }) .getRawOne(); - blockNumber = latestBlock.max; - if (!blockNumber) { - return []; - } + result = latestBlock?.max; } else { const closestBlock = await entityManager .createQueryBuilder(BalanceOfLp, "b") @@ -114,11 +103,39 @@ export class BalanceOfLpRepository extends BaseRepository { .orderBy("b.blockNumber", "DESC") .getOne(); - if (!closestBlock) { - return []; - } + result = Number(closestBlock?.blockNumber); + } + return result; + } + + async getUserPositionsByProjectAndTokens({ + projectName, + tokenAddresses, + page = 1, + limit = 10, + blockNumber, + userAddress, + }: Omit & { + projectName: string; + tokenAddresses: string[]; + }): Promise< + { + userAddress: string; + tokenAddress: string; + balance: string; + }[] + > { + const pairAddressBuffers = + await this.projectRepository.getPairAddresses(projectName); + + const entityManager = this.unitOfWork.getTransactionManager(); - blockNumber = closestBlock.blockNumber.toString(); + const closestBlockNumber = await this.getClosestBlockNumber( + blockNumber, + pairAddressBuffers, + ); + if (!closestBlockNumber) { + return []; } let queryBuilder = entityManager @@ -128,16 +145,16 @@ export class BalanceOfLpRepository extends BaseRepository { 'b.tokenAddress AS "tokenAddress"', 'SUM(CAST(b.balance AS numeric)) AS "balance"', ]) - .where("b.blockNumber = :blockNumber", { blockNumber }) + .where("b.blockNumber = :blockNumber", { + blockNumber: closestBlockNumber, + }) .andWhere("b.pairAddress IN (:...pairAddresses)", { pairAddresses: pairAddressBuffers, }) - .groupBy("b.address, b.tokenAddress") - .skip((page - 1) * limit) - .take(limit); + .groupBy("b.address, b.tokenAddress"); - if (tokenAddressList.length > 0) { - const tokenAddressBuffers = tokenAddressList.map((addr) => + if (tokenAddresses.length > 0) { + const tokenAddressBuffers = tokenAddresses.map((addr) => Buffer.from(addr.slice(2), "hex"), ); @@ -147,6 +164,14 @@ export class BalanceOfLpRepository extends BaseRepository { ); } + if (limit) { + if (page) { + queryBuilder = queryBuilder.skip((page - 1) * limit).take(limit); + } else { + queryBuilder = queryBuilder.take(limit); + } + } + if (userAddress) { const userAddressBuffer = Buffer.from(userAddress.slice(2), "hex"); From 20db217ffaaf9fcce0eaf141050ff4723d26c9d7 Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 22:50:24 +0800 Subject: [PATCH 7/9] fix: update entity --- src/entities/balanceOfLp.entity.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/entities/balanceOfLp.entity.ts b/src/entities/balanceOfLp.entity.ts index 3f009bb..b5667ca 100644 --- a/src/entities/balanceOfLp.entity.ts +++ b/src/entities/balanceOfLp.entity.ts @@ -4,8 +4,7 @@ import { bigIntNumberTransformer } from "../transformers/bigIntNumber.transforme import { hexTransformer } from "../transformers/hex.transformer"; @Entity({ name: "balancesOfLp" }) -@Index(["blockNumber", "pairAddress", "tokenAddress"]) -@Index(["blockNumber", "pairAddress", "address", "tokenAddress"]) +@Index(["blockNumber", "pairAddress", "tokenAddress", "address"]) export class BalanceOfLp extends BaseEntity { @PrimaryColumn({ type: "bytea", transformer: hexTransformer }) public readonly address: string; From bfd84cf0d1bdea7d4b04554cc310eb87ecc2a94b Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Thu, 13 Jun 2024 22:57:17 +0800 Subject: [PATCH 8/9] fix: blockNumber type --- src/repositories/balanceOfLp.repository.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/repositories/balanceOfLp.repository.ts b/src/repositories/balanceOfLp.repository.ts index c9e0a73..6d779c9 100644 --- a/src/repositories/balanceOfLp.repository.ts +++ b/src/repositories/balanceOfLp.repository.ts @@ -91,7 +91,7 @@ export class BalanceOfLpRepository extends BaseRepository { }) .getRawOne(); - result = latestBlock?.max; + result = latestBlock ? Number(latestBlock.max) : undefined; } else { const closestBlock = await entityManager .createQueryBuilder(BalanceOfLp, "b") @@ -103,7 +103,7 @@ export class BalanceOfLpRepository extends BaseRepository { .orderBy("b.blockNumber", "DESC") .getOne(); - result = Number(closestBlock?.blockNumber); + result = closestBlock?.blockNumber; } return result; } @@ -134,6 +134,7 @@ export class BalanceOfLpRepository extends BaseRepository { blockNumber, pairAddressBuffers, ); + if (!closestBlockNumber) { return []; } From 98a810b31447d5c0e9dd0bcf37f4b50b2f626d55 Mon Sep 17 00:00:00 2001 From: xsteadybcgo Date: Fri, 14 Jun 2024 10:42:31 +0800 Subject: [PATCH 9/9] fix: add pagination metadata on positions endpoint --- src/common/pagination.util.ts | 18 ++++ src/positions/positions.controller.ts | 9 +- src/positions/positions.dto.ts | 8 ++ src/positions/positions.service.ts | 30 ++++--- src/repositories/balanceOfLp.repository.ts | 97 ++++++++++++++++------ 5 files changed, 117 insertions(+), 45 deletions(-) diff --git a/src/common/pagination.util.ts b/src/common/pagination.util.ts index 3ea2235..77a8cf3 100644 --- a/src/common/pagination.util.ts +++ b/src/common/pagination.util.ts @@ -18,4 +18,22 @@ export class PaginationUtil { items: paginatedItems, } as PagingDto; } + + static genPaginateMetaByTotalCount( + total: number, + page: number, + limit: number, + ): PagingMetaDto { + const itemCount = + page * limit > total ? Math.max(total - (page - 1) * limit, 0) : limit; + const pagingMeta = { + currentPage: Number(page), + itemCount: itemCount, + itemsPerPage: Number(limit), + totalItems: total, + totalPages: Math.ceil(total / limit), + }; + + return pagingMeta; + } } diff --git a/src/positions/positions.controller.ts b/src/positions/positions.controller.ts index 2cce6c6..17f44a4 100644 --- a/src/positions/positions.controller.ts +++ b/src/positions/positions.controller.ts @@ -31,12 +31,12 @@ export class PositionsController { @ApiNotFoundResponse({ description: '{ "errno": 1, "errmsg": "not found" }', }) - async getUserPositionsByProjectAndTokens( + async getProjectPositions( @Param("projectName") projectName: string, @Query() queryParams: GetUserPositionsDto, ): Promise { - const balances = - await this.positionsService.getUserPositionsByProjectAndTokens({ + const { data, meta } = + await this.positionsService.getPositionsByProjectAndAddress({ projectName, ...queryParams, }); @@ -44,7 +44,8 @@ export class PositionsController { return { errmsg: "no error", errno: 0, - data: balances, + meta: meta, + data: data, }; } diff --git a/src/positions/positions.dto.ts b/src/positions/positions.dto.ts index 31e4e0a..4603ca8 100644 --- a/src/positions/positions.dto.ts +++ b/src/positions/positions.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { IsInt, IsOptional, Min } from "class-validator"; +import { PagingMetaDto } from "src/common/paging.dto"; import { PagingOptionsDto } from "src/common/pagingOptionsDto.dto"; export class GetAGXPositionDto { @@ -74,6 +75,13 @@ export class UserPositionsResponseDto { }) public readonly errmsg: string; + @ApiProperty({ + type: PagingMetaDto, + description: "page meta", + example: 0, + }) + public readonly meta?: PagingMetaDto; + @ApiProperty({ type: UserPositionsDto, description: "user position list", diff --git a/src/positions/positions.service.ts b/src/positions/positions.service.ts index e59194d..46b7eca 100644 --- a/src/positions/positions.service.ts +++ b/src/positions/positions.service.ts @@ -2,12 +2,13 @@ import { Injectable } from "@nestjs/common"; import { BalanceOfLpRepository } from "src/repositories/balanceOfLp.repository"; import { GetUserPositionsDto } from "./positions.dto"; import { ethers } from "ethers"; +import { PaginationUtil } from "src/common/pagination.util"; @Injectable() export class PositionsService { constructor(private balanceOfRepository: BalanceOfLpRepository) {} - async getUserPositionsByProjectAndTokens( + async getPositionsByProjectAndAddress( params: GetUserPositionsDto & { projectName: string }, ) { const { tokenAddresses, limit = 100, page = 1 } = params; @@ -17,29 +18,26 @@ export class PositionsService { limit, page, }; - const data = - await this.balanceOfRepository.getUserPositionsByProjectAndTokens( + const { list, totalCount } = + await this.balanceOfRepository.getProjectPositionsByAddress( formattedParams, ); - return data; + const meta = PaginationUtil.genPaginateMetaByTotalCount( + totalCount, + page, + limit, + ); + return { data: list, meta: meta }; } async getAgxEtherfiPositionsByBlock(blockNumber: number) { - const tokenAddresses = [ - "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", - "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", - ]; - let result: Array<{ address: string; effective_balance: number }> = []; - const balances = - await this.balanceOfRepository.getUserPositionsByProjectAndTokens({ - projectName: "agx", - tokenAddresses, - blockNumber: blockNumber, - }); + const data = await this.balanceOfRepository.getAgxEtherfiPositions({ + blockNumber, + }); - result = balances.map((i) => ({ + result = data.map((i) => ({ address: i.userAddress, effective_balance: Number(ethers.formatUnits(i.balance)), })); diff --git a/src/repositories/balanceOfLp.repository.ts b/src/repositories/balanceOfLp.repository.ts index 6d779c9..f87a0f3 100644 --- a/src/repositories/balanceOfLp.repository.ts +++ b/src/repositories/balanceOfLp.repository.ts @@ -3,7 +3,17 @@ import { UnitOfWork } from "../unitOfWork"; import { BaseRepository } from "./base.repository"; import { BalanceOfLp } from "../entities/balanceOfLp.entity"; import { ProjectRepository } from "./project.repository"; -import { GetUserPositionsDto } from "src/positions/positions.dto"; +import { + GetAGXPositionDto, + GetUserPositionsDto, +} from "src/positions/positions.dto"; + +export interface Position { + userAddress: string; + tokenAddress: string; + balance: string; +} + @Injectable() export class BalanceOfLpRepository extends BaseRepository { public constructor( @@ -76,7 +86,7 @@ export class BalanceOfLpRepository extends BaseRepository { }); } - async getClosestBlockNumber( + private async getClosestBlockNumber( blockNumber: number, pairAddressBuffers: Buffer[], ) { @@ -91,7 +101,7 @@ export class BalanceOfLpRepository extends BaseRepository { }) .getRawOne(); - result = latestBlock ? Number(latestBlock.max) : undefined; + result = latestBlock ? Number(latestBlock.max) : 0; } else { const closestBlock = await entityManager .createQueryBuilder(BalanceOfLp, "b") @@ -103,28 +113,16 @@ export class BalanceOfLpRepository extends BaseRepository { .orderBy("b.blockNumber", "DESC") .getOne(); - result = closestBlock?.blockNumber; + result = closestBlock?.blockNumber ?? 0; } return result; } - async getUserPositionsByProjectAndTokens({ - projectName, - tokenAddresses, - page = 1, - limit = 10, - blockNumber, - userAddress, - }: Omit & { - projectName: string; - tokenAddresses: string[]; - }): Promise< - { - userAddress: string; - tokenAddress: string; - balance: string; - }[] - > { + private async genPositionsQueryBuilder( + projectName: string, + blockNumber: number, + tokenAddresses: string[], + ) { const pairAddressBuffers = await this.projectRepository.getPairAddresses(projectName); @@ -135,10 +133,6 @@ export class BalanceOfLpRepository extends BaseRepository { pairAddressBuffers, ); - if (!closestBlockNumber) { - return []; - } - let queryBuilder = entityManager .createQueryBuilder(BalanceOfLp, "b") .select([ @@ -164,6 +158,30 @@ export class BalanceOfLpRepository extends BaseRepository { { tokenAddressList: tokenAddressBuffers }, ); } + return queryBuilder; + } + + public async getProjectPositionsByAddress({ + projectName, + tokenAddresses, + page, + limit, + blockNumber, + userAddress, + }: Omit & { + projectName: string; + tokenAddresses: string[]; + }): Promise<{ + totalCount: number; + list: Position[]; + }> { + let queryBuilder = await this.genPositionsQueryBuilder( + projectName, + blockNumber, + tokenAddresses, + ); + + const total = await queryBuilder.getCount(); if (limit) { if (page) { @@ -187,6 +205,35 @@ export class BalanceOfLpRepository extends BaseRepository { balance: string; }>(); + return { + totalCount: total, + list: balances.map((item) => ({ + ...item, + userAddress: "0x" + item.userAddress.toString("hex"), + tokenAddress: "0x" + item.tokenAddress.toString("hex"), + })), + }; + } + + public async getAgxEtherfiPositions({ + blockNumber, + }: GetAGXPositionDto): Promise { + const tokenAddresses = [ + "0x35D5f1b41319e0ebb5a10e55C3BD23f121072da8", + "0xE227155217513f1ACaA2849A872ab933cF2d6a9A", + ]; + const queryBuilder = await this.genPositionsQueryBuilder( + "agx", + blockNumber, + tokenAddresses, + ); + + const balances = await queryBuilder.getRawMany<{ + userAddress: Buffer; + tokenAddress: Buffer; + balance: string; + }>(); + return balances.map((item) => ({ ...item, userAddress: "0x" + item.userAddress.toString("hex"),