From 1a288db4f646896cb25c2c684cda4d2036bd46e9 Mon Sep 17 00:00:00 2001 From: robert Date: Thu, 26 Dec 2024 17:41:51 +0800 Subject: [PATCH] feat: add endpoint to query user rseth balance at a specific timestamp --- src/rseth/rseth.controller.ts | 41 +++++++++- src/rseth/rseth.dto.ts | 85 +++++++++++++++++++++ src/rseth/rseth.service.ts | 137 ++++++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+), 2 deletions(-) diff --git a/src/rseth/rseth.controller.ts b/src/rseth/rseth.controller.ts index 3a85a7d..c651206 100644 --- a/src/rseth/rseth.controller.ts +++ b/src/rseth/rseth.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Logger, Query } from "@nestjs/common"; +import { Controller, Get, Logger, Param, Query } from "@nestjs/common"; import { ApiBadRequestResponse, ApiExcludeController, @@ -19,7 +19,11 @@ import { import { PagingOptionsDto } from "src/common/pagingOptionsDto.dto"; import { PagingMetaDto } from "src/common/paging.dto"; import { PaginationUtil } from "src/common/pagination.util"; -import { RsethPointItem, RsethReturnDto } from "./rseth.dto"; +import { + RsethPointItem, + RsethReturnDto, + UserRsethDateBalanceDto, +} from "./rseth.dto"; @ApiTags("rseth") @ApiExcludeController(false) @@ -235,4 +239,37 @@ export class RsethController { } return result as RsethReturnDto; } + + @Get("/rseth/:address/balanceOfTimestamp") + @ApiOkResponse({ + description: + "Return users' rseth balance at a specific time. Including the withdrawing and staked balance in dapp.", + type: UserRsethDateBalanceDto, + }) + public async queryUserPufferDateBalance( + @Param("address", new ParseAddressPipe()) address: string, + @Query("timestamp") timestamp: number, + ) { + try { + const data = await this.rsethService.getBalanceByAddresses( + address, + timestamp, + ); + const res = { + errno: 0, + errmsg: "no error", + data: data, + }; + return res; + } catch (err) { + this.logger.error( + `get rseth balance at a specific time failed: ${err.stack}`, + ); + return { + errno: 1, + errmsg: err.message, + data: null, + }; + } + } } diff --git a/src/rseth/rseth.dto.ts b/src/rseth/rseth.dto.ts index b243b57..16faefc 100644 --- a/src/rseth/rseth.dto.ts +++ b/src/rseth/rseth.dto.ts @@ -1,4 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; import { PagingMetaDto } from "src/common/paging.dto"; export class RsethTotalPointDto { @@ -98,3 +99,87 @@ export class RsethReturnDto { }) public readonly data?: RsethPointItem[]; } + +class LiquidityDetails { + @ApiProperty({ + type: String, + description: "dapp name", + example: "Aqua", + }) + dappName: string; + + @ApiProperty({ + type: String, + description: "rseth balance in the dapp", + example: "0.020000", + }) + balance: string; +} + +export class UserRsEthDateBalanceItem { + @ApiProperty({ + type: String, + description: "total rseth balance", + example: "0.020000", + }) + totalBalance: string; + + @ApiProperty({ + type: String, + description: "total withdrawn rseth balance in progress", + example: "0.010000", + }) + withdrawingBalance: string; + + @ApiProperty({ + type: String, + description: "rseth balance of the user account", + example: "0.010000", + }) + userBalance: string; + + @ApiProperty({ + type: String, + description: "total user staked rseth on dapps", + example: "0.000000", + }) + liquidityBalance: string; + + @ApiProperty({ + type: LiquidityDetails, + description: "user staked details on dapps", + example: [ + { + dappName: "Aqua", + balance: "0.000023", + }, + ], + }) + @Type(() => LiquidityDetails) + liquidityDetails: LiquidityDetails[]; +} + +export class UserRsethDateBalanceDto { + @ApiProperty({ + type: Number, + description: "error code", + example: 0, + }) + public readonly errno: number; + //errmsg + @ApiProperty({ + type: String, + description: "error message", + example: "no error", + }) + public readonly errmsg: string; + + @ApiProperty({ + description: "rseth balance data", + nullable: true, + }) + public readonly data: { + rsethEthereum: UserRsEthDateBalanceItem; + rsethArbitrum: UserRsEthDateBalanceItem; + }; +} diff --git a/src/rseth/rseth.service.ts b/src/rseth/rseth.service.ts index b25fb8b..3b1cdb3 100644 --- a/src/rseth/rseth.service.ts +++ b/src/rseth/rseth.service.ts @@ -11,6 +11,8 @@ import BigNumber from "bignumber.js"; import waitFor from "src/utils/waitFor"; import { LocalPointsItem } from "../common/service/projectGraph.service"; import { Worker } from "src/common/worker"; +import { ethers } from "ethers"; +import { WithdrawService } from "src/common/service/withdraw.service"; export interface RsethPointItemWithBalance { address: string; @@ -43,6 +45,13 @@ export interface RsethData { items: RsethPointItemWithBalance[] | RsethPointItemWithoutBalance[]; } +const RSETH_ETHEREUM = "0x186c0c42C617f1Ce65C4f7DF31842eD7C5fD8260"; +const RSETH_ARBITRUM = "0x4A2da287deB06163fB4D77c52901683d69bD06f4"; +const AQUA_VAULT = + "0x4AC97E2727B0e92AE32F5796b97b7f98dc47F059".toLocaleLowerCase(); +const AQUA_RSETH_LP = + "0xae8AF9bdFE0099f6d0A5234009b78642EfAC1b00".toLocaleLowerCase(); + @Injectable() export class RsethService extends Worker { private readonly projectName: string = "rseth"; @@ -64,6 +73,7 @@ export class RsethService extends Worker { private readonly graphQueryService: GraphQueryService, private readonly explorerService: ExplorerService, private readonly configService: ConfigService, + private readonly withdrawService: WithdrawService, ) { super(); this.logger = new Logger(RsethService.name); @@ -293,4 +303,131 @@ export class RsethService extends Worker { } return tokensMapBridgeTokens; } + + public async getBalanceByAddresses(address: string, toTimestamp: number) { + const rsethEthereum = await this.getBalanceByAddress( + address, + toTimestamp, + RSETH_ETHEREUM, + ); + const rsethArbitrum = await this.getBalanceByAddress( + address, + toTimestamp, + RSETH_ARBITRUM, + ); + return { + rsethEthereum, + rsethArbitrum, + }; + } + + public async getBalanceByAddress( + address: string, + toTimestamp: number, + tokenAddress: string, + ) { + const blocks = await this.explorerService.getLastBlocks(toTimestamp); + if (!blocks || blocks.length === 0) { + throw new Error("Failed to get blocks."); + } + const blockNumber = blocks[0].number ?? 0; + if (blockNumber === 0) { + throw new Error("Failed to get block number."); + } + let directBalance = BigInt(0); + let withdrawBalance = BigInt(0); + let aquaBalance = BigInt(0); + + const provider = new ethers.JsonRpcProvider("https://rpc.zklink.io"); + const block = await provider.getBlock(Number(blockNumber)); + const balanceOfMethod = "0x70a08231"; + const totalSupplyMethod = "0x18160ddd"; + const promiseList = []; + + // rsseth balance of address + promiseList.push( + provider.call({ + to: tokenAddress, + data: balanceOfMethod + address.replace("0x", "").padStart(64, "0"), + blockTag: Number(blockNumber), + }), + ); + + // rsseth balance of aqua pairaddress + promiseList.push( + provider.call({ + to: tokenAddress, + data: balanceOfMethod + AQUA_VAULT.replace("0x", "").padStart(64, "0"), + blockTag: Number(blockNumber), + }), + ); + + // aq-lrseth balance of address + promiseList.push( + provider.call({ + to: AQUA_RSETH_LP, + data: balanceOfMethod + address.replace("0x", "").padStart(64, "0"), + blockTag: Number(blockNumber), + }), + ); + + // aq-lrseth total supply + promiseList.push( + provider.call({ + to: AQUA_RSETH_LP, + data: totalSupplyMethod, + blockTag: Number(blockNumber), + }), + ); + + const [ + rsethEthAddress, + rsethEthAqua, + aqlrsethAddress, + aqlrsethTotalSupply, + ] = await Promise.all(promiseList); + + directBalance = BigInt(rsethEthAddress); + + const rsethAquaBigInt = BigNumber(rsethEthAqua); + const aqlrsethAddressBigInt = BigNumber(aqlrsethAddress); + const aqlrsethTotalSupplyBigInt = BigNumber(aqlrsethTotalSupply); + + // aqua balance + const aquaBalanceBg = aqlrsethAddressBigInt + .multipliedBy(rsethAquaBigInt) + .div(aqlrsethTotalSupplyBigInt); + aquaBalance = BigInt(aquaBalanceBg.toFixed(0)); + + // withdrawHistory + const withdrawHistory = await this.withdrawService.getWithdrawHistory( + address, + tokenAddress, + block.timestamp, + ); + const blockTimestamp = block.timestamp; + for (const item of withdrawHistory) { + const tmpEndTime = this.withdrawService.findWithdrawEndTime( + item.blockTimestamp, + ); + // if withdrawTime is in the future, add balance to withdrawBalance + if (tmpEndTime > blockTimestamp) { + withdrawBalance = withdrawBalance + BigInt(item.balance); + } + } + + const totalBalance = directBalance + withdrawBalance + aquaBalance; + return { + totalBalance: ethers.formatEther(totalBalance).toString(), + withdrawingBalance: ethers.formatEther(withdrawBalance).toString(), + userBalance: ethers.formatEther(directBalance).toString(), + liquidityBalance: ethers.formatEther(aquaBalance).toString(), + liquidityDetails: [ + { + dappName: "aqua", + balance: ethers.formatEther(aquaBalance).toString(), + }, + ], + }; + } }