diff --git a/src/app.module.ts b/src/app.module.ts index e37a182..e139c1c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -23,11 +23,12 @@ import { NovaApiService } from './nova/novaapi.service'; import { NovaService } from './nova/nova.service'; import { NovaController } from './nova/nova.controller'; import { RenzoPagingController } from './renzo/renzo.paging.controller'; -import { MagpiePagingController } from './magpie/magpie.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,8 +55,6 @@ import { WithdrawService } from './common/service/withdraw.service'; MagpieController, NovaController, RenzoPagingController, - RsethPagingController, - MagpiePagingController, NovaPagingController ], providers: [ @@ -75,7 +74,10 @@ import { WithdrawService } from './common/service/withdraw.service'; NovaApiService, NovaService, ProjectGraphService, - WithdrawService + WithdrawService, + 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 52d5df6..a8fc260 100644 --- a/src/magpie/magpie.controller.ts +++ b/src/magpie/magpie.controller.ts @@ -7,26 +7,14 @@ import { ApiOperation, ApiTags, } from '@nestjs/swagger'; -import { LRUCache } from 'lru-cache'; import { ethers } from 'ethers'; import { ParseAddressPipe } from 'src/common/pipes/parseAddress.pipe'; import { NOT_FOUND_EXCEPTION, SERVICE_EXCEPTION } from '../puffer/tokenPointsWithoutDecimals.dto'; -import { MagiePointsWithoutDecimalsDto } from 'src/magpie/magiePointsWithoutDecimalsDto.dto'; -import { ProjectService } from 'src/common/service/project.service'; -import { MagpieGraphQueryService } from 'src/magpie/magpieGraphQuery.service'; - -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 MAGPIE_ALL_POINTS_CACHE_KEY = 'allMagpiePoints'; -const MAGPIE_ALL_POINTS_WITH_BALANCE_CACHE_KEY = 'allMagpiePointsWithBalance'; -const GRAPH_QUERY_PROJECT_ID = 'magpie'; +import { MagiePointsWithoutDecimalsDto, PointsWithoutDecimalsDto } from 'src/magpie/magiePointsWithoutDecimalsDto.dto'; +import { MagpieData, MagpiePointItemWithBalance, MagpiePointItemWithoutBalance, MagpieService } from './magpie.service'; +import { PagingOptionsDto } from 'src/common/pagingOptionsDto.dto'; +import { PaginationUtil } from 'src/common/pagination.util'; +import { PagingMetaDto } from 'src/common/paging.dto'; @ApiTags('magpie') @ApiExcludeController(false) @@ -34,10 +22,7 @@ const GRAPH_QUERY_PROJECT_ID = 'magpie'; export class MagpieController { private readonly logger = new Logger(MagpieController.name); - constructor( - private projectService: ProjectService, - private magpieGraphQueryService: MagpieGraphQueryService - ) {} + constructor( private magpieService: MagpieService ) {} @Get('/points') @ApiOperation({ summary: 'Get magpie personal points' }) @@ -50,23 +35,14 @@ export class MagpieController { public async getMagpiePoints( @Query('address', new ParseAddressPipe()) address: string, ): Promise { - let finalPoints: any[], finalTotalPoints: bigint; + let pointData: MagpieData; try{ - const pointData = await this.projectService.getPoints(GRAPH_QUERY_PROJECT_ID, address); - finalPoints = pointData.finalPoints; - finalTotalPoints = pointData.finalTotalPoints; + pointData = await this.magpieService.getPointsData(address); } catch (err) { this.logger.error('Get magpie all points failed', err); return SERVICE_EXCEPTION; } - if(!finalPoints || !finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - - // Get real points. - const [eigenpiePoints, eigenLayerPoints] = this.magpieGraphQueryService.getTotalPoints(); - - return this.getReturnData(finalPoints, finalTotalPoints, eigenpiePoints, eigenLayerPoints); + return this.getReturnData(pointData.items, pointData.realTotalEigenpiePoints, pointData.realTotalEigenLayerPoints); } @Get('/all/points') @@ -87,31 +63,44 @@ export class MagpieController { public async getAllMagpiePoints(): Promise< Partial > { - const allPoints = cache.get( - MAGPIE_ALL_POINTS_CACHE_KEY, - ) as MagiePointsWithoutDecimalsDto; - if (allPoints) { - return allPoints; + let pointData: MagpieData; + try{ + pointData = await this.magpieService.getPointsDataGroupByAddress(); + } catch (err) { + this.logger.error('Get magpie all points failed', err); + return SERVICE_EXCEPTION; } + return this.getReturnData(pointData.items, pointData.realTotalEigenpiePoints, pointData.realTotalEigenLayerPoints); + } - let cacheData: MagiePointsWithoutDecimalsDto, finalPoints: any[], finalTotalPoints: bigint; + @Get('/all/points/paging') + @ApiOperation({ + summary: + 'Get magpie point for all users, point are based on user token dimension', + }) + @ApiOkResponse({ + description: "Return all users' magpie points.", + type: MagiePointsWithoutDecimalsDto, + }) + @ApiBadRequestResponse({ + description: '{ "errno": 1, "errmsg": "Service exception" }', + }) + @ApiNotFoundResponse({ + description: '{ "errno": 1, "errmsg": "not found" }', + }) + public async getPagingMagpiePoints( + @Query() pagingOptions: PagingOptionsDto + ): Promise< + Partial + > { + let pointData: MagpieData; try{ - const pointData = await this.projectService.getAllPoints(GRAPH_QUERY_PROJECT_ID); - finalPoints = pointData.finalPoints; - finalTotalPoints = pointData.finalTotalPoints; + pointData = await this.magpieService.getPointsDataGroupByAddress(); } catch (err) { this.logger.error('Get magpie all points failed', err); return SERVICE_EXCEPTION; } - if(!finalPoints || !finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - - // Get real points. - const [eigenpiePoints, eigenLayerPoints] = this.magpieGraphQueryService.getTotalPoints(); - cacheData = this.getReturnData(finalPoints, finalTotalPoints, eigenpiePoints, eigenLayerPoints); - cache.set(MAGPIE_ALL_POINTS_CACHE_KEY, cacheData); - return cacheData; + return this.getReturnData(pointData.items, pointData.realTotalEigenpiePoints, pointData.realTotalEigenLayerPoints, pagingOptions); } @Get('/all/points-with-balance') @@ -132,54 +121,93 @@ export class MagpieController { public async getAllMagpiePointsWithBalance(): Promise< Partial > { - const allPoints = cache.get( - MAGPIE_ALL_POINTS_WITH_BALANCE_CACHE_KEY, - ) as MagiePointsWithoutDecimalsDto; - if (allPoints) { - return allPoints; + let pointData: MagpieData; + try{ + pointData = await this.magpieService.getPointsData(); + } catch (err) { + this.logger.error('Get magpie all points failed', err); + return SERVICE_EXCEPTION; } + return this.getReturnData(pointData.items, pointData.realTotalEigenpiePoints, pointData.realTotalEigenLayerPoints); + } - let cacheData: MagiePointsWithoutDecimalsDto, finalPoints: any[], finalTotalPoints: bigint; + @Get('/all/points-with-balance/paging') + @ApiOperation({ + summary: + 'Get paging magpie point for all users, point are based on user token dimension', + }) + @ApiOkResponse({ + description: "Return all users' magpie points with balance.", + type: MagiePointsWithoutDecimalsDto, + }) + @ApiBadRequestResponse({ + description: '{ "errno": 1, "errmsg": "Service exception" }', + }) + @ApiNotFoundResponse({ + description: '{ "errno": 1, "errmsg": "not found" }', + }) + public async getPagingMagpiePointsWithBalance( + @Query() pagingOptions: PagingOptionsDto + ): Promise< + Partial + > { + let pointData: MagpieData; try{ - const pointData = await this.projectService.getAllPointsWithBalance(GRAPH_QUERY_PROJECT_ID); - finalPoints = pointData.finalPoints; - finalTotalPoints = pointData.finalTotalPoints; + pointData = await this.magpieService.getPointsData(); } catch (err) { this.logger.error('Get magpie all points failed', err); return SERVICE_EXCEPTION; } - if(!finalPoints || !finalTotalPoints){ - return NOT_FOUND_EXCEPTION - } - - // Get real points. - const [eigenpiePoints, eigenLayerPoints] = this.magpieGraphQueryService.getTotalPoints(); - cacheData = this.getReturnData(finalPoints, finalTotalPoints, eigenpiePoints, eigenLayerPoints); - cache.set(MAGPIE_ALL_POINTS_WITH_BALANCE_CACHE_KEY, cacheData); - return cacheData; + return this.getReturnData(pointData.items, pointData.realTotalEigenpiePoints, pointData.realTotalEigenLayerPoints, pagingOptions); } private getReturnData( - finalPoints: any[], - finnalTotalPoints: bigint, + finalPoints: MagpiePointItemWithBalance[] | MagpiePointItemWithoutBalance[], eigenpiePoints: bigint, eigenLayerPoints: bigint, + pagingOptions?: PagingOptionsDto ): MagiePointsWithoutDecimalsDto { - return { + let list = finalPoints; + let meta: PagingMetaDto; + if(undefined != pagingOptions){ + const {page = 1, limit = 100} = pagingOptions; + const paging = PaginationUtil.paginate(finalPoints, page, limit); + list = paging.items; + meta = paging.meta; + } + let res = { errno: 0, errmsg: 'no error', totals: { - eigenpiePoints: ethers.formatEther(eigenpiePoints), - eigenLayerPoints: ethers.formatEther(eigenLayerPoints), + eigenpiePoints: Number(ethers.formatEther(eigenpiePoints)).toFixed(6), + eigenLayerPoints: Number(ethers.formatEther(eigenLayerPoints)).toFixed(6), }, - data: finalPoints.map(point => { - const tmpPoints = point.points; - point.points = { - eigenpiePoints: ethers.formatEther(this.projectService.getRealPoints(tmpPoints, finnalTotalPoints, eigenpiePoints)), - eigenLayerPoints: ethers.formatEther(this.projectService.getRealPoints(tmpPoints, finnalTotalPoints, eigenLayerPoints)), - }; - return point; + data: list.map(item => { + return item.tokenAddress ? + { + address: item.address, + tokenAddress: item.tokenAddress, + balance: Number(ethers.formatEther(item.balance)).toFixed(6), + points:{ + eigenpiePoints: Number(ethers.formatEther(item.realEigenpiePoints)).toFixed(6), + eigenLayerPoints: Number(ethers.formatEther(item.realEigenLayerPoints)).toFixed(6) + }, + updated_at: item.updatedAt + } as PointsWithoutDecimalsDto + : + { + address: item.address, + points:{ + eigenpiePoints: Number(ethers.formatEther(item.realEigenpiePoints)).toFixed(6), + eigenLayerPoints: Number(ethers.formatEther(item.realEigenLayerPoints)).toFixed(6) + }, + updated_at: item.updatedAt + } as PointsWithoutDecimalsDto; }) - } as MagiePointsWithoutDecimalsDto; + }; + if(meta){ + res["meta"] = meta; + } + return res as MagiePointsWithoutDecimalsDto; } -} +} \ No newline at end of file diff --git a/src/magpie/magpie.paging.controller.ts b/src/magpie/magpie.paging.controller.ts deleted file mode 100644 index e541091..0000000 --- a/src/magpie/magpie.paging.controller.ts +++ /dev/null @@ -1,186 +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 { MagiePointsWithoutDecimalsDto } from 'src/magpie/magiePointsWithoutDecimalsDto.dto'; -import { ProjectService } from 'src/common/service/project.service'; -import { MagpieGraphQueryService } from 'src/magpie/magpieGraphQuery.service'; -import { PointData } from 'src/common/service/project.service'; -import { PagingMetaDto } from 'src/common/paging.dto'; -import { PagingOptionsDto } from 'src/common/pagingOptionsDto.dto'; -import { PaginationUtil } from 'src/common/pagination.util'; -import { NOT_FOUND_EXCEPTION, SERVICE_EXCEPTION } 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 MAGPIE_ALL_POINTS_CACHE_KEY = 'allMagpiePointsData'; -const MAGPIE_ALL_POINTS_WITH_BALANCE_CACHE_KEY = 'allMagpiePointsWithBalanceData'; -const GRAPH_QUERY_PROJECT_ID = 'magpie'; - -@ApiTags('magpie') -@ApiExcludeController(false) -@Controller('magpie') -export class MagpiePagingController { - private readonly logger = new Logger(MagpiePagingController.name); - - constructor( - private projectService: ProjectService, - private magpieGraphQueryService: MagpieGraphQueryService - ) {} - - @Get('/points/paging') - @ApiOperation({ summary: 'Get paginated magpie personal points' }) - @ApiBadRequestResponse({ - description: '{ "errno": 1, "errmsg": "Service exception" }', - }) - @ApiNotFoundResponse({ - description: '{ "errno": 1, "errmsg": "not found" }', - }) - public async getMagpiePoints( - @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 magpie all points failed', err.stack); - return SERVICE_EXCEPTION; - } - - // Get real points. - const [eigenpiePoints, eigenLayerPoints] = this.magpieGraphQueryService.getTotalPoints(); - return this.getReturnData(pointData, pagingOptions, eigenpiePoints, eigenLayerPoints); - } - - @Get('/all/points/paging') - @ApiOperation({ - summary: - 'Get paginated magpie point for all users, point are based on user token dimension', - }) - @ApiOkResponse({ - description: "Return all users' magpie points.", - type: MagiePointsWithoutDecimalsDto, - }) - @ApiBadRequestResponse({ - description: '{ "errno": 1, "errmsg": "Service exception" }', - }) - @ApiNotFoundResponse({ - description: '{ "errno": 1, "errmsg": "not found" }', - }) - public async getAllMagpiePoints( - @Query() pagingOptions: PagingOptionsDto - ): Promise< - Partial - > { - let pointData = cache.get(MAGPIE_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(MAGPIE_ALL_POINTS_CACHE_KEY, pointData); - } catch (err) { - this.logger.error('Get magpie all points failed', err.stack); - return SERVICE_EXCEPTION; - } - } - - // Get real points. - const [eigenpiePoints, eigenLayerPoints] = this.magpieGraphQueryService.getTotalPoints(); - return this.getReturnData(pointData, pagingOptions, eigenpiePoints, eigenLayerPoints); - } - - @Get('/all/points-with-balance/paging') - @ApiOperation({ - summary: - 'Get paginated magpie point for all users, point are based on user token dimension', - }) - @ApiOkResponse({ - description: "Return all users' magpie points with balance.", - type: MagiePointsWithoutDecimalsDto, - }) - @ApiBadRequestResponse({ - description: '{ "errno": 1, "errmsg": "Service exception" }', - }) - @ApiNotFoundResponse({ - description: '{ "errno": 1, "errmsg": "not found" }', - }) - public async getAllMagpiePointsWithBalance( - @Query() pagingOptions: PagingOptionsDto - ): Promise< - Partial - > { - let pointData = cache.get(MAGPIE_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(MAGPIE_ALL_POINTS_WITH_BALANCE_CACHE_KEY, pointData); - } catch (err) { - this.logger.error('Get magpie all points failed', err.stack); - return SERVICE_EXCEPTION; - } - } - - // Get real points. - const [eigenpiePoints, eigenLayerPoints] = this.magpieGraphQueryService.getTotalPoints(); - return this.getReturnData(pointData, pagingOptions, eigenpiePoints, eigenLayerPoints); - } - - private getReturnData( - pointData: PointData, - pagingOptions: PagingOptionsDto, - eigenpiePoints: bigint, - eigenLayerPoints: bigint - ): MagiePointsWithoutDecimalsDto { - let list = pointData.finalPoints; - let meta: PagingMetaDto; - if(null != pagingOptions){ - const {page = 1, limit = 100} = pagingOptions; - const paging = PaginationUtil.paginate(pointData.finalPoints, page, limit); - list = paging.items; - meta = paging.meta; - } - - return { - errno: 0, - errmsg: 'no error', - totals: { - eigenpiePoints: ethers.formatEther(eigenpiePoints), - eigenLayerPoints: ethers.formatEther(eigenLayerPoints), - }, - meta: meta, - data: list.map(point => { - let tmpPoint = { ...point }; - const tmpPoints = tmpPoint.points; - tmpPoint.points = { - eigenpiePoints: ethers.formatEther(this.projectService.getRealPoints(tmpPoints, pointData.finalTotalPoints, eigenpiePoints)), - eigenLayerPoints: ethers.formatEther(this.projectService.getRealPoints(tmpPoints, pointData.finalTotalPoints, eigenLayerPoints)), - }; - return tmpPoint; - }) - } as MagiePointsWithoutDecimalsDto; - } -} diff --git a/src/magpie/magpie.service.ts b/src/magpie/magpie.service.ts new file mode 100644 index 0000000..1174b72 --- /dev/null +++ b/src/magpie/magpie.service.ts @@ -0,0 +1,142 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { cloneDeep } from 'lodash'; +import { LocalPointData, ProjectGraphService } from 'src/common/service/projectGraph.service'; +import { MagpieGraphQueryService, MagpieGraphTotalPoint } from './magpieGraphQuery.service'; + +export interface MagpiePointItemWithBalance{ + address: string, + tokenAddress: string, + balance: bigint, + localPoints: bigint, + localTotalPointsPerToken: bigint, + realEigenpiePoints: bigint, + realEigenLayerPoints: bigint, + realTotalEigenpiePointsPerToken: bigint, + realTotalEigenLayerPointsPerToken: bigint, + updatedAt: number +} + +export interface MagpiePointItemWithoutBalance{ + address: string, + realEigenpiePoints: bigint, + realEigenLayerPoints: bigint, + updatedAt: number +} + +export interface MagpieData{ + localTotalPoints: bigint, + realTotalEigenpiePoints: bigint, + realTotalEigenLayerPoints: bigint, + items: MagpiePointItemWithBalance[] | MagpiePointItemWithoutBalance[] +} + +@Injectable() +export class MagpieService{ + private readonly projectName: string = "magpie"; + private readonly logger: Logger; + + private magpieData: MagpieData = {localTotalPoints: 0n, realTotalEigenpiePoints: 0n, realTotalEigenLayerPoints: 0n, items:[]}; + + public constructor( + private readonly projectGraphService: ProjectGraphService, + private readonly magpieGraphQueryService: MagpieGraphQueryService + ) { + this.logger = new Logger(MagpieService.name); + } + + public async onModuleInit(){ + this.logger.log(`Init ${MagpieService.name} onmoduleinit`); + const func = async () => { + try { + await this.loadPointsData(); + } catch (err) { + this.logger.error(`${MagpieService.name} init failed.`, err.stack); + } + }; + func(); + setInterval(func, 1000 * 60); + } + + // load points data + public async loadPointsData(){ + const realTotalPointsData = await this.getRealPointsData(); + const localPointsData = await this.getLocalPointsData(); + const localPoints = localPointsData.localPoints; + const localTotalPoints = localPointsData.localTotalPoints; + + let data: MagpiePointItemWithBalance[] = []; + for (const item of localPoints) { + const realEigenpiePoints = BigInt(item.points) * BigInt(realTotalPointsData.eigenpiePoints) / BigInt(localTotalPoints); + const realEigenLayerPoints = BigInt(item.points) * BigInt(realTotalPointsData.eigenLayerPoints) / BigInt(localTotalPoints); + const pointsItem: MagpiePointItemWithBalance = { + address: item.address, + tokenAddress: item.token, + balance: item.balance, + localPoints: item.points, + localTotalPointsPerToken: item.totalPointsPerToken, + realEigenpiePoints: realEigenpiePoints, + realEigenLayerPoints: realEigenLayerPoints, + realTotalEigenpiePointsPerToken: realTotalPointsData.eigenpiePoints, + realTotalEigenLayerPointsPerToken: realTotalPointsData.eigenLayerPoints, + updatedAt: item.updatedAt + }; + data.push(pointsItem); + } + if(data.length > 0){ + this.magpieData = { + localTotalPoints: localTotalPoints, + realTotalEigenpiePoints: realTotalPointsData.eigenpiePoints, + realTotalEigenLayerPoints: realTotalPointsData.eigenLayerPoints, + items: data + }; + }else{ + this.logger.log(`Load ${this.projectName} data empty.`); + } + } + + // return points data + public getPointsData(address?: string): MagpieData { + let result: MagpieData = cloneDeep(this.magpieData); + if(address){ + const _address = address.toLocaleLowerCase(); + result.items = this.magpieData.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.magpieGraphQueryService.getRealData(); + } + + // return real points group by address + public getPointsDataGroupByAddress(): MagpieData{ + let result: MagpieData = cloneDeep(this.magpieData); + let data: Map = new Map; + const now = (new Date().getTime() / 1000) | 0; + for (let i = 0; i < this.magpieData.items.length; i++) { + const item = this.magpieData.items[i]; + if(!data.has(item.address)){ + data.set(item.address, { + address: item.address, + realEigenpiePoints: item.realEigenpiePoints, + realEigenLayerPoints: item.realEigenLayerPoints, + updatedAt: now + } as MagpiePointItemWithoutBalance); + }else{ + const tmpItem = data.get(item.address); + tmpItem.realEigenLayerPoints += item.realEigenLayerPoints; + tmpItem.realEigenpiePoints += item.realEigenpiePoints; + } + } + result.items = Array.from(data.values()); + return result; + } +} \ No newline at end of file diff --git a/src/magpie/magpieGraphQuery.service.ts b/src/magpie/magpieGraphQuery.service.ts index 81c0dfd..9c835d9 100644 --- a/src/magpie/magpieGraphQuery.service.ts +++ b/src/magpie/magpieGraphQuery.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; export interface MagpieGraphTotalPoint { @@ -8,13 +8,10 @@ export interface MagpieGraphTotalPoint { } @Injectable() -export class MagpieGraphQueryService implements OnModuleInit { +export class MagpieGraphQueryService { private readonly logger: Logger; private readonly magpiePointRedistributeGraphApi: string = "https://gateway-arbitrum.network.thegraph.com/api/db54be382da0a4d60b8ea908242dda0c/subgraphs/id/F2wKriMMFuc8sMtFxYd3Kew46DHvBGfGdTvhvvNAWg8x"; private readonly magpieUserinfoId: string; - - private eigenpiePoints: bigint = BigInt(0); - private eigenLayerPoints: bigint = BigInt(0); public constructor(configService: ConfigService) { this.logger = new Logger(MagpieGraphQueryService.name); @@ -23,57 +20,35 @@ export class MagpieGraphQueryService implements OnModuleInit { ); } - public async onModuleInit() { - // setInterval will wait for 100s, so it's necessary to execute the loadMagpieData function once first. - const func = async () => { - try { - await this.loadMagpieData(); - } catch (err) { - this.logger.error("MagpieGraphQueryService init failed", err.stack); - } - }; - await func(); - setInterval(func, 1000 * 300); - } - - public async loadMagpieData() { - this.logger.log('loadMagpieData has been load.'); + public async getRealData():Promise { const query = ` -{ - userInfo(id:"${this.magpieUserinfoId}") { - id - eigenpiePoints - eigenLayerPoints - } -} + { + userInfo(id:"${this.magpieUserinfoId}") { + id + eigenpiePoints + eigenLayerPoints + } + } `; const data = await this.query(query); if (data && data.data && data.data.userInfo) { - const magpieGraphTotalPoint = data.data.userInfo as MagpieGraphTotalPoint; - this.eigenpiePoints = magpieGraphTotalPoint.eigenpiePoints; - this.eigenLayerPoints = magpieGraphTotalPoint.eigenLayerPoints; + return data.data.userInfo as MagpieGraphTotalPoint; } else { // Exception in fetching Magpie GraphQL data. this.logger.error("Exception in fetching Magpie GraphQL data."); } } - public getTotalPoints(): [bigint, bigint] { - return [this.eigenpiePoints, this.eigenLayerPoints]; - } - private async query(query: string) { try{ const body = { query: query, }; - const response = await fetch(this.magpiePointRedistributeGraphApi, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); - const data = await response.json(); return data; } catch (err) { @@ -81,4 +56,4 @@ export class MagpieGraphQueryService implements OnModuleInit { return undefined; } } -} +} \ No newline at end of file diff --git a/src/nova/nova.service.ts b/src/nova/nova.service.ts index ad9834e..c7c502e 100644 --- a/src/nova/nova.service.ts +++ b/src/nova/nova.service.ts @@ -58,8 +58,8 @@ export class NovaService { this.projectName ); - let tempProjectIdGraphTotalPoint: Map = new Map; - let tempProjectIdTotalPoints: Map = new Map; + let tempProjectIdGraphTotalPoint: Map = new Map; + let tempProjectIdTotalPoints: Map = new Map; let finalTotalPoints: bigint = BigInt(0); for (const item of totalPoints) { const projectArr = item.project.split('-'); 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