diff --git a/src/app.module.ts b/src/app.module.ts index ba54103..e139c1c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -24,10 +24,11 @@ import { NovaService } from './nova/nova.service'; import { NovaController } from './nova/nova.controller'; import { RenzoPagingController } from './renzo/renzo.paging.controller'; import { NovaPagingController } from './nova/nova.paging.controller'; -import { RsethPagingController } from './rseth/rseth.paging.controller'; import { ProjectGraphService } from './common/service/projectGraph.service'; import { WithdrawService } from './common/service/withdraw.service'; import { MagpieService } from './magpie/magpie.service'; +import { RsethApiService } from './rseth/rseth.api.service'; +import { RsethService } from './rseth/rseth.service'; @Module({ imports: [ @@ -54,7 +55,6 @@ import { MagpieService } from './magpie/magpie.service'; MagpieController, NovaController, RenzoPagingController, - RsethPagingController, NovaPagingController ], providers: [ @@ -75,7 +75,9 @@ import { MagpieService } from './magpie/magpie.service'; NovaService, ProjectGraphService, WithdrawService, - MagpieService + MagpieService, + RsethApiService, + RsethService, ], }) export class AppModule {} diff --git a/src/common/service/graphQuery.service.ts b/src/common/service/graphQuery.service.ts index 8b65489..728f753 100644 --- a/src/common/service/graphQuery.service.ts +++ b/src/common/service/graphQuery.service.ts @@ -50,7 +50,7 @@ export class GraphQueryService implements OnModuleInit { this.logger.error("GraphQueryService init failed", err.stack); } }; - await func(); + func(); setInterval(func, 1000 * 600); } @@ -72,8 +72,8 @@ export class GraphQueryService implements OnModuleInit { const allPoints = data.data.totalPoints as GraphTotalPoint[]; allPoints.forEach((totalPoint) => { const projectArr = totalPoint.project.split('-'); - const projectName = projectArr[0]; - const tokenAddress = projectArr[1]; + const projectName = projectArr[0].toLocaleLowerCase(); + const tokenAddress = projectArr[1].toLocaleLowerCase(); if (!this.projectTokenMap.has(projectName)) { this.projectTokenMap.set(projectName, new Map()); this.logger.log(`GraphQueryService ${projectName} had save to cache.`); @@ -86,10 +86,6 @@ export class GraphQueryService implements OnModuleInit { } public getAllTokenAddresses(projectName: string): string[] { - for (const key in this.projectTokenMap) { - console.log(`token:${key}`); - console.log(this.projectTokenMap[key].keys()); - } const project = this.projectTokenMap.get(projectName); return project ? Array.from(project.keys()) : []; } diff --git a/src/common/service/withdraw.service.ts b/src/common/service/withdraw.service.ts index 84dda8e..5f68558 100644 --- a/src/common/service/withdraw.service.ts +++ b/src/common/service/withdraw.service.ts @@ -45,7 +45,7 @@ export class WithdrawService implements OnModuleInit{ this.logger.error(`${WithdrawService.name} init failed`, err.stack); } }; - await func(); + func(); setInterval(func, 1000 * 60); } diff --git a/src/magpie/magpie.controller.ts b/src/magpie/magpie.controller.ts index 583e261..a8fc260 100644 --- a/src/magpie/magpie.controller.ts +++ b/src/magpie/magpie.controller.ts @@ -175,14 +175,14 @@ export class MagpieController { list = paging.items; meta = paging.meta; } - const res = { + let res = { errno: 0, errmsg: 'no error', totals: { eigenpiePoints: Number(ethers.formatEther(eigenpiePoints)).toFixed(6), eigenLayerPoints: Number(ethers.formatEther(eigenLayerPoints)).toFixed(6), }, - data: finalPoints.map(item => { + data: list.map(item => { return item.tokenAddress ? { address: item.address, diff --git a/src/nova/novaapi.service.ts b/src/nova/novaapi.service.ts index 1366926..ca94d1a 100644 --- a/src/nova/novaapi.service.ts +++ b/src/nova/novaapi.service.ts @@ -33,7 +33,7 @@ export class NovaApiService { this.logger.error("NovaApiService init failed", err.stack); } }; - await func(); + func(); setInterval(func, 1000 * 10); } diff --git a/src/renzo/renzo.service.ts b/src/renzo/renzo.service.ts index 2b08df4..f53768e 100644 --- a/src/renzo/renzo.service.ts +++ b/src/renzo/renzo.service.ts @@ -45,7 +45,7 @@ export class RenzoService{ private readonly renzoApiService: RenzoApiService, private readonly projectGraphService: ProjectGraphService, private readonly graphQueryService: GraphQueryService, - private readonly configService: ConfigService, + private readonly configService: ConfigService ) { this.logger = new Logger(RenzoService.name); this.l1Erc20BridgeEthereum = configService.get('l1Erc20BridgeEthereum'); @@ -165,9 +165,7 @@ export class RenzoService{ } private async getTokensMapBriageTokens(): Promise>{ - const tokens = this.tokenAddress.map(item=>{ - return item.toLocaleLowerCase(); - }); + const tokens = this.tokenAddress; const tokensMapBridgeTokens: Map = new Map; const allTokens = await this.explorerService.getTokens(); for (const item of allTokens) { diff --git a/src/renzo/renzoapi.service.spec.ts b/src/renzo/renzoapi.service.spec.ts index 5420231..ae95a0a 100644 --- a/src/renzo/renzoapi.service.spec.ts +++ b/src/renzo/renzoapi.service.spec.ts @@ -16,32 +16,4 @@ describe('RenzoApiService', () => { renzoApiService = moduleFixture.get(RenzoApiService); configService = moduleFixture.get(ConfigService); }); - - it('fetchRenzoPoints', async () => { - const renzoPoints = await renzoApiService.fetchRenzoPoints(); - const renzoPoints2 = [ - await renzoApiService._fetchRenzoPoints( - configService.get('l1Erc20BridgeEthereum'), - ), - await renzoApiService._fetchRenzoPoints( - configService.get('l1Erc20BridgeArbitrum'), - ), - await renzoApiService._fetchRenzoPoints( - configService.get('l1Erc20BridgeLinea'), - ), - await renzoApiService._fetchRenzoPoints( - configService.get('l1Erc20BridgeBlast'), - ), - ] - .flat() - .reduce( - (acc, renzoPoints) => { - acc.renzoPoints += renzoPoints.renzoPoints; - acc.eigenLayerPoints += renzoPoints.eigenLayerPoints; - return acc; - }, - { renzoPoints: 0, eigenLayerPoints: 0 }, - ); - expect(renzoPoints).toEqual(renzoPoints2); - },10000); }); diff --git a/src/renzo/renzoapi.service.ts b/src/renzo/renzoapi.service.ts index 931f4ee..e7e3d86 100644 --- a/src/renzo/renzoapi.service.ts +++ b/src/renzo/renzoapi.service.ts @@ -37,24 +37,6 @@ export class RenzoApiService { } } - public async fetchRenzoPoints(): Promise { - const allRenzoPoints = []; - - for (const bridgeAddress of this.l1Erc20Bridges) { - const renzoPoints = await this._fetchRenzoPoints(bridgeAddress); - allRenzoPoints.push(renzoPoints); - await new Promise(resolve => setTimeout(resolve, 1000)); // wait 1s - } - return allRenzoPoints.reduce( - (acc, renzoPoints) => { - acc.renzoPoints += renzoPoints.renzoPoints; - acc.eigenLayerPoints += renzoPoints.eigenLayerPoints; - return acc; - }, - { renzoPoints: 0, eigenLayerPoints: 0 }, - ); - } - public async fetchTokensRenzoPoints(): Promise> { const allRenzoPoints: Map = new Map; diff --git a/src/rseth/rseth.api.service.ts b/src/rseth/rseth.api.service.ts new file mode 100644 index 0000000..3b19b76 --- /dev/null +++ b/src/rseth/rseth.api.service.ts @@ -0,0 +1,58 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +export interface RsethPoints { + elPoints: number; + kelpMiles: number; +} + +@Injectable() +export class RsethApiService { + private readonly logger: Logger; + private readonly rsethApiBaseurl: string; + private readonly l1Erc20Bridges: string[]; + public constructor(configService: ConfigService) { + this.logger = new Logger(RsethApiService.name); + this.rsethApiBaseurl = 'https://common.kelpdao.xyz/km-el-points/user/'; + this.l1Erc20Bridges = [ + configService.get('l1Erc20BridgeEthereum'), + configService.get('l1Erc20BridgeArbitrum') + ]; + } + + public async fetchTokensRsethPoints(): Promise> { + const allRsethPoints: Map = new Map; + + for (const bridgeAddress of this.l1Erc20Bridges) { + const rsethPoints = await this._fetchRsethPoints(bridgeAddress); + allRsethPoints.set(bridgeAddress.toLocaleLowerCase(), rsethPoints); + await new Promise(resolve => setTimeout(resolve, 1000)); // wait 1s + } + return allRsethPoints; + } + + public async _fetchRsethPoints(bridgeAddress: string): Promise { + this.logger.log(`start fetchRsethPoints bridgeAddress: ${bridgeAddress}`); + const responseStr = await fetch(`${this.rsethApiBaseurl}${bridgeAddress}`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36', + }, + }); + this.logger.log(`end fetchRsethPoints bridgeAddress: ${bridgeAddress}`); + const response = await responseStr.json(); + if ( + (response?.value?.elPoints ?? undefined) === undefined || + (response?.value?.kelpMiles ?? undefined) === undefined + ) { + this.logger.error(`No rseth points bridgeAddress: ${bridgeAddress}`); + return { elPoints: 0, kelpMiles: 0 }; + } + this.logger.log(`success fetchRsethPoints bridgeAddress: ${bridgeAddress}, elPoints:${response.value.elPoints}, kelpMiles:${response.value.kelpMiles} `); + return { + elPoints: response.value.elPoints, + kelpMiles: response.value.kelpMiles + }; + } +} \ No newline at end of file diff --git a/src/rseth/rseth.controller.ts b/src/rseth/rseth.controller.ts index f7fde3f..bd5f621 100644 --- a/src/rseth/rseth.controller.ts +++ b/src/rseth/rseth.controller.ts @@ -7,28 +7,14 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { LRUCache } from 'lru-cache'; import { ParseAddressPipe } from 'src/common/pipes/parseAddress.pipe'; -import { - NOT_FOUND_EXCEPTION, - SERVICE_EXCEPTION, - TokenPointsWithoutDecimalsDto, -} from '../puffer/tokenPointsWithoutDecimals.dto'; -import { ProjectService } from 'src/common/service/project.service'; +import { SERVICE_EXCEPTION } from '../puffer/tokenPointsWithoutDecimals.dto'; import { ethers } from 'ethers'; - -const options = { - // how long to live in ms - ttl: 1000 * 10, - // return stale items before removing from cache? - allowStale: false, - ttlAutopurge: true, -}; - -const cache = new LRUCache(options); -const RSETH_ALL_POINTS_CACHE_KEY = 'allRsethPoints'; -const RSETH_ALL_POINTS_WITH_BALANCE_CACHE_KEY = 'allRsethPointsWithBalance'; -const GRAPH_QUERY_PROJECT_ID = 'rseth'; +import { RsethData, RsethPointItemWithBalance, RsethPointItemWithoutBalance, RsethService } from './rseth.service'; +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'; @ApiTags('rseth') @ApiExcludeController(false) @@ -36,7 +22,7 @@ const GRAPH_QUERY_PROJECT_ID = 'rseth'; export class RsethController { private readonly logger = new Logger(RsethController.name); - constructor(private projectService: ProjectService) {} + constructor(private rsethService: RsethService) {} @Get('/points') @ApiOperation({ summary: 'Get rsETH personal points' }) @@ -48,22 +34,16 @@ export class RsethController { }) public async getRsethPoints( @Query('address', new ParseAddressPipe()) address: string, - ): Promise { - let finalPoints: any[], finalTotalPoints: bigint; - + ): Promise { + let pointData: RsethData; try{ - const pointData = await this.projectService.getPoints(GRAPH_QUERY_PROJECT_ID, address); - finalPoints = pointData.finalPoints; - finalTotalPoints = pointData.finalTotalPoints; + pointData = await this.rsethService.getPointsData(address); } catch (err) { - this.logger.error('Get rsETH all points failed', err); + this.logger.error('Get rsETH all points failed', err.stack); return SERVICE_EXCEPTION; } - if(!finalPoints || !finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - return this.getReturnData(finalPoints, finalTotalPoints); + return this.getReturnData(pointData.items, pointData.realTotalElPoints, pointData.realTotalKelpMiles); } @Get('/all/points') @@ -73,7 +53,7 @@ export class RsethController { }) @ApiOkResponse({ description: "Return all users' rsETH points.", - type: TokenPointsWithoutDecimalsDto, + type: RsethReturnDto, }) @ApiBadRequestResponse({ description: '{ "errno": 1, "errmsg": "Service exception" }', @@ -82,31 +62,48 @@ export class RsethController { description: '{ "errno": 1, "errmsg": "not found" }', }) public async getAllRsethPoints(): Promise< - Partial + Partial > { - const allPoints = cache.get( - RSETH_ALL_POINTS_CACHE_KEY, - ) as TokenPointsWithoutDecimalsDto; - if (allPoints) { - return allPoints; + let pointData: RsethData; + try{ + pointData = await this.rsethService.getPointsData(); + } catch (err) { + this.logger.error('Get rsETH all points failed', err.stack); + return SERVICE_EXCEPTION; } - let cacheData: TokenPointsWithoutDecimalsDto, finalPoints: any[], finalTotalPoints: bigint; + return this.getReturnData(pointData.items, pointData.realTotalElPoints, pointData.realTotalKelpMiles); + } + + @Get('/all/points/paging') + @ApiOperation({ + summary: + 'Get rsETH paging point for all users, point are based on user token dimension', + }) + @ApiOkResponse({ + description: "Return all users' rsETH points.", + type: RsethReturnDto, + }) + @ApiBadRequestResponse({ + description: '{ "errno": 1, "errmsg": "Service exception" }', + }) + @ApiNotFoundResponse({ + description: '{ "errno": 1, "errmsg": "not found" }', + }) + public async getPagingRsethPoints( + @Query() pagingOptions: PagingOptionsDto + ): Promise< + Partial + > { + let pointData: RsethData; try{ - const pointData = await this.projectService.getAllPoints(GRAPH_QUERY_PROJECT_ID); - finalPoints = pointData.finalPoints; - finalTotalPoints = pointData.finalTotalPoints; + pointData = await this.rsethService.getPointsData(); } catch (err) { - this.logger.error('Get rsETH all points failed', err); + this.logger.error('Get rsETH all points failed', err.stack); return SERVICE_EXCEPTION; } - if(!finalPoints || !finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - cacheData = this.getReturnData(finalPoints, finalTotalPoints);; - cache.set(RSETH_ALL_POINTS_CACHE_KEY, cacheData); - return cacheData; + return this.getReturnData(pointData.items, pointData.realTotalElPoints, pointData.realTotalKelpMiles, pagingOptions); } @Get('/all/points-with-balance') @@ -116,7 +113,7 @@ export class RsethController { }) @ApiOkResponse({ description: "Return all users' rsETH points with balance.", - type: TokenPointsWithoutDecimalsDto, + type: RsethReturnDto, }) @ApiBadRequestResponse({ description: '{ "errno": 1, "errmsg": "Service exception" }', @@ -125,45 +122,97 @@ export class RsethController { description: '{ "errno": 1, "errmsg": "not found" }', }) public async getAllRsethPointsWithBalance(): Promise< - Partial + Partial > { - const allPoints = cache.get( - RSETH_ALL_POINTS_WITH_BALANCE_CACHE_KEY, - ) as TokenPointsWithoutDecimalsDto; - if (allPoints) { - return allPoints; + let pointData: RsethData; + try{ + pointData = await this.rsethService.getPointsData(); + } catch (err) { + this.logger.error('Get rsETH all points failed', err.stack); + return SERVICE_EXCEPTION; } - let cacheData: TokenPointsWithoutDecimalsDto, finalPoints: any[], finalTotalPoints: bigint; + return this.getReturnData(pointData.items, pointData.realTotalElPoints, pointData.realTotalKelpMiles); + } + + @Get('/all/points-with-balance/paging') + @ApiOperation({ + summary: + 'Get rsETH paging point for all users, point are based on user token dimension', + }) + @ApiOkResponse({ + description: "Return all users' rsETH points with balance.", + type: RsethReturnDto, + }) + @ApiBadRequestResponse({ + description: '{ "errno": 1, "errmsg": "Service exception" }', + }) + @ApiNotFoundResponse({ + description: '{ "errno": 1, "errmsg": "not found" }', + }) + public async getPagingRsethPointsWithBalance( + @Query() pagingOptions: PagingOptionsDto + ): Promise< + Partial + > { + let pointData: RsethData; try{ - const pointData = await this.projectService.getAllPointsWithBalance(GRAPH_QUERY_PROJECT_ID); - finalPoints = pointData.finalPoints; - finalTotalPoints = pointData.finalTotalPoints; + pointData = await this.rsethService.getPointsData(); } catch (err) { - this.logger.error('Get rsETH all points failed', err); + this.logger.error('Get rsETH all points failed', err.stack); return SERVICE_EXCEPTION; } - if(!finalPoints || !finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - cacheData = this.getReturnData(finalPoints, finalTotalPoints);; - cache.set(RSETH_ALL_POINTS_WITH_BALANCE_CACHE_KEY, cacheData); - return cacheData; + return this.getReturnData(pointData.items, pointData.realTotalElPoints, pointData.realTotalKelpMiles, pagingOptions); } private getReturnData( - finalPoints: any[], - finnalTotalPoints: bigint, - ): TokenPointsWithoutDecimalsDto{ - return { + finalPoints: RsethPointItemWithBalance[] | RsethPointItemWithoutBalance[], + finnalTotalElPoints: number, + finnalTotalKelpMiles: number, + pagingOptions?: PagingOptionsDto + ): RsethReturnDto{ + let list = finalPoints; + let meta: PagingMetaDto; + if(null != pagingOptions){ + const {page = 1, limit = 100} = pagingOptions; + const paging = PaginationUtil.paginate(list, page, limit); + list = paging.items; + meta = paging.meta; + } + let result = { errno: 0, errmsg: 'no error', - total_points: ethers.formatEther(finnalTotalPoints), - data: finalPoints.map(point => { - point.points = ethers.formatEther(point.points); - return point; + points: { + elPoints: finnalTotalElPoints.toString(), + kelpMiles: finnalTotalKelpMiles.toString() + }, + data: list.map(item => { + return item.tokenAddress ? + { + address: item.address, + tokenAddress: item.tokenAddress, + balance: Number(ethers.formatEther(item.balance)).toFixed(6), + points:{ + elPoints: item.realElPoints.toString(), + kelpMiles: item.realKelpMiles.toString() + }, + updated_at: item.updatedAt + } as RsethPointItem + : + { + address: item.address, + points:{ + elPoints: item.realElPoints.toString(), + kelpMiles: item.realKelpMiles.toString() + }, + updated_at: item.updatedAt + } as RsethPointItem; }) - } as TokenPointsWithoutDecimalsDto; + }; + if(meta){ + result["meta"] = meta; + } + return result as RsethReturnDto; } } diff --git a/src/rseth/rseth.dto.ts b/src/rseth/rseth.dto.ts new file mode 100644 index 0000000..da457e5 --- /dev/null +++ b/src/rseth/rseth.dto.ts @@ -0,0 +1,100 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { PagingMetaDto } from 'src/common/paging.dto'; + +export class RsethTotalPointDto { + @ApiProperty({ + type: String, + description: 'elPoints', + example: '1759.5893', + }) + public readonly elPoints: string; + + @ApiProperty({ + type: String, + description: 'kelpMiles', + example: '1759.5893', + }) + public readonly kelpMiles: string; +} + +export class RsethPointItem { + @ApiProperty({ + type: String, + description: 'user address', + example: '0xd754Ff5e8a6f257E162F72578A4bB0493c0681d8', + }) + public readonly address: string; + + @ApiProperty({ + type: RsethTotalPointDto, + description: 'user points', + example: { + elPoints: '437936.342254', + kelpMiles: '437936.342254', + }, + }) + public readonly points: RsethTotalPointDto; + + @ApiProperty({ + type: String, + description: 'token address', + example: '0xd754Ff5e8a6f257E162F72578A4bB0493c0681d8', + required: false, + }) + public readonly tokenAddress?: string; + + @ApiProperty({ + type: String, + description: 'user balance', + example: "1759.589382", + }) + public readonly balance?: string; + + @ApiProperty({ + type: Number, + description: 'The timestamp when the points was updated', + example: 1710834827, + }) + public readonly updated_at: number; +} + +export class RsethReturnDto { + @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: RsethTotalPointDto, + description: 'elPoints and kelpMiles', + example: { + elPoints: '437936.342254', + kelpMiles: '437936.342254', + }, + required: false, + }) + public readonly points?: RsethTotalPointDto; + + @ApiProperty({ + type: PagingMetaDto, + description: 'page meta', + example: 0, + }) + public readonly meta?: PagingMetaDto; + + @ApiProperty({ + type: [RsethPointItem], + description: 'user points data', + nullable: true, + }) + public readonly data?: RsethPointItem[]; +} diff --git a/src/rseth/rseth.paging.controller.ts b/src/rseth/rseth.paging.controller.ts deleted file mode 100644 index 4fd3ba5..0000000 --- a/src/rseth/rseth.paging.controller.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Controller, Get, Logger, Query } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiExcludeController, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiTags, -} from '@nestjs/swagger'; -import { LRUCache } from 'lru-cache'; -import { ethers } from 'ethers'; -import { ParseAddressPipe } from 'src/common/pipes/parseAddress.pipe'; -import { ProjectService } from 'src/common/service/project.service'; -import { PagingOptionsDto } from 'src/common/pagingOptionsDto.dto'; -import { PaginationUtil } from 'src/common/pagination.util'; -import { PointData } from 'src/common/service/project.service'; -import { PagingMetaDto } from 'src/common/paging.dto'; -import { - NOT_FOUND_EXCEPTION, - SERVICE_EXCEPTION, - TokenPointsWithoutDecimalsDto, -} from '../puffer/tokenPointsWithoutDecimals.dto'; - -const options = { - // how long to live in ms - ttl: 1000 * 10, - // return stale items before removing from cache? - allowStale: false, - ttlAutopurge: true, -}; - -const cache = new LRUCache(options); -const RSETH_ALL_POINTS_CACHE_KEY = 'allRsethPointsData'; -const RSETH_ALL_POINTS_WITH_BALANCE_CACHE_KEY = 'allRsethPointsWithBalanceData'; -const GRAPH_QUERY_PROJECT_ID = 'rseth'; - -@ApiTags('rseth') -@ApiExcludeController(false) -@Controller('rseth') -export class RsethPagingController { - private readonly logger = new Logger(RsethPagingController.name); - - constructor(private projectService: ProjectService) {} - - @Get('/points/paging') - @ApiOperation({ summary: 'Get paginated rsETH personal points' }) - @ApiBadRequestResponse({ - description: '{ "errno": 1, "errmsg": "Service exception" }', - }) - @ApiNotFoundResponse({ - description: '{ "errno": 1, "errmsg": "not found" }', - }) - public async getRsethPoints( - @Query('address', new ParseAddressPipe()) address: string, - @Query() pagingOptions: PagingOptionsDto - ): Promise { - let pointData: PointData; - - try{ - pointData = await this.projectService.getPoints(GRAPH_QUERY_PROJECT_ID, address); - if(!pointData.finalPoints || !pointData.finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - } catch (err) { - this.logger.error('Get rsETH all points failed', err.stack); - return SERVICE_EXCEPTION; - } - - return this.getReturnData(pointData, pagingOptions); - } - - @Get('/all/points/paging') - @ApiOperation({ - summary: - 'Get paginated rsETH point for all users, point are based on user token dimension', - }) - @ApiOkResponse({ - description: "Return paginated all users' rsETH points.", - type: TokenPointsWithoutDecimalsDto, - }) - @ApiBadRequestResponse({ - description: '{ "errno": 1, "errmsg": "Service exception" }', - }) - @ApiNotFoundResponse({ - description: '{ "errno": 1, "errmsg": "not found" }', - }) - public async getAllRsethPoints( - @Query() pagingOptions: PagingOptionsDto - ): Promise< - Partial - > { - let pointData: PointData; - pointData = cache.get(RSETH_ALL_POINTS_CACHE_KEY) as PointData; - if (!pointData) { - try{ - pointData = await this.projectService.getAllPoints(GRAPH_QUERY_PROJECT_ID); - if(!pointData.finalPoints || !pointData.finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - cache.set(RSETH_ALL_POINTS_CACHE_KEY, pointData); - } catch (err) { - this.logger.error('Get rsETH all points failed', err.stack); - return SERVICE_EXCEPTION; - } - } - - return this.getReturnData(pointData, pagingOptions); - } - - @Get('/all/points-with-balance/paging') - @ApiOperation({ - summary: - 'Get paginated rsETH point for all users, point are based on user token dimension', - }) - @ApiOkResponse({ - description: "Return paginated all users' rsETH points with balance.", - type: TokenPointsWithoutDecimalsDto, - }) - @ApiBadRequestResponse({ - description: '{ "errno": 1, "errmsg": "Service exception" }', - }) - @ApiNotFoundResponse({ - description: '{ "errno": 1, "errmsg": "not found" }', - }) - public async getAllRsethPointsWithBalance( - @Query() pagingOptions: PagingOptionsDto - ): Promise< - Partial - > { - let pointData: PointData; - pointData = cache.get(RSETH_ALL_POINTS_WITH_BALANCE_CACHE_KEY) as PointData; - if (!pointData) { - try{ - pointData = await this.projectService.getAllPointsWithBalance(GRAPH_QUERY_PROJECT_ID); - if(!pointData.finalPoints || !pointData.finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - cache.set(RSETH_ALL_POINTS_WITH_BALANCE_CACHE_KEY, pointData); - } catch (err) { - this.logger.error('Get rsETH all points failed', err.stack); - return SERVICE_EXCEPTION; - } - } - - return this.getReturnData(pointData, pagingOptions); - } - - private getReturnData( - pointData: PointData, - pagingOptions: PagingOptionsDto - ): TokenPointsWithoutDecimalsDto{ - let list = pointData.finalPoints; - let meta: PagingMetaDto; - if(null != pagingOptions){ - const {page = 1, limit = 100} = pagingOptions; - const paging = PaginationUtil.paginate(list, page, limit); - list = paging.items; - meta = paging.meta; - } - - return { - errno: 0, - errmsg: 'no error', - total_points: ethers.formatEther(pointData.finalTotalPoints), - meta: meta, - data: list.map(point => { - let tmpPoint = { ...point }; - tmpPoint.points = ethers.formatEther(tmpPoint.points); - return tmpPoint; - }) - } as TokenPointsWithoutDecimalsDto; - } -} diff --git a/src/rseth/rseth.service.ts b/src/rseth/rseth.service.ts new file mode 100644 index 0000000..411e167 --- /dev/null +++ b/src/rseth/rseth.service.ts @@ -0,0 +1,214 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { cloneDeep } from 'lodash'; +import { LocalPointData, ProjectGraphService } from 'src/common/service/projectGraph.service'; +import { RsethApiService, RsethPoints } from './rseth.api.service'; +import { GraphQueryService } from 'src/common/service/graphQuery.service'; +import { ExplorerService } from 'src/common/service/explorer.service'; +import { ConfigService } from '@nestjs/config'; +import BigNumber from 'bignumber.js'; + +export interface RsethPointItemWithBalance{ + address: string, + tokenAddress: string, + balance: bigint, + localPoints: bigint, + realElPoints: number, + realKelpMiles: number, + localTotalPointsPerToken: bigint, + realTotalElPointsPerToken: number, + realTotalKelpMilesPerToken: number, + updatedAt: number +} + +export interface RsethPointItemWithoutBalance{ + address: string, + realElPoints: number, + realKelpMiles: number, + updatedAt: number +} + +export interface RsethData{ + localTotalPoints: bigint, + realTotalElPoints: number, + realTotalKelpMiles: number, + items: RsethPointItemWithBalance[] | RsethPointItemWithoutBalance[] +} + +@Injectable() +export class RsethService{ + private readonly projectName: string = "rseth"; + private readonly logger: Logger; + + public tokenAddress: string[]; + private rsethData: RsethData = {localTotalPoints: BigInt(0), realTotalElPoints: 0, realTotalKelpMiles: 0, items:[]}; + private readonly l1Erc20BridgeEthereum: string; + private readonly l1Erc20BridgeArbitrum: string; + public constructor( + private readonly projectGraphService: ProjectGraphService, + private readonly rsethApiService: RsethApiService, + private readonly graphQueryService: GraphQueryService, + private readonly explorerService: ExplorerService, + private readonly configService: ConfigService + ) { + this.logger = new Logger(RsethService.name); + this.l1Erc20BridgeEthereum = configService.get('l1Erc20BridgeEthereum'); + this.l1Erc20BridgeArbitrum = configService.get('l1Erc20BridgeArbitrum'); + } + + public async onModuleInit(){ + this.logger.log(`Init ${RsethService.name} onmoduleinit`); + const func = async () => { + try { + await this.loadPointsData(); + } catch (err) { + this.logger.error(`${RsethService.name} init failed.`, err.stack); + } + }; + func(); + setInterval(func, 1000 * 60); + } + + // load points data + public async loadPointsData(){ + // get tokens from graph + const tokens = this.graphQueryService.getAllTokenAddresses(this.projectName); + if(tokens.length <= 0){ + this.logger.log(`Graph don't have ${this.projectName} tokens`); + return; + } + this.tokenAddress = tokens; + + const realTotalPointsData = await this.getRealPointsData(); + const localPointsData = await this.getLocalPointsData(); + const localPoints = localPointsData.localPoints; + const localTotalPoints = localPointsData.localTotalPoints; + + // define a variable to store the matched bridge token + const tokensMapBridgeTokens = await this.getTokensMapBriageTokens(); + // define a variable to store the real total el points and kelp miles + let realTotalElPoints = 0, + realTotalKelpMiles = 0; + + let data: RsethPointItemWithBalance[] = []; + // calculate real points = local points * real total points / local total points + for (const item of localPoints) { + const bridgeToken = tokensMapBridgeTokens.get(item.token); + // if the token is not in the bridge token list, skip it + if(!bridgeToken){ + this.logger.log(`Token ${item.token} is not in the bridge token list.`); + continue; + } + const elPointsPerToken = realTotalPointsData.get(bridgeToken)?.elPoints ?? 0; + const kelpMilesPerToken = realTotalPointsData.get(bridgeToken)?.kelpMiles ?? 0; + const realElPoints = Number( + new BigNumber(item.points.toString()) + .multipliedBy(elPointsPerToken.toString()) + .div(item.totalPointsPerToken.toString()) + .toFixed(6) + ); + realTotalElPoints += realElPoints; + const realKelpMiles = Number( + new BigNumber(item.points.toString()) + .multipliedBy(kelpMilesPerToken.toString()) + .div(item.totalPointsPerToken.toString()) + .toFixed(6) + ); + realTotalKelpMiles += realKelpMiles; + const pointsItem: RsethPointItemWithBalance = { + address: item.address, + tokenAddress: item.token, + balance: item.balance, + localPoints: item.points, + realElPoints: realElPoints, + realKelpMiles: realKelpMiles, + localTotalPointsPerToken: item.totalPointsPerToken, + realTotalElPointsPerToken: elPointsPerToken, + realTotalKelpMilesPerToken: kelpMilesPerToken, + updatedAt: item.updatedAt + }; + data.push(pointsItem); + } + if(data.length > 0){ + this.rsethData = { + localTotalPoints: localTotalPoints, + realTotalElPoints: realTotalElPoints, + realTotalKelpMiles: realTotalKelpMiles, + items: data + }; + }else{ + this.logger.log(`Load ${this.projectName} data empty.`); + } + } + + // return points data + public getPointsData(address?: string): RsethData { + let result: RsethData = cloneDeep(this.rsethData); + if(address){ + const _address = address.toLocaleLowerCase(); + result.items = this.rsethData.items.filter( + (item) => item.address === _address + ); + } + return result; + } + + // return local points and totalPoints + public async getLocalPointsData(): Promise{ + return await this.projectGraphService.getPoints(this.projectName); + } + + // return real totalPoints + public async getRealPointsData(): Promise>{ + return await this.rsethApiService.fetchTokensRsethPoints(); + } + + // return real points group by address + public getPointsDataGroupByAddress(): RsethData{ + let result: RsethData = cloneDeep(this.rsethData); + let data: Map = new Map; + const now = (new Date().getTime() / 1000) | 0; + for (let i = 0; i < this.rsethData.items.length; i++) { + const item = this.rsethData.items[i]; + if(!data.has(item.address)){ + data.set(item.address, { + address: item.address, + realElPoints: item.realElPoints, + realKelpMiles: item.realKelpMiles, + updatedAt: now + } as RsethPointItemWithoutBalance); + }else{ + const tmpItem = data.get(item.address); + tmpItem.realKelpMiles += item.realKelpMiles; + tmpItem.realElPoints += item.realElPoints; + } + } + result.items = Array.from(data.values()); + return result; + } + + // token match bridge token + private async getTokensMapBriageTokens(): Promise>{ + const tokens = this.tokenAddress; + const tokensMapBridgeTokens: Map = new Map; + const allTokens = await this.explorerService.getTokens(); + for (const item of allTokens) { + const l2Address = item.l2Address?.toLocaleLowerCase(); + if(tokens.includes(l2Address)){ + let tmpBridgeToken = ""; + switch(item.networkKey){ + case "ethereum" : + tmpBridgeToken = this.l1Erc20BridgeEthereum; + break; + case "arbitrum" : + tmpBridgeToken = this.l1Erc20BridgeArbitrum; + break; + } + if (tmpBridgeToken == "") { + throw new Error(`There is a unknown token : ${l2Address}`); + } + tokensMapBridgeTokens.set(l2Address, tmpBridgeToken.toLocaleLowerCase()); + } + } + return tokensMapBridgeTokens; + } +} \ No newline at end of file