From 507775a2786d3883642370a8fc9d9227494ca3b2 Mon Sep 17 00:00:00 2001 From: "yevhen.burkovskyi" Date: Tue, 14 May 2024 16:42:05 +0300 Subject: [PATCH 1/4] feat: added info for single validator page --- .env.example | 8 +- src/core/config/config.dto.ts | 6 + src/core/config/config.schema.ts | 2 + src/core/config/config.ts | 4 + src/core/lib/keybase/keybase.service.ts | 59 ++++++++ .../lib/keybase/responses/failed.response.ts | 10 ++ .../generic-success-failed.response.ts | 3 + .../keybase/responses/user-lookup.response.ts | 17 +++ src/core/lib/okp4/enums/endpoints.enum.ts | 1 + src/core/lib/okp4/enums/route-param.enum.ts | 1 + src/core/lib/okp4/okp4.service.ts | 21 +++ .../validator-delegations.response.ts | 15 ++ .../dtos/my-validator-delegation.dto.ts | 4 + .../staking/dtos/validator-delegations.dto.ts | 5 + .../staking/dtos/validators-view.dto.ts | 16 +- src/modules/staking/enums/query-param.enum.ts | 1 + .../enums/staking-cache-prefix.enum.ts | 6 + .../staking/enums/staking-endpoints.enum.ts | 2 + .../schemas/my-validator-delegation.schema.ts | 6 + .../schemas/validator-delegations.schema.ts | 10 ++ src/modules/staking/services/staking.cache.ts | 47 ++++-- .../staking/services/staking.service.ts | 142 +++++++++++++++--- src/modules/staking/staking.controller.ts | 20 +++ src/modules/staking/staking.module.ts | 2 + 24 files changed, 377 insertions(+), 31 deletions(-) create mode 100644 src/core/lib/keybase/keybase.service.ts create mode 100644 src/core/lib/keybase/responses/failed.response.ts create mode 100644 src/core/lib/keybase/responses/generic-success-failed.response.ts create mode 100644 src/core/lib/keybase/responses/user-lookup.response.ts create mode 100644 src/core/lib/okp4/responses/validator-delegations.response.ts create mode 100644 src/modules/staking/dtos/my-validator-delegation.dto.ts create mode 100644 src/modules/staking/dtos/validator-delegations.dto.ts create mode 100644 src/modules/staking/enums/staking-cache-prefix.enum.ts create mode 100644 src/modules/staking/schemas/my-validator-delegation.schema.ts create mode 100644 src/modules/staking/schemas/validator-delegations.schema.ts diff --git a/.env.example b/.env.example index 5b03892..5a4d529 100644 --- a/.env.example +++ b/.env.example @@ -19,5 +19,9 @@ REDIS_HOST=localhost REDIS_PORT=6379 # cache -MY_STAKING_OVERVIEW=120000 #2 mins -GLOBAL_STAKING_OVERVIEW=120000 #2 mins \ No newline at end of file +MY_STAKING_OVERVIEW=120 #2 mins +GLOBAL_STAKING_OVERVIEW=120 #2 mins +STAKING_VALIDATORS=120 #2 mins + +#Keybase +KEYBASE_URL=https://keybase.io/_/api/1.0 \ No newline at end of file diff --git a/src/core/config/config.dto.ts b/src/core/config/config.dto.ts index 1fc42bc..62529d6 100644 --- a/src/core/config/config.dto.ts +++ b/src/core/config/config.dto.ts @@ -4,6 +4,7 @@ export interface ConfigDto { okp4: Okp4Config; redis: RedisConfig; cache: CacheConfig; + keybase: KeybaseConfig; } export interface AppConfig { @@ -31,4 +32,9 @@ export interface CacheConfig { myStakingOverview: number; globalStakingOverview: number; validators: number; + validatorDelegation: number; +} + +export interface KeybaseConfig { + url: string; } \ No newline at end of file diff --git a/src/core/config/config.schema.ts b/src/core/config/config.schema.ts index cce6de9..bdc7582 100644 --- a/src/core/config/config.schema.ts +++ b/src/core/config/config.schema.ts @@ -14,4 +14,6 @@ export const ConfigSchema = Joi.object({ MY_STAKING_OVERVIEW: Joi.number().required(), GLOBAL_STAKING_OVERVIEW: Joi.number().required(), STAKING_VALIDATORS: Joi.number().required(), + STAKING_VALIDATOR_DELEGATION: Joi.number().required(), + KEYBASE_URL: Joi.string().required(), }).required(); diff --git a/src/core/config/config.ts b/src/core/config/config.ts index 96ce482..d3bd777 100644 --- a/src/core/config/config.ts +++ b/src/core/config/config.ts @@ -22,5 +22,9 @@ export const config: ConfigDto = { myStakingOverview: +process.env.MY_STAKING_OVERVIEW!, globalStakingOverview: +process.env.GLOBAL_STAKING_OVERVIEW!, validators: +process.env.STAKING_VALIDATORS!, + validatorDelegation: +process.env.STAKING_VALIDATOR_DELEGATION!, + }, + keybase: { + url: process.env.KEYBASE_URL!, } }; diff --git a/src/core/lib/keybase/keybase.service.ts b/src/core/lib/keybase/keybase.service.ts new file mode 100644 index 0000000..88fb008 --- /dev/null +++ b/src/core/lib/keybase/keybase.service.ts @@ -0,0 +1,59 @@ +import { BadRequestException, Injectable } from "@nestjs/common"; +import { HttpService } from "../http.service"; +import { config } from "@core/config/config"; +import { GSFResponse } from "./responses/generic-success-failed.response"; +import { FailedResponse } from "./responses/failed.response"; +import { createUrlParams } from "@utils/create-url-params"; +import { UserLookupResponse } from "./responses/user-lookup.response"; + +@Injectable() +export class KeybaseService { + private BASE_URL = config.keybase.url; + + constructor(private readonly httpService: HttpService) {} + + private constructUrl(endpoint: string, params?: string): string { + return `${this.BASE_URL}/${endpoint}${params ? `?${params}` : ''}`; + } + + private getWithErrorHandling(url: string): Promise { + return this.errorHandleWrapper( + this.httpService.get.bind( + null, + url, + ), + ); + } + + async getUserLookup(key: string): Promise { + return this.getWithErrorHandling( + this.constructUrl( + 'user/lookup.json', + createUrlParams({ + fields: 'pictures', + key_suffix: key + }) + ) + ) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async errorHandleWrapper(fn: any): Promise { + try { + const response: GSFResponse = await fn(); + + if (this.isFailedResponse(response)) { + throw new BadRequestException(response.status.name); + } + + return response as T; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + throw new BadRequestException(e.message); + } + } + + private isFailedResponse(response: GSFResponse): response is FailedResponse { + return (response as FailedResponse).status.code !== 0; + } +} \ No newline at end of file diff --git a/src/core/lib/keybase/responses/failed.response.ts b/src/core/lib/keybase/responses/failed.response.ts new file mode 100644 index 0000000..928a6cf --- /dev/null +++ b/src/core/lib/keybase/responses/failed.response.ts @@ -0,0 +1,10 @@ +export interface FailedResponse { + status: { + code: number; + desc: string; + fields: { + key_suffix: string; + }; + name: string; + } +} \ No newline at end of file diff --git a/src/core/lib/keybase/responses/generic-success-failed.response.ts b/src/core/lib/keybase/responses/generic-success-failed.response.ts new file mode 100644 index 0000000..47a3b20 --- /dev/null +++ b/src/core/lib/keybase/responses/generic-success-failed.response.ts @@ -0,0 +1,3 @@ +import { FailedResponse } from './failed.response'; + +export type GSFResponse = T | FailedResponse; // Generic Success / Failed Response for Osmosis diff --git a/src/core/lib/keybase/responses/user-lookup.response.ts b/src/core/lib/keybase/responses/user-lookup.response.ts new file mode 100644 index 0000000..94639e0 --- /dev/null +++ b/src/core/lib/keybase/responses/user-lookup.response.ts @@ -0,0 +1,17 @@ +export interface UserLookupResponse { + status: { + code: number; + name: string; + }, + them: [ + { + id: string; + pictures: { + primary: { + url: string; + source: string; + } + } + } + ] +} \ No newline at end of file diff --git a/src/core/lib/okp4/enums/endpoints.enum.ts b/src/core/lib/okp4/enums/endpoints.enum.ts index c653f69..a6a81a0 100644 --- a/src/core/lib/okp4/enums/endpoints.enum.ts +++ b/src/core/lib/okp4/enums/endpoints.enum.ts @@ -6,4 +6,5 @@ export enum Endpoints { SPENDABLE_BALANCE = 'cosmos/bank/v1beta1/spendable_balances', VALIDATORS = 'cosmos/staking/v1beta1/validators', TOTAL_SUPPLY = 'cosmos/bank/v1beta1/supply', + VALIDATO_DELEGATIONS = 'cosmos/staking/v1beta1/validators/:validator_addr/delegations', } diff --git a/src/core/lib/okp4/enums/route-param.enum.ts b/src/core/lib/okp4/enums/route-param.enum.ts index 7688551..f8e1f96 100644 --- a/src/core/lib/okp4/enums/route-param.enum.ts +++ b/src/core/lib/okp4/enums/route-param.enum.ts @@ -1,3 +1,4 @@ export enum RouteParam { DELEGATOR_ADDRES = ':delegator_addr', + VALIDATOR_ADDRES = ':validator_addr', } \ No newline at end of file diff --git a/src/core/lib/okp4/okp4.service.ts b/src/core/lib/okp4/okp4.service.ts index 434bba9..9d69ad1 100644 --- a/src/core/lib/okp4/okp4.service.ts +++ b/src/core/lib/okp4/okp4.service.ts @@ -13,6 +13,7 @@ import { DelegatorsRewardsResponse } from "./responses/delegators-rewards.respon import { SpendableBalancesResponse } from "./responses/spendable-balances.response"; import { SupplyResponse } from "./responses/supply.response"; import { ValidatorStatus } from "./enums/validator-status.enum"; +import { ValidatorDelegationsResponse } from "./responses/validator-delegations.response"; @Injectable() export class Okp4Service { @@ -95,6 +96,26 @@ export class Okp4Service { return this.getWithErrorHandling(url); } + async getValidatorDelegations(validatorAddr: string, limit?: number, offset?: number): Promise { + let params = undefined; + if (limit && offset) { + params = createUrlParams({ + 'pagination.offset': offset.toString(), + 'pagination.limit': limit.toString(), + 'pagination.count_total': true.toString() + }) + } + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.VALIDATO_DELEGATIONS.replace( + RouteParam.VALIDATOR_ADDRES, + validatorAddr, + ), + params + ) + ); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private async errorHandleWrapper(fn: any): Promise { try { diff --git a/src/core/lib/okp4/responses/validator-delegations.response.ts b/src/core/lib/okp4/responses/validator-delegations.response.ts new file mode 100644 index 0000000..a294f48 --- /dev/null +++ b/src/core/lib/okp4/responses/validator-delegations.response.ts @@ -0,0 +1,15 @@ +import { WithPaginationResponse } from "./with-pagination.response"; + +export type ValidatorDelegationsResponse = WithPaginationResponse<{ delegation_responses: Delegation[] }>; + +export interface Delegation { + delegation: { + delegator_address: string; + validator_address: string; + shares: string + }; + balance: { + denom: string; + amount: string; + } +} diff --git a/src/modules/staking/dtos/my-validator-delegation.dto.ts b/src/modules/staking/dtos/my-validator-delegation.dto.ts new file mode 100644 index 0000000..c3690a5 --- /dev/null +++ b/src/modules/staking/dtos/my-validator-delegation.dto.ts @@ -0,0 +1,4 @@ +export interface MyValidatorDelegationDto { + address: string; + validatorAddress: string; +} \ No newline at end of file diff --git a/src/modules/staking/dtos/validator-delegations.dto.ts b/src/modules/staking/dtos/validator-delegations.dto.ts new file mode 100644 index 0000000..eab8921 --- /dev/null +++ b/src/modules/staking/dtos/validator-delegations.dto.ts @@ -0,0 +1,5 @@ +export interface ValidatorDelegationsDto { + address: string; + limit?: number; + offset?: number; +} \ No newline at end of file diff --git a/src/modules/staking/dtos/validators-view.dto.ts b/src/modules/staking/dtos/validators-view.dto.ts index 4a43298..96afd98 100644 --- a/src/modules/staking/dtos/validators-view.dto.ts +++ b/src/modules/staking/dtos/validators-view.dto.ts @@ -1,8 +1,20 @@ export interface ValidatorsViewDto { + logo: string; + description: { + moniker: string; + identity: string; + website: string; + securityContact: string; + details: string; + }; + commission: { + rate: string; + maxRate: string; + maxChangeRate: string; + updateTime: string; + }; address: string; - name: string; status: string; jailed: boolean; stakedAmount: string; - commission: string; } \ No newline at end of file diff --git a/src/modules/staking/enums/query-param.enum.ts b/src/modules/staking/enums/query-param.enum.ts index 826012f..69871f6 100644 --- a/src/modules/staking/enums/query-param.enum.ts +++ b/src/modules/staking/enums/query-param.enum.ts @@ -1,3 +1,4 @@ export enum QueryParam { ADDRESS = 'address', + VALIDATOR_ADDRESS = 'validatorAddress', } diff --git a/src/modules/staking/enums/staking-cache-prefix.enum.ts b/src/modules/staking/enums/staking-cache-prefix.enum.ts new file mode 100644 index 0000000..5cdf833 --- /dev/null +++ b/src/modules/staking/enums/staking-cache-prefix.enum.ts @@ -0,0 +1,6 @@ +export enum StakingCachePrefix { + STAKING = 'staking', + GLOBAL_OVERVIEW = 'global_overview', + VALIDATORS = 'validators', + VALIDATOR_IMG = 'validator_img', +} \ No newline at end of file diff --git a/src/modules/staking/enums/staking-endpoints.enum.ts b/src/modules/staking/enums/staking-endpoints.enum.ts index 658d053..3772bc5 100644 --- a/src/modules/staking/enums/staking-endpoints.enum.ts +++ b/src/modules/staking/enums/staking-endpoints.enum.ts @@ -1,5 +1,7 @@ export enum StakingEndpoints { MY_OVERVIEW = '/my/overview', + MY_VALIDATOR_DELEGATION = '/my/validator-delegation', + VALIDATOR_DELEGATIONS = '/validator-delegations', OVERVIEW = '/overview', VALIDATORS = '/validators', } \ No newline at end of file diff --git a/src/modules/staking/schemas/my-validator-delegation.schema.ts b/src/modules/staking/schemas/my-validator-delegation.schema.ts new file mode 100644 index 0000000..f2aeb11 --- /dev/null +++ b/src/modules/staking/schemas/my-validator-delegation.schema.ts @@ -0,0 +1,6 @@ +import * as Joi from 'joi'; + +export const MyValidatorDelegationSchema = Joi.object({ + address: Joi.string().required(), + validatorAddress: Joi.string().required(), +}).required() \ No newline at end of file diff --git a/src/modules/staking/schemas/validator-delegations.schema.ts b/src/modules/staking/schemas/validator-delegations.schema.ts new file mode 100644 index 0000000..392f93e --- /dev/null +++ b/src/modules/staking/schemas/validator-delegations.schema.ts @@ -0,0 +1,10 @@ +import * as Joi from 'joi'; + +export const ValidatorDelegationsSchema = Joi.object({ + address: Joi.string().required(), + limit: Joi.number().optional(), + offset: Joi.number().optional(), +}) + .keys() + .and('limit', 'offset') + .required(); \ No newline at end of file diff --git a/src/modules/staking/services/staking.cache.ts b/src/modules/staking/services/staking.cache.ts index 9e3d458..2795add 100644 --- a/src/modules/staking/services/staking.cache.ts +++ b/src/modules/staking/services/staking.cache.ts @@ -1,14 +1,12 @@ import { config } from "@core/config/config"; import { CACHE_MANAGER } from "@nestjs/cache-manager"; import { Inject, Injectable } from "@nestjs/common"; +import { createHash } from 'crypto'; import { Cache } from 'cache-manager'; +import { StakingCachePrefix } from "../enums/staking-cache-prefix.enum"; @Injectable() export class StakingCache { - private redisStakingPrefix = 'staking'; - private globalOverviewPrefix = 'global_overview'; - private validatorsPrefix = 'validators'; - constructor( @Inject(CACHE_MANAGER) private cacheService: Cache, ) { } @@ -34,23 +32,52 @@ export class StakingCache { async setGlobalStakedOverview(data: unknown) { const serialized = JSON.stringify(data); - await this.cacheService.set(this.createRedisKey(this.globalOverviewPrefix), serialized, config.cache.globalStakingOverview); + await this.cacheService.set(this.createRedisKey(StakingCachePrefix.GLOBAL_OVERVIEW), serialized, config.cache.globalStakingOverview); } async getGlobalStakedOverview() { - return this.getObjByRedisKey(this.createRedisKey(this.globalOverviewPrefix)); + return this.getObjByRedisKey(this.createRedisKey(StakingCachePrefix.GLOBAL_OVERVIEW)); } async setValidators(validators: unknown[]) { const serialized = JSON.stringify(validators); - await this.cacheService.set(this.createRedisKey(this.validatorsPrefix), serialized, config.cache.validators); + await this.cacheService.set(this.createRedisKey(StakingCachePrefix.VALIDATORS), serialized, config.cache.validators); } async getValidators() { - return this.getObjByRedisKey(this.createRedisKey(this.validatorsPrefix)); + return this.getObjByRedisKey(this.createRedisKey(StakingCachePrefix.VALIDATORS)); + } + + async setValidatorDelegation(address: string, validatorAddress: string, data: unknown) { + const serialized = JSON.stringify(data); + const hash = this.createValidatorDelegationHash(address, validatorAddress); + await this.cacheService.set( + this.createRedisKey(hash), + serialized, + config.cache.validators + ); + } + + async getValidatorDelegation(address: string, validatorAddress: string) { + const hash = this.createValidatorDelegationHash(address, validatorAddress); + return this.getObjByRedisKey(this.createRedisKey(hash)); + } + + private createValidatorDelegationHash(address: string, validatorAddress: string) { + const hash = createHash('sha256'); + hash.update(`${address}_${validatorAddress}`); + return hash.digest('hex'); + } + + async setValidatorImg(id: string, imgUrl: string) { + this.cacheService.set(this.createRedisKey(StakingCachePrefix.VALIDATOR_IMG, id), imgUrl); + } + + async getValidatorImg(id: string) { + return this.cacheService.get(this.createRedisKey(StakingCachePrefix.VALIDATOR_IMG, id)); } - private createRedisKey(id: string) { - return `${this.redisStakingPrefix}_${id}`; + private createRedisKey(...ids: string[]) { + return ids.reduce((acc, id) => acc + `_${id}`, `${StakingCachePrefix.STAKING}`); } } \ No newline at end of file diff --git a/src/modules/staking/services/staking.service.ts b/src/modules/staking/services/staking.service.ts index bd863f8..bcab2d3 100644 --- a/src/modules/staking/services/staking.service.ts +++ b/src/modules/staking/services/staking.service.ts @@ -1,24 +1,39 @@ import { Okp4Service } from "@core/lib/okp4/okp4.service"; -import { Injectable } from "@nestjs/common"; +import { Injectable, OnModuleInit } from "@nestjs/common"; import { StakingCache } from "./staking.cache"; import { config } from "@core/config/config"; import { MyStakedOverviewDto } from "../dtos/my-staked-overview.dto"; import { OsmosisService } from "@core/lib/osmosis/osmosis.service"; -import { DelegatorValidatorsResponse, Validator } from "@core/lib/okp4/responses/delegators-validators.response"; +import { Validator } from "@core/lib/okp4/responses/delegators-validators.response"; import Big from "big.js"; import { GlobalStakedOverviewDto } from "../dtos/global-staked-overview.dto"; import { ValidatorStatus } from "@core/lib/okp4/enums/validator-status.enum"; import { ValidatorStatusView } from "../enums/validator-status-view.enum"; import { ValidatorsViewDto } from "../dtos/validators-view.dto"; +import { MyValidatorDelegationDto } from "../dtos/my-validator-delegation.dto"; +import { Delegation } from "@core/lib/okp4/responses/validator-delegations.response"; +import { ValidatorDelegationsDto } from "../dtos/validator-delegations.dto"; +import { KeybaseService } from "@core/lib/keybase/keybase.service"; +import { Log } from "@core/loggers/log"; +import { UserLookupResponse } from "@core/lib/keybase/responses/user-lookup.response"; @Injectable() -export class StakingService { +export class StakingService implements OnModuleInit { constructor( private readonly okp4Service: Okp4Service, private readonly cache: StakingCache, private readonly osmosisService: OsmosisService, + private readonly keybaseService: KeybaseService, ) { } + + async onModuleInit() { + try { + await this.loadAndCacheValidatorImages(); + } catch (e) { + Log.warn("Some of images failed to load"); + } + } async getMyStakedOverview(address: string) { const cache = await this.cache.getMyStakedOverview(address); @@ -48,9 +63,15 @@ export class StakingService { return myStakedOverviewDto; } - private async fetchMyStakedAmount(address: string) { + private async fetchMyStakedAmount(address: string, validatorAddress?: string) { const res = await this.okp4Service.getDelegations(address); - return res.delegation_responses.reduce((acc, val) => acc + +val.balance.amount, 0).toString(); + return res.delegation_responses.reduce((acc, val) => { + if (validatorAddress && val.delegation.validator_address !== validatorAddress) { + return acc; + } + + return acc + +val.balance.amount; + }, 0).toString(); } private async fetchDelegatorsValidatorsAmount(address: string) { @@ -58,9 +79,14 @@ export class StakingService { return res.pagination.total; } - private async fetchDelegatorsRewards(address: string) { + private async fetchDelegatorsRewards(address: string, validatorAddress?: string) { const res = await this.okp4Service.getDelegatorsRewards(address); - return res.total.find(({denom}) => config.app.tokenDenom === denom)?.amount || '0'; + return res.rewards.reduce((acc, val) => { + if (validatorAddress && val.validator_address !== validatorAddress) { + return acc; + } + return acc + +(val.reward.find(({ denom }) => config.app.tokenDenom === denom)?.amount || 0) + }, 0).toString(); } private async fetchAvailableBalance(address: string) { @@ -107,7 +133,7 @@ export class StakingService { private async fetchTotalSupply() { const res = await this.okp4Service.getTotalSupply(); return res.supply.find(({ denom }) => denom === config.app.tokenDenom); - } + } async getValidators() { const cache = await this.cache.getValidators(); @@ -121,20 +147,102 @@ export class StakingService { private async fetchAndCacheValidators() { const res = await this.okp4Service.getValidators(); - const formattedValidators = this.validatorsView(res); + const formattedValidators = await this.validatorsView(res.validators); await this.cache.setValidators(formattedValidators); return formattedValidators; } - private validatorsView(toView: DelegatorValidatorsResponse): ValidatorsViewDto[] { - return toView.validators.map((validator) => ({ - address: validator.operator_address, - name: validator.description.moniker, - status: validator.status === ValidatorStatus.BONDED ? ValidatorStatusView.BONDED : ValidatorStatusView.UN_BONDED, - jailed: validator.jailed, - stakedAmount: validator.delegator_shares, - commission: validator.commission.commission_rates.rate + private async validatorsView(toView: Validator[]): Promise { + const view = []; + for (const validator of toView) { + const logo = await this.cache.getValidatorImg(validator.description.identity) as string; + view.push({ + logo, + description: { + moniker: validator.description.moniker, + details: validator.description.details, + securityContact: validator.description.security_contact, + identity: validator.description.identity, + website: validator.description.website, + }, + address: validator.operator_address, + status: validator.status === ValidatorStatus.BONDED ? ValidatorStatusView.BONDED : ValidatorStatusView.UN_BONDED, + jailed: validator.jailed, + stakedAmount: validator.delegator_shares, + commission: { + updateTime: validator.commission.update_time, + rate: validator.commission.commission_rates.rate, + maxChangeRate: validator.commission.commission_rates.max_change_rate, + maxRate: validator.commission.commission_rates.max_rate, + }, + }); + } + return view; + } + + async getMyValidatorDelegation(payload: MyValidatorDelegationDto) { + const cache = await this.cache.getValidatorDelegation(payload.address, payload.validatorAddress); + + if (cache === null) { + return this.fetchAndSaveMyValidatorDelegation(payload); + } + + return cache; + } + + private async fetchAndSaveMyValidatorDelegation(payload: MyValidatorDelegationDto) { + const rez = await Promise.all([ + this.fetchMyStakedAmount(payload.address, payload.validatorAddress), + this.fetchDelegatorsRewards(payload.address, payload.validatorAddress), + ]); + + const dto = { + delegation: rez[0], + earnings: rez[1], + }; + + await this.cache.setValidatorDelegation(payload.address, payload.validatorAddress, dto); + + return dto; + } + + async getValidatorDelegations(payload: ValidatorDelegationsDto) { + const res = await this.okp4Service.getValidatorDelegations(payload.address, payload.limit, payload.offset); + const validators: ValidatorsViewDto[] = await this.getValidators(); + const validator = validators.find((validator) => validator.address === payload.address); + const validatorDelegations = this.validatorDelegationView(res.delegation_responses, validator!.commission.rate); + + return { + validatorDelegations, + pagination: { + total: res.pagination.total, + limit: payload.limit || null, + offset: payload.offset || null, + } + } + } + + private validatorDelegationView(toView: Delegation[], validatorCommission: string) { + return toView.map((delegation) => ({ + delegator: delegation.delegation.delegator_address, + delegatedAmount: delegation.balance.amount, + commission: validatorCommission, })); } + + private async loadAndCacheValidatorImages() { + const validators: ValidatorsViewDto[] = await this.getValidators(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let promises: any = []; + validators.forEach(validator => promises.push(this.keybaseService.getUserLookup(validator.description.identity))); + const rez: UserLookupResponse[] = await Promise.all(promises); + promises = []; + for (let i = 0; i < validators.length; i++) { + const validator = validators[i]; + const imgUrl = rez[i]?.them[0]?.pictures?.primary?.url || ''; + promises.push(this.cache.setValidatorImg(validator.description.identity, imgUrl)); + } + await Promise.all(promises); + } } \ No newline at end of file diff --git a/src/modules/staking/staking.controller.ts b/src/modules/staking/staking.controller.ts index 13933cf..1607225 100644 --- a/src/modules/staking/staking.controller.ts +++ b/src/modules/staking/staking.controller.ts @@ -5,6 +5,10 @@ import { StakingEndpoints } from "./enums/staking-endpoints.enum"; import { QueryParam } from "./enums/query-param.enum"; import { SchemaValidatePipe } from "@core/pipes/schema-validate.pipe"; import { AddressSchema } from "./schemas/address.schema"; +import { MyValidatorDelegationSchema } from "./schemas/my-validator-delegation.schema"; +import { MyValidatorDelegationDto } from "./dtos/my-validator-delegation.dto"; +import { ValidatorDelegationsSchema } from "./schemas/validator-delegations.schema"; +import { ValidatorDelegationsDto } from "./dtos/validator-delegations.dto"; @Controller(Routes.STAKING) export class StakingController { @@ -29,4 +33,20 @@ export class StakingController { async getValidators() { return this.service.getValidators(); } + + @Get(StakingEndpoints.MY_VALIDATOR_DELEGATION) + async getMyValidatorDelegation( + @Query(new SchemaValidatePipe(MyValidatorDelegationSchema)) + params: MyValidatorDelegationDto, + ) { + return this.service.getMyValidatorDelegation(params); + } + + @Get(StakingEndpoints.VALIDATOR_DELEGATIONS) + async getValidatorDelegations( + @Query(new SchemaValidatePipe(ValidatorDelegationsSchema)) + params: ValidatorDelegationsDto, + ) { + return this.service.getValidatorDelegations(params); + } } \ No newline at end of file diff --git a/src/modules/staking/staking.module.ts b/src/modules/staking/staking.module.ts index 71efea5..d49ad65 100644 --- a/src/modules/staking/staking.module.ts +++ b/src/modules/staking/staking.module.ts @@ -5,12 +5,14 @@ import { StakingController } from "./staking.controller"; import { HttpService } from "@core/lib/http.service"; import { StakingCache } from "./services/staking.cache"; import { OsmosisService } from "@core/lib/osmosis/osmosis.service"; +import { KeybaseService } from "@core/lib/keybase/keybase.service"; @Module({ imports: [], providers: [ Okp4Service, OsmosisService, + KeybaseService, StakingService, StakingCache, HttpService, From 4a397897029b4fd4d15e1f2c7780f3cd2a2529f0 Mon Sep 17 00:00:00 2001 From: "yevhen.burkovskyi" Date: Tue, 14 May 2024 17:06:44 +0300 Subject: [PATCH 2/4] fix: added missed fields to response --- src/modules/staking/dtos/validators-view.dto.ts | 2 ++ src/modules/staking/services/staking.service.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/modules/staking/dtos/validators-view.dto.ts b/src/modules/staking/dtos/validators-view.dto.ts index 96afd98..f4179a9 100644 --- a/src/modules/staking/dtos/validators-view.dto.ts +++ b/src/modules/staking/dtos/validators-view.dto.ts @@ -17,4 +17,6 @@ export interface ValidatorsViewDto { status: string; jailed: boolean; stakedAmount: string; + uptime: number; + votingPower: number; } \ No newline at end of file diff --git a/src/modules/staking/services/staking.service.ts b/src/modules/staking/services/staking.service.ts index bcab2d3..f8c3fb8 100644 --- a/src/modules/staking/services/staking.service.ts +++ b/src/modules/staking/services/staking.service.ts @@ -170,6 +170,8 @@ export class StakingService implements OnModuleInit { status: validator.status === ValidatorStatus.BONDED ? ValidatorStatusView.BONDED : ValidatorStatusView.UN_BONDED, jailed: validator.jailed, stakedAmount: validator.delegator_shares, + uptime: 0, + votingPower: 0, commission: { updateTime: validator.commission.update_time, rate: validator.commission.commission_rates.rate, From 8bea6bb5e90b85c49e5649e380f4ba011f4c9e52 Mon Sep 17 00:00:00 2001 From: "yevhen.burkovskyi" Date: Tue, 14 May 2024 18:20:24 +0300 Subject: [PATCH 3/4] fix: first run logo null problem fixed --- src/modules/staking/services/staking.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/staking/services/staking.service.ts b/src/modules/staking/services/staking.service.ts index f8c3fb8..a5607b4 100644 --- a/src/modules/staking/services/staking.service.ts +++ b/src/modules/staking/services/staking.service.ts @@ -234,7 +234,7 @@ export class StakingService implements OnModuleInit { } private async loadAndCacheValidatorImages() { - const validators: ValidatorsViewDto[] = await this.getValidators(); + const { validators } = await this.okp4Service.getValidators(); // eslint-disable-next-line @typescript-eslint/no-explicit-any let promises: any = []; validators.forEach(validator => promises.push(this.keybaseService.getUserLookup(validator.description.identity))); From 6483209ff946cae7ce42ac37cb16462e82cb5f7a Mon Sep 17 00:00:00 2001 From: "yevhen.burkovskyi" Date: Wed, 15 May 2024 06:49:39 +0300 Subject: [PATCH 4/4] fix: fixed mistake in enum field name --- src/core/lib/okp4/enums/endpoints.enum.ts | 2 +- src/core/lib/okp4/okp4.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/lib/okp4/enums/endpoints.enum.ts b/src/core/lib/okp4/enums/endpoints.enum.ts index a6a81a0..4c82300 100644 --- a/src/core/lib/okp4/enums/endpoints.enum.ts +++ b/src/core/lib/okp4/enums/endpoints.enum.ts @@ -6,5 +6,5 @@ export enum Endpoints { SPENDABLE_BALANCE = 'cosmos/bank/v1beta1/spendable_balances', VALIDATORS = 'cosmos/staking/v1beta1/validators', TOTAL_SUPPLY = 'cosmos/bank/v1beta1/supply', - VALIDATO_DELEGATIONS = 'cosmos/staking/v1beta1/validators/:validator_addr/delegations', + VALIDATOR_DELEGATIONS = 'cosmos/staking/v1beta1/validators/:validator_addr/delegations', } diff --git a/src/core/lib/okp4/okp4.service.ts b/src/core/lib/okp4/okp4.service.ts index 9d69ad1..b663f68 100644 --- a/src/core/lib/okp4/okp4.service.ts +++ b/src/core/lib/okp4/okp4.service.ts @@ -107,7 +107,7 @@ export class Okp4Service { } return this.getWithErrorHandling( this.constructUrl( - Endpoints.VALIDATO_DELEGATIONS.replace( + Endpoints.VALIDATOR_DELEGATIONS.replace( RouteParam.VALIDATOR_ADDRES, validatorAddr, ),