From 9274f03545370d6e8e9fd6c4dab64b1cc6583fbd Mon Sep 17 00:00:00 2001 From: "yevhen.burkovskyi" Date: Mon, 17 Jun 2024 13:19:04 +0300 Subject: [PATCH 1/2] feat: added proposals list --- .env.example | 5 +- src/core/config/config.dto.ts | 4 +- src/core/config/config.ts | 2 + src/core/lib/okp4/enums/endpoints.enum.ts | 1 + src/core/lib/okp4/enums/route-param.enum.ts | 3 +- src/core/lib/okp4/okp4.service.ts | 404 ++++++++++-------- .../okp4/responses/get-proposal.response.ts | 4 + src/modules/staking/enums/query-param.enum.ts | 1 + .../enums/staking-cache-prefix.enum.ts | 4 +- .../staking/enums/staking-endpoints.enum.ts | 4 +- src/modules/staking/services/staking.cache.ts | 26 +- .../staking/services/staking.service.ts | 42 +- src/modules/staking/staking.controller.ts | 17 +- 13 files changed, 319 insertions(+), 198 deletions(-) create mode 100644 src/core/lib/okp4/responses/get-proposal.response.ts diff --git a/.env.example b/.env.example index 478ea2f..dcb32c1 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,9 @@ MY_STAKING_OVERVIEW=120 #2 mins GLOBAL_STAKING_OVERVIEW=120 #2 mins STAKING_VALIDATORS=120 #2 mins STAKING_VALIDATOR_DELEGATION=120 #2 mins +VALIDATOR_SIGNATURE=120 #2 mins +PROPOSALS_CACHE_TTL=120 #2 mins +PROPOSAL_CACHE_TTL=120 #2 mins #Keybase -KEYBASE_URL=https://keybase.io/_/api/1.0 \ No newline at end of file +KEYBASE_URL=https://keybase.io/_/api/1.0 diff --git a/src/core/config/config.dto.ts b/src/core/config/config.dto.ts index 18ed5e9..c4265d0 100644 --- a/src/core/config/config.dto.ts +++ b/src/core/config/config.dto.ts @@ -35,8 +35,10 @@ export interface CacheConfig { validators: number; validatorDelegation: number; validatorSignature: number; + proposals: number; + proposal: number; } export interface KeybaseConfig { url: string; -} \ No newline at end of file +} diff --git a/src/core/config/config.ts b/src/core/config/config.ts index 7cfdede..94fb652 100644 --- a/src/core/config/config.ts +++ b/src/core/config/config.ts @@ -25,6 +25,8 @@ export const config: ConfigDto = { validators: +process.env.STAKING_VALIDATORS!, validatorDelegation: +process.env.STAKING_VALIDATOR_DELEGATION!, validatorSignature: +process.env.VALIDATOR_SIGNATURE!, + proposals: +process.env.PROPOSALS_CACHE_TTL!, + proposal: +process.env.PROPOSAL_CACHE_TTL!, }, keybase: { url: process.env.KEYBASE_URL!, diff --git a/src/core/lib/okp4/enums/endpoints.enum.ts b/src/core/lib/okp4/enums/endpoints.enum.ts index cacdc42..32c0478 100644 --- a/src/core/lib/okp4/enums/endpoints.enum.ts +++ b/src/core/lib/okp4/enums/endpoints.enum.ts @@ -11,4 +11,5 @@ export enum Endpoints { BLOCKS_BY_HEIGHT = 'cosmos/base/tendermint/v1beta1/blocks/:height', GOV_PARAMS = 'cosmos/gov/v1/params/:params_type', GOV_PROPOSALS = 'cosmos/gov/v1/proposals', + GOV_PROPOSAL = 'cosmos/gov/v1/proposals/:proposal_id', } diff --git a/src/core/lib/okp4/enums/route-param.enum.ts b/src/core/lib/okp4/enums/route-param.enum.ts index 1d270c1..6ede3d5 100644 --- a/src/core/lib/okp4/enums/route-param.enum.ts +++ b/src/core/lib/okp4/enums/route-param.enum.ts @@ -3,4 +3,5 @@ export enum RouteParam { VALIDATOR_ADDRES = ':validator_addr', HEIGHT = ':height', PARAMS_TYPE = ':params_type', -} \ No newline at end of file + PROPOSAL_ID = ':proposal_id', +} diff --git a/src/core/lib/okp4/okp4.service.ts b/src/core/lib/okp4/okp4.service.ts index db02ee4..20bb1fc 100644 --- a/src/core/lib/okp4/okp4.service.ts +++ b/src/core/lib/okp4/okp4.service.ts @@ -14,198 +14,236 @@ import { SpendableBalancesResponse } from "./responses/spendable-balances.respon import { SupplyResponse } from "./responses/supply.response"; import { ValidatorStatus } from "./enums/validator-status.enum"; import { ValidatorDelegationsResponse } from "./responses/validator-delegations.response"; -import { fromBase64, toBase64, fromHex, toHex } from '@cosmjs/encoding'; -import { sha256 } from '@cosmjs/crypto'; +import { fromBase64, toBase64, fromHex, toHex } from "@cosmjs/encoding"; +import { sha256 } from "@cosmjs/crypto"; import { BlocksResponse } from "./responses/blocks.response"; -import { WebSocket } from 'ws'; +import { WebSocket } from "ws"; import { Log } from "@core/loggers/log"; import { EventEmitter2 } from "@nestjs/event-emitter"; import { GovType } from "./enums/gov-type.enum"; import { GovParamsResponse } from "./responses/gov-params.response"; import { GetProposalsResponse } from "./responses/get-proposals.response"; +import { GetProposalResponse } from "@core/lib/okp4/responses/get-proposal.response"; @Injectable() export class Okp4Service { - private BASE_URL = config.okp4.url; - - constructor( - private readonly httpService: HttpService, - private eventEmitter: EventEmitter2, - ) { } - - 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 getSupplyByDenom(denom: string): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.SUPPLY_BY_DENOM, - createUrlParams({ denom }), - ) - ); - } - - async getDelegations(addr: string): Promise { - return this.getWithErrorHandling(this.constructUrl(`${Endpoints.STAKING_DELEGATIONS}/${addr}`)); - } - - async getDelegatorsValidators(addr: string): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.DELEGATORS_VALIDATORS.replace( - RouteParam.DELEGATOR_ADDRES, - addr, - ) - ) - ); - } - - async getDelegatorsRewards(addr: string): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.DELEGATORS_REWARDS.replace( - RouteParam.DELEGATOR_ADDRES, - addr, - ) - ) - ); - } - - async getSpendableBalances(addr: string): Promise { - return this.getWithErrorHandling(this.constructUrl(`${Endpoints.SPENDABLE_BALANCE}/${addr}`)); - } - - async getBondValidators() { - return this.getValidators(ValidatorStatus.BONDED); - } - - async getValidators(status?: string): Promise { - let params = undefined; - if (status) { - params = createUrlParams({ status }); - } - const url = this.constructUrl( - Endpoints.VALIDATORS, - params, - ); - return this.getWithErrorHandling(url); - } - - async getTotalSupply(): Promise { - const url = this.constructUrl( - Endpoints.TOTAL_SUPPLY, - ); - 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.VALIDATOR_DELEGATIONS.replace( - RouteParam.VALIDATOR_ADDRES, - validatorAddr, - ), - params - ) - ); - } - - async getLatestBlocks(): Promise { - return this.getWithErrorHandling( - this.constructUrl(Endpoints.BLOCKS_LATEST), - ) - } - - async getBlocksByHeight(height: number): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.BLOCKS_BY_HEIGHT.replace(RouteParam.HEIGHT, height.toString()) - ) - ) - } - - apiPubkeyToAddr(pubkey: string) { - return toBase64(fromHex(toHex(sha256(fromBase64(pubkey))).slice(0, 40))) - } - - wssPubkeyToAddr(pubkey: string) { - return toHex(sha256(fromBase64(pubkey))).slice(0, 40); - } - - async connectToNewBlockSocket(event: string) { - const client = new WebSocket(config.okp4.wss); - client.on('open', () => { - client.send(JSON.stringify({"jsonrpc":"2.0","method":"subscribe","id":0,"params":{"query":"tm.event='NewBlock'"}})); - }); - client.on('message', (data) => { - if (Buffer.isBuffer(data)) { - const message = data.toString('utf-8'); + private BASE_URL = config.okp4.url; + + constructor( + private readonly httpService: HttpService, + private eventEmitter: EventEmitter2 + ) {} + + 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 getSupplyByDenom(denom: string): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.SUPPLY_BY_DENOM, + createUrlParams({ denom }) + ) + ); + } + + async getDelegations(addr: string): Promise { + return this.getWithErrorHandling( + this.constructUrl(`${Endpoints.STAKING_DELEGATIONS}/${addr}`) + ); + } + + async getDelegatorsValidators( + addr: string + ): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.DELEGATORS_VALIDATORS.replace( + RouteParam.DELEGATOR_ADDRES, + addr + ) + ) + ); + } + + async getDelegatorsRewards( + addr: string + ): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.DELEGATORS_REWARDS.replace( + RouteParam.DELEGATOR_ADDRES, + addr + ) + ) + ); + } + + async getSpendableBalances( + addr: string + ): Promise { + return this.getWithErrorHandling( + this.constructUrl(`${Endpoints.SPENDABLE_BALANCE}/${addr}`) + ); + } + + async getBondValidators() { + return this.getValidators(ValidatorStatus.BONDED); + } + + async getValidators(status?: string): Promise { + let params = undefined; + if (status) { + params = createUrlParams({ status }); + } + const url = this.constructUrl(Endpoints.VALIDATORS, params); + return this.getWithErrorHandling(url); + } + + async getTotalSupply(): Promise { + const url = this.constructUrl(Endpoints.TOTAL_SUPPLY); + 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.VALIDATOR_DELEGATIONS.replace( + RouteParam.VALIDATOR_ADDRES, + validatorAddr + ), + params + ) + ); + } + + async getLatestBlocks(): Promise { + return this.getWithErrorHandling( + this.constructUrl(Endpoints.BLOCKS_LATEST) + ); + } + + async getBlocksByHeight(height: number): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.BLOCKS_BY_HEIGHT.replace( + RouteParam.HEIGHT, + height.toString() + ) + ) + ); + } + + apiPubkeyToAddr(pubkey: string) { + return toBase64( + fromHex(toHex(sha256(fromBase64(pubkey))).slice(0, 40)) + ); + } + + wssPubkeyToAddr(pubkey: string) { + return toHex(sha256(fromBase64(pubkey))).slice(0, 40); + } + + async connectToNewBlockSocket(event: string) { + const client = new WebSocket(config.okp4.wss); + client.on("open", () => { + client.send( + JSON.stringify({ + jsonrpc: "2.0", + method: "subscribe", + id: 0, + params: { query: "tm.event='NewBlock'" }, + }) + ); + }); + client.on("message", (data) => { + if (Buffer.isBuffer(data)) { + const message = data.toString("utf-8"); + try { + const jsonData = JSON.parse(message); + if ( + jsonData && + jsonData?.result && + jsonData?.result?.query === "tm.event='NewBlock'" + ) { + this.eventEmitter.emit( + event, + jsonData?.result?.data?.value + ); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + Log.warn( + "[OKP4] Problem with parsing data from wss\n" + + e.message + ); + } + } + }); + } + + async getGovParams(type = GovType.VOTING): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.GOV_PARAMS.replace(RouteParam.PARAMS_TYPE, type) + ) + ); + } + + async getProposals(): Promise { + return this.getWithErrorHandling( + this.constructUrl(Endpoints.GOV_PROPOSALS) + ); + } + + async getProposal( + proposalId: string | number + ): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.GOV_PROPOSAL.replace( + RouteParam.PROPOSAL_ID, + String(proposalId) + ) + ) + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async errorHandleWrapper(fn: any): Promise { try { - const jsonData = JSON.parse(message); - if ( - jsonData && - jsonData?.result && - jsonData?.result?.query === "tm.event='NewBlock'" - ) { - this.eventEmitter.emit(event, jsonData?.result?.data?.value); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const response: GSFResponse = await fn(); + + if (this.isFailedResponse(response)) { + throw new BadRequestException(response.message); + } + + return response as T; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { - Log.warn('[OKP4] Problem with parsing data from wss\n' + e.message); + throw new BadRequestException(e.message); } - } - }); - } - - async getGovParams(type = GovType.VOTING): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.GOV_PARAMS.replace(RouteParam.PARAMS_TYPE, type) - ) - ); - } - - async getProposals(): Promise { - return this.getWithErrorHandling( - this.constructUrl(Endpoints.GOV_PROPOSALS) - ); - } - - // 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.message); - } - - 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).message !== undefined; - } -} \ No newline at end of file + } + + private isFailedResponse( + response: GSFResponse + ): response is FailedResponse { + return (response as FailedResponse).message !== undefined; + } +} diff --git a/src/core/lib/okp4/responses/get-proposal.response.ts b/src/core/lib/okp4/responses/get-proposal.response.ts new file mode 100644 index 0000000..3803ea3 --- /dev/null +++ b/src/core/lib/okp4/responses/get-proposal.response.ts @@ -0,0 +1,4 @@ +import { WithPaginationResponse } from "./with-pagination.response" +import { Proposal } from "@core/lib/okp4/responses/get-proposals.response"; + +export type GetProposalResponse = WithPaginationResponse<{ proposal: Proposal }>; diff --git a/src/modules/staking/enums/query-param.enum.ts b/src/modules/staking/enums/query-param.enum.ts index 69871f6..8b3bba1 100644 --- a/src/modules/staking/enums/query-param.enum.ts +++ b/src/modules/staking/enums/query-param.enum.ts @@ -1,4 +1,5 @@ export enum QueryParam { ADDRESS = 'address', VALIDATOR_ADDRESS = 'validatorAddress', + PROPOSAL_ID = 'proposal_id' } diff --git a/src/modules/staking/enums/staking-cache-prefix.enum.ts b/src/modules/staking/enums/staking-cache-prefix.enum.ts index ffd75e2..471713e 100644 --- a/src/modules/staking/enums/staking-cache-prefix.enum.ts +++ b/src/modules/staking/enums/staking-cache-prefix.enum.ts @@ -5,4 +5,6 @@ export enum StakingCachePrefix { VALIDATOR_IMG = 'validator_img', VALIDATOR_SIGNATURES = 'validator_signatures', VALIDATOR_RECENTLY_PROPOSED_BLOCKS = 'validator_recently_propored_blocks', -} \ No newline at end of file + PROPOSALS = 'proposals', + PROPOSAL = 'proposal', +} diff --git a/src/modules/staking/enums/staking-endpoints.enum.ts b/src/modules/staking/enums/staking-endpoints.enum.ts index c46c1f3..2e3d3b5 100644 --- a/src/modules/staking/enums/staking-endpoints.enum.ts +++ b/src/modules/staking/enums/staking-endpoints.enum.ts @@ -7,4 +7,6 @@ export enum StakingEndpoints { VALIDATORS_BY_ADDRESS = '/validators/:address', VALIDATORS_UPTIME = '/validators/:address/uptime', VALIDATORS_RECENTLY_PROPOSED_BLOCKS = '/validators/:address/recently-proposed-blocks', -} \ No newline at end of file + PROPOSALS = '/proposals', + PROPOSAL = '/proposals/:proposal_id', +} diff --git a/src/modules/staking/services/staking.cache.ts b/src/modules/staking/services/staking.cache.ts index ace13b4..3ed55ba 100644 --- a/src/modules/staking/services/staking.cache.ts +++ b/src/modules/staking/services/staking.cache.ts @@ -4,6 +4,8 @@ import { createHash } from 'crypto'; import { StakingCachePrefix } from "../enums/staking-cache-prefix.enum"; import { RedisService } from "@core/lib/redis.service"; import { v4 } from 'uuid'; +import { GetProposalsResponse } from "@core/lib/okp4/responses/get-proposals.response"; +import { GetProposalResponse } from "@core/lib/okp4/responses/get-proposal.response"; @Injectable() export class StakingCache { @@ -86,7 +88,7 @@ export class StakingCache { const pattern = this.createRedisKey(StakingCachePrefix.VALIDATOR_SIGNATURES, address, '*'); const keys = await this.redisService.keys(pattern); const signatures = await Promise.all(keys.map((key: string) => this.redisService.get(key))); - + return signatures.map(signature => JSON.parse(signature!)); } @@ -99,11 +101,29 @@ export class StakingCache { const pattern = this.createRedisKey(StakingCachePrefix.VALIDATOR_RECENTLY_PROPOSED_BLOCKS, '*'); const keys = await this.redisService.keys(pattern); const recentlyProposedBlocks = await Promise.all(keys.map((key: string) => this.redisService.get(key))); - + return recentlyProposedBlocks.map(block => JSON.parse(block!)); } private createRedisKey(...ids: string[]) { return ids.reduce((acc, id) => acc + `_${id}`, `${StakingCachePrefix.STAKING}`); } -} \ No newline at end of file + + async setProposals(proposals: GetProposalsResponse) { + const serialized = JSON.stringify(proposals); + await this.redisService.setWithTTL(this.createRedisKey(StakingCachePrefix.PROPOSALS), serialized, config.cache.proposals); + } + + async getProposals(): Promise { + return this.getObjByRedisKey(this.createRedisKey(StakingCachePrefix.PROPOSALS)); + } + + async setProposal(proposalId: string | number, proposal: GetProposalResponse) { + const serialized = JSON.stringify(proposal); + await this.redisService.setWithTTL(this.createRedisKey(StakingCachePrefix.PROPOSAL, String(proposalId)), serialized, config.cache.proposal); + } + + async getProposal(proposalId: string | number): Promise { + return this.getObjByRedisKey(this.createRedisKey(StakingCachePrefix.PROPOSAL, String(proposalId))); + } +} diff --git a/src/modules/staking/services/staking.service.ts b/src/modules/staking/services/staking.service.ts index e9fcd77..aa6416a 100644 --- a/src/modules/staking/services/staking.service.ts +++ b/src/modules/staking/services/staking.service.ts @@ -218,7 +218,7 @@ export class StakingService implements OnModuleInit { delegation: rez[0], earnings: rez[1], }; - + await this.cache.setValidatorDelegation(payload.address, payload.validatorAddress, dto); return dto; @@ -288,7 +288,7 @@ export class StakingService implements OnModuleInit { Log.warn("New block error " + e.message); } } - + private async cacheBlock(res: BlocksResponse) { try { await this.cache.setRecentlyProposedBlock({ @@ -366,7 +366,7 @@ export class StakingService implements OnModuleInit { } return Big(blocks.length).div(signed).toNumber(); } - + async getValidatorUptime(address: string) { const signatures = await this.getSortedValidatorSignatures(address); const current = await this.getLastBlockHeight(); @@ -398,7 +398,7 @@ export class StakingService implements OnModuleInit { private async getSortedRecentlyProposedBlocks() { try { const recentlyBlocks: RecentlyProposedBlockDto[] = await this.cache.getRecentlyProposedBlock(); - return recentlyBlocks.sort((a, b) => Number.parseFloat(b.height) - Number.parseFloat(a.height)); + return recentlyBlocks.sort((a, b) => Number.parseFloat(b.height) - Number.parseFloat(a.height)); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch(e: any) { Log.warn("Cache recently proposed blocks deserialization error " + e.message); @@ -420,4 +420,36 @@ export class StakingService implements OnModuleInit { return []; } -} \ No newline at end of file + + private async fetchProposals() { + const proposals = await this.okp4Service.getProposals(); + await this.cache.setProposals(proposals); + return proposals; + } + + async getProposals() { + const cache = await this.cache.getProposals(); + + if (cache === null) { + return this.fetchProposals(); + } + + return cache; + } + + private async fetchProposal(proposalId: string | number) { + const proposal = await this.okp4Service.getProposal(proposalId); + await this.cache.setProposal(proposalId, proposal); + return proposal; + } + + async getProposal(proposalId: string | number) { + const cache = await this.cache.getProposal(proposalId); + + if (cache === null) { + return this.fetchProposal(proposalId); + } + + return cache; + } +} diff --git a/src/modules/staking/staking.controller.ts b/src/modules/staking/staking.controller.ts index 3585b0d..d1e6e69 100644 --- a/src/modules/staking/staking.controller.ts +++ b/src/modules/staking/staking.controller.ts @@ -16,7 +16,7 @@ export class StakingController { constructor( private readonly service: StakingService, ) { } - + @Get(StakingEndpoints.MY_OVERVIEW) async getMyStakedOverview( @Query(QueryParam.ADDRESS, new SchemaValidatePipe(AddressSchema)) @@ -71,4 +71,17 @@ export class StakingController { async getValidatorRecentlyProposedBlocks() { return this.service.getValidatorRecentlyProposedBlocks(); } -} \ No newline at end of file + + @Get(StakingEndpoints.PROPOSALS) + async getProposals() { + return this.service.getProposals(); + } + + @Get(StakingEndpoints.PROPOSAL) + async getProposal( + @Param(QueryParam.PROPOSAL_ID, new SchemaValidatePipe(StringSchema)) + proposalId: string, + ) { + return this.service.getProposal(proposalId); + } +} From 49dad51a1e44f833a3b006713c071c7922a27806 Mon Sep 17 00:00:00 2001 From: "yevhen.burkovskyi" Date: Mon, 17 Jun 2024 13:20:37 +0300 Subject: [PATCH 2/2] style: fixed lint problems --- src/core/lib/okp4/okp4.service.ts | 424 +++++++++++----------- src/modules/staking/staking.controller.ts | 4 +- 2 files changed, 214 insertions(+), 214 deletions(-) diff --git a/src/core/lib/okp4/okp4.service.ts b/src/core/lib/okp4/okp4.service.ts index 20bb1fc..7b11062 100644 --- a/src/core/lib/okp4/okp4.service.ts +++ b/src/core/lib/okp4/okp4.service.ts @@ -27,223 +27,223 @@ import { GetProposalResponse } from "@core/lib/okp4/responses/get-proposal.respo @Injectable() export class Okp4Service { - private BASE_URL = config.okp4.url; + private BASE_URL = config.okp4.url; - constructor( + constructor( private readonly httpService: HttpService, private eventEmitter: EventEmitter2 - ) {} - - 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 getSupplyByDenom(denom: string): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.SUPPLY_BY_DENOM, - createUrlParams({ denom }) - ) - ); - } - - async getDelegations(addr: string): Promise { - return this.getWithErrorHandling( - this.constructUrl(`${Endpoints.STAKING_DELEGATIONS}/${addr}`) - ); - } - - async getDelegatorsValidators( - addr: string - ): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.DELEGATORS_VALIDATORS.replace( - RouteParam.DELEGATOR_ADDRES, - addr - ) - ) - ); - } - - async getDelegatorsRewards( - addr: string - ): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.DELEGATORS_REWARDS.replace( - RouteParam.DELEGATOR_ADDRES, - addr - ) - ) - ); - } - - async getSpendableBalances( - addr: string - ): Promise { - return this.getWithErrorHandling( - this.constructUrl(`${Endpoints.SPENDABLE_BALANCE}/${addr}`) - ); - } - - async getBondValidators() { - return this.getValidators(ValidatorStatus.BONDED); - } - - async getValidators(status?: string): Promise { - let params = undefined; - if (status) { - params = createUrlParams({ status }); - } - const url = this.constructUrl(Endpoints.VALIDATORS, params); - return this.getWithErrorHandling(url); - } - - async getTotalSupply(): Promise { - const url = this.constructUrl(Endpoints.TOTAL_SUPPLY); - 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.VALIDATOR_DELEGATIONS.replace( - RouteParam.VALIDATOR_ADDRES, - validatorAddr - ), - params - ) - ); - } - - async getLatestBlocks(): Promise { - return this.getWithErrorHandling( - this.constructUrl(Endpoints.BLOCKS_LATEST) - ); - } - - async getBlocksByHeight(height: number): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.BLOCKS_BY_HEIGHT.replace( - RouteParam.HEIGHT, - height.toString() - ) - ) - ); - } - - apiPubkeyToAddr(pubkey: string) { - return toBase64( - fromHex(toHex(sha256(fromBase64(pubkey))).slice(0, 40)) - ); - } - - wssPubkeyToAddr(pubkey: string) { - return toHex(sha256(fromBase64(pubkey))).slice(0, 40); - } - - async connectToNewBlockSocket(event: string) { - const client = new WebSocket(config.okp4.wss); - client.on("open", () => { - client.send( - JSON.stringify({ - jsonrpc: "2.0", - method: "subscribe", - id: 0, - params: { query: "tm.event='NewBlock'" }, - }) - ); - }); - client.on("message", (data) => { - if (Buffer.isBuffer(data)) { - const message = data.toString("utf-8"); - try { - const jsonData = JSON.parse(message); - if ( - jsonData && + ) {} + + 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 getSupplyByDenom(denom: string): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.SUPPLY_BY_DENOM, + createUrlParams({ denom }) + ) + ); + } + + async getDelegations(addr: string): Promise { + return this.getWithErrorHandling( + this.constructUrl(`${Endpoints.STAKING_DELEGATIONS}/${addr}`) + ); + } + + async getDelegatorsValidators( + addr: string + ): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.DELEGATORS_VALIDATORS.replace( + RouteParam.DELEGATOR_ADDRES, + addr + ) + ) + ); + } + + async getDelegatorsRewards( + addr: string + ): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.DELEGATORS_REWARDS.replace( + RouteParam.DELEGATOR_ADDRES, + addr + ) + ) + ); + } + + async getSpendableBalances( + addr: string + ): Promise { + return this.getWithErrorHandling( + this.constructUrl(`${Endpoints.SPENDABLE_BALANCE}/${addr}`) + ); + } + + async getBondValidators() { + return this.getValidators(ValidatorStatus.BONDED); + } + + async getValidators(status?: string): Promise { + let params = undefined; + if (status) { + params = createUrlParams({ status }); + } + const url = this.constructUrl(Endpoints.VALIDATORS, params); + return this.getWithErrorHandling(url); + } + + async getTotalSupply(): Promise { + const url = this.constructUrl(Endpoints.TOTAL_SUPPLY); + 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.VALIDATOR_DELEGATIONS.replace( + RouteParam.VALIDATOR_ADDRES, + validatorAddr + ), + params + ) + ); + } + + async getLatestBlocks(): Promise { + return this.getWithErrorHandling( + this.constructUrl(Endpoints.BLOCKS_LATEST) + ); + } + + async getBlocksByHeight(height: number): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.BLOCKS_BY_HEIGHT.replace( + RouteParam.HEIGHT, + height.toString() + ) + ) + ); + } + + apiPubkeyToAddr(pubkey: string) { + return toBase64( + fromHex(toHex(sha256(fromBase64(pubkey))).slice(0, 40)) + ); + } + + wssPubkeyToAddr(pubkey: string) { + return toHex(sha256(fromBase64(pubkey))).slice(0, 40); + } + + async connectToNewBlockSocket(event: string) { + const client = new WebSocket(config.okp4.wss); + client.on("open", () => { + client.send( + JSON.stringify({ + jsonrpc: "2.0", + method: "subscribe", + id: 0, + params: { query: "tm.event='NewBlock'" }, + }) + ); + }); + client.on("message", (data) => { + if (Buffer.isBuffer(data)) { + const message = data.toString("utf-8"); + try { + const jsonData = JSON.parse(message); + if ( + jsonData && jsonData?.result && jsonData?.result?.query === "tm.event='NewBlock'" - ) { - this.eventEmitter.emit( - event, - jsonData?.result?.data?.value - ); - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (e: any) { - Log.warn( - "[OKP4] Problem with parsing data from wss\n" + - e.message - ); - } - } - }); - } - - async getGovParams(type = GovType.VOTING): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.GOV_PARAMS.replace(RouteParam.PARAMS_TYPE, type) - ) - ); - } - - async getProposals(): Promise { - return this.getWithErrorHandling( - this.constructUrl(Endpoints.GOV_PROPOSALS) - ); - } - - async getProposal( - proposalId: string | number - ): Promise { - return this.getWithErrorHandling( - this.constructUrl( - Endpoints.GOV_PROPOSAL.replace( - RouteParam.PROPOSAL_ID, - String(proposalId) - ) - ) - ); - } - - // 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.message); - } - - return response as T; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) { + this.eventEmitter.emit( + event, + jsonData?.result?.data?.value + ); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e: any) { - throw new BadRequestException(e.message); + Log.warn( + "[OKP4] Problem with parsing data from wss\n" + + e.message + ); } - } - - private isFailedResponse( - response: GSFResponse - ): response is FailedResponse { - return (response as FailedResponse).message !== undefined; - } + } + }); + } + + async getGovParams(type = GovType.VOTING): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.GOV_PARAMS.replace(RouteParam.PARAMS_TYPE, type) + ) + ); + } + + async getProposals(): Promise { + return this.getWithErrorHandling( + this.constructUrl(Endpoints.GOV_PROPOSALS) + ); + } + + async getProposal( + proposalId: string | number + ): Promise { + return this.getWithErrorHandling( + this.constructUrl( + Endpoints.GOV_PROPOSAL.replace( + RouteParam.PROPOSAL_ID, + String(proposalId) + ) + ) + ); + } + + // 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.message); + } + + 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).message !== undefined; + } } diff --git a/src/modules/staking/staking.controller.ts b/src/modules/staking/staking.controller.ts index d1e6e69..1994794 100644 --- a/src/modules/staking/staking.controller.ts +++ b/src/modules/staking/staking.controller.ts @@ -74,13 +74,13 @@ export class StakingController { @Get(StakingEndpoints.PROPOSALS) async getProposals() { - return this.service.getProposals(); + return this.service.getProposals(); } @Get(StakingEndpoints.PROPOSAL) async getProposal( @Param(QueryParam.PROPOSAL_ID, new SchemaValidatePipe(StringSchema)) - proposalId: string, + proposalId: string, ) { return this.service.getProposal(proposalId); }