Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add position api #63

Merged
merged 9 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import { SwethController } from "./sweth/sweth.controller";
import { SwethService } from "./sweth/sweth.service";
import { SwethApiService } from "./sweth/sweth.api.service";
import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index";
import { PositionsService } from "./positions/positions.service";
import { PositionsController } from "./positions/positions.controller";

@Module({
imports: [
Expand Down Expand Up @@ -90,6 +92,7 @@ import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index";
NovaPagingController,
CacheController,
SwethController,
PositionsController,
],
providers: [
{
Expand Down Expand Up @@ -123,6 +126,7 @@ import { User, UserHolding, UserStaked, UserWithdraw } from "./entities/index";
RedistributeBalanceRepository,
SwethService,
SwethApiService,
PositionsService,
],
})
export class AppModule {}
3 changes: 2 additions & 1 deletion src/entities/balanceOfLp.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
54 changes: 54 additions & 0 deletions src/positions/positions.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Controller, Get, Param, Query } from "@nestjs/common";
import {
ApiBadRequestResponse,
ApiExcludeController,
ApiNotFoundResponse,
ApiParam,
ApiTags,
} from "@nestjs/swagger";
import { GetUserPositionsDto, UserPositionsResponseDto } from "./positions.dto";
import { PositionsService } from "./positions.service";

@ApiTags("positions")
@ApiExcludeController(false)
@Controller("positions")
export class PositionsController {
constructor(private positionsService: PositionsService) {}

@Get(":projectName/tokens")
xsteadybcgo marked this conversation as resolved.
Show resolved Hide resolved
@ApiParam({
name: "projectName",
required: true,
description: "Project name",
})
@ApiBadRequestResponse({
description: '{ "errno": 1, "errmsg": "Service exception" }',
})
@ApiNotFoundResponse({
description: '{ "errno": 1, "errmsg": "not found" }',
})
async getUserPositionsByProjectAndTokens(
xsteadybcgo marked this conversation as resolved.
Show resolved Hide resolved
@Param("projectName") projectName: string,
@Query() queryParams: GetUserPositionsDto,
): Promise<UserPositionsResponseDto> {
const balances =
await this.positionsService.getUserPositionsByProjectAndTokens({
projectName,
...queryParams,
});

return {
errmsg: "no error",
errno: 0,
data: balances,
};
}

@Get("agx/etherfi")
async getAgxUserEtherFiPositions(@Query("blockNumber") blockNumber?: string) {
const data =
await this.positionsService.getAgxEtherfiPositionsByBlock(blockNumber);

return data;
}
}
68 changes: 68 additions & 0 deletions src/positions/positions.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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;

@ApiProperty({
required: false,
description: "user Address",
})
userAddress?: string;
}

export class UserPositionsDto {
@ApiProperty({
type: String,
description: "user address",
example: "0xc48F99afe872c2541f530C6c87E3A6427e0C40d5",
})
userAddress: string;

@ApiProperty({
type: String,
description: "token address",
example: "0x8280a4e7D5B3B658ec4580d3Bc30f5e50454F169",
})
tokenAddress: string;

@ApiProperty({
type: String,
description: "token balance",
example: "10000000000000000",
})
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 position list",
nullable: true,
})
public readonly data?: UserPositionsDto[];
}
52 changes: 52 additions & 0 deletions src/positions/positions.service.ts
Original file line number Diff line number Diff line change
@@ -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,
};
xsteadybcgo marked this conversation as resolved.
Show resolved Hide resolved
}
}
100 changes: 98 additions & 2 deletions src/repositories/balanceOfLp.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BalanceOfLp> {
public constructor(unitOfWork: UnitOfWork) {
public constructor(
unitOfWork: UnitOfWork,
readonly projectRepository: ProjectRepository,
) {
super(BalanceOfLp, unitOfWork);
}

Expand Down Expand Up @@ -71,4 +75,96 @@ export class BalanceOfLpRepository extends BaseRepository<BalanceOfLp> {
return row;
});
}

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);

const entityManager = this.unitOfWork.getTransactionManager();

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;
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 },
);
}

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;
balance: string;
}>();

return balances.map((item) => ({
...item,
userAddress: "0x" + item.userAddress.toString("hex"),
tokenAddress: "0x" + item.tokenAddress.toString("hex"),
}));
}
}