diff --git a/.env.example b/.env.example index 82e666a..f55b4e3 100644 --- a/.env.example +++ b/.env.example @@ -34,4 +34,4 @@ L1_ERC20_BRIDGE_LINEA=0x62cE247f34dc316f93D3830e4Bf10959FCe630f8 L1_ERC20_BRIDGE_BLAST=0x8Df0c2bA3916bF4789c50dEc5A79b2fc719F500b NOVA_POINT_REDISTRIBUTE_GRAPH_API= -NOVA_POINT_PUFFER_EL_POINTS_GRAPH_API= \ No newline at end of file +NOVA_POINT_PUFFER_EL_POINTS_GRAPH_API=http://3.114.68.110:8000/subgraphs/name/puffer-eth-points-v2 \ No newline at end of file diff --git a/src/puffer/points.controller.ts b/src/puffer/points.controller.ts index 53d3699..d84e594 100644 --- a/src/puffer/points.controller.ts +++ b/src/puffer/points.controller.ts @@ -35,6 +35,8 @@ import { ElPointsDto, PointsDto, ElPointsDtoItem, + LayerBankPufferPointQueryOptionsDto, + PufferPointUserBalance, } from "./points.dto"; import { TokensDto } from "./tokens.dto"; import { NovaService } from "src/nova/nova.service"; @@ -501,7 +503,7 @@ export class PointsController { @Get("/puffer") @ApiOkResponse({ description: - "Return paginated results of all users' Puffer Eigenlayer Points. The rule is to add 30 points per hour.\nTiming starts from the user's first deposit, with each user having an independent timer.", + "Return paginated results of all users' Puffer Points. The rule is to add 30 points per hour.\nTiming starts from the user's first deposit, with each user having an independent timer.", type: ElPointsDto, }) @ApiBadRequestResponse({ @@ -611,4 +613,65 @@ export class PointsController { return res; } + + @Get("/puffer/:address/balances") + @ApiOkResponse({ + description: + "Return users' puffer balance. Including the withdrawing and staked balance in dapp.", + type: ElPointsDto, + }) + @ApiBadRequestResponse({ + description: '{ "message": "Not Found", "statusCode": 404 }', + }) + public async queryUserPufferHistoricData( + @Param("address", new ParseAddressPipe()) address: string, + @Query() queryOptions: LayerBankPufferPointQueryOptionsDto, + ): Promise { + let res: PufferPointUserBalance; + try { + const [userPosition, pools] = + await this.puffPointsService.getPufferUserBalance( + address, + queryOptions.time, + ); + + const dappBalance = userPosition.positionHistory.map((item) => { + const pool = pools.find((i) => i.pool === item.pool); + return { + dappName: item.poolName, + balance: Number( + ethers.formatEther( + (BigInt(pool.balance) * BigInt(item.supplied)) / + BigInt(pool.totalSupplied), + ), + ).toFixed(6), + }; + }); + res = { + errno: 0, + errmsg: "no error", + data: { + dappBalance: dappBalance, + withdrawingBalance: Number( + ethers.formatEther( + userPosition.withdrawHistory.reduce((prev, cur) => { + return prev + BigInt(cur.balance); + }, BigInt(0)), + ), + ).toFixed(6), + }, + }; + } catch (e) { + res = { + errno: 1, + errmsg: "Not Found", + data: { + dappBalance: [], + withdrawingBalance: "0", + }, + }; + } + + return res; + } } diff --git a/src/puffer/points.dto.ts b/src/puffer/points.dto.ts index 03b6501..9679eff 100644 --- a/src/puffer/points.dto.ts +++ b/src/puffer/points.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -// import { IsArray, IsNumber, IsString } from "class-validator"; +import { IsDateString } from "class-validator"; export class PointsDto { @ApiProperty({ @@ -220,3 +220,61 @@ export class ElPointsDto { }) public readonly data: ElPointsDtoData; } + +export class LayerBankPufferPointQueryOptionsDto { + @ApiProperty({ + type: Number, + description: "date time to query", + example: "2024-04-28 10:20:22", + }) + @IsDateString() + public readonly time: string; +} + +export class PufferPointUserBalanceData { + @ApiProperty({ + type: String, + description: "withdrawing balance", + example: "0.020000", + }) + public readonly withdrawingBalance: string; + + @ApiProperty({ + type: LiquidityDetails, + description: "user staked details on dapps", + example: [ + { + dappName: "LayerBank", + balance: "0.000023", + }, + { + dappName: "Aqua", + balance: "0.010000", + }, + ], + }) + public readonly dappBalance: LiquidityDetails[]; +} + +export class PufferPointUserBalance { + @ApiProperty({ + type: Number, + description: "error code", + example: 0, + }) + public readonly errno: number; + //err msg + @ApiProperty({ + type: String, + description: "error message", + example: "no error", + }) + public readonly errmsg: string; + + @ApiProperty({ + type: PufferPointUserBalanceData, + description: "puffer points data", + nullable: true, + }) + public readonly data: PufferPointUserBalanceData; +} diff --git a/src/puffer/puffPoints.service.ts b/src/puffer/puffPoints.service.ts index a12a351..48b6f7c 100644 --- a/src/puffer/puffPoints.service.ts +++ b/src/puffer/puffPoints.service.ts @@ -67,6 +67,22 @@ interface PufferElPoints { userPositions: EigenlayerPosition[]; } +type PufferUserBalance = [ + { + id: string; + balance: string; + positionHistory: { + id: string; + pool: string; + supplied: string; + token: string; + poolName: string; + }[]; + withdrawHistory: WithdrawnItem[]; + }, + Array, +]; + const LAYERBANK_LPUFFER = "0xdd6105865380984716C6B2a1591F9643e6ED1C48".toLocaleLowerCase(); const AQUA_LPUFFER = @@ -315,7 +331,7 @@ export class PuffPointsService { .toNumber(), })); } - + //get aqua point public async getAquaPointList( addresses: string[], @@ -339,7 +355,7 @@ export class PuffPointsService { .toNumber(), })); } - + public async getPuffElPointsByAddress( address: string, ): Promise { @@ -393,6 +409,115 @@ export class PuffPointsService { } } + public async getPufferUserBalance( + address: string, + date: string, + ): Promise { + const protocolName = ["LayerBank"]; // "Aqua" to be added + + const specialDateTime = new Date("2024-05-05 00:00:00").getTime(); + const queryDateTime = new Date(date).getTime(); + + const queryUnixTime = Math.floor(queryDateTime) / 1000; + const queryWithdrawnUnixTime = + queryDateTime > specialDateTime + ? Math.floor((queryDateTime - 7 * 24 * 60 * 60 * 1000) / 1000) + : Math.floor((queryDateTime - 14 * 24 * 60 * 60 * 1000) / 1000); + + try { + const balanceQueryBody = { + query: `{ + userPosition(id: "${address}") { + id + balance + positionHistory( + where: { + poolName_in: ${JSON.stringify(protocolName)} + blockTimestamp_lte: "${queryUnixTime}" + } + first: 1 + orderBy: blockNumber + orderDirection: desc + ) { + id + pool + supplied + token + poolName + blockNumber + blockTimestamp + } + withdrawHistory(first: 1000, where: { + blockTimestamp_gte: "${queryWithdrawnUnixTime}", + blockTimestamp_lte: "${queryUnixTime}", + token: "0x1B49eCf1A8323Db4abf48b2F5EFaA33F7DdAB3FC"} + ) { + token + id + blockTimestamp + blockNumber + balance + } + } + }`, + }; + + const response = await fetch(this.puffElPointsGraphApi, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(balanceQueryBody), + }); + const { data } = await response.json(); + + const historicData = data.userPosition.positionHistory.map((i) => ({ + poolId: i.pool, + })); + + const genPoolQueryBody = (poolId: string) => ({ + query: `{ + poolHistoricItems( + where: { + pool: "${poolId}" + blockTimestamp_lte: "${queryUnixTime}" + } + orderBy: blockTimestamp + orderDirection: desc + first: 1 + ) { + decimals + id + pool + name + symbol + totalSupplied + underlying + balance + blockTimestamp + blockNumber + } + }`, + }); + + const poolData = await Promise.all( + historicData.map(async (item) => { + const queryString = genPoolQueryBody(item.poolId); + const response = await fetch(this.puffElPointsGraphApi, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(queryString), + }); + const { data } = await response.json(); + return data.poolHistoricItems[0]; + }), + ); + + return [data.userPosition, poolData]; + } catch (err) { + this.logger.error("Fetch puffer points by address data fail", err.stack); + return undefined; + } + } + public async getPuffElPoints( pagingOption: PagingOptionsDto, ): Promise {