Skip to content

Commit

Permalink
Merge pull request #15 from axone-protocol/feat/single-validator-page
Browse files Browse the repository at this point in the history
feat: added info for single validator page
  • Loading branch information
dbatarin authored May 15, 2024
2 parents a039a97 + 6483209 commit 7514183
Show file tree
Hide file tree
Showing 24 changed files with 381 additions and 31 deletions.
8 changes: 6 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,9 @@ REDIS_HOST=localhost
REDIS_PORT=6379

# cache
MY_STAKING_OVERVIEW=120000 #2 mins
GLOBAL_STAKING_OVERVIEW=120000 #2 mins
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
6 changes: 6 additions & 0 deletions src/core/config/config.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface ConfigDto {
okp4: Okp4Config;
redis: RedisConfig;
cache: CacheConfig;
keybase: KeybaseConfig;
}

export interface AppConfig {
Expand Down Expand Up @@ -31,4 +32,9 @@ export interface CacheConfig {
myStakingOverview: number;
globalStakingOverview: number;
validators: number;
validatorDelegation: number;
}

export interface KeybaseConfig {
url: string;
}
2 changes: 2 additions & 0 deletions src/core/config/config.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
4 changes: 4 additions & 0 deletions src/core/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!,
}
};
59 changes: 59 additions & 0 deletions src/core/lib/keybase/keybase.service.ts
Original file line number Diff line number Diff line change
@@ -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<T>(url: string): Promise<T> {
return this.errorHandleWrapper(
this.httpService.get.bind(
null,
url,
),
);
}

async getUserLookup(key: string): Promise<UserLookupResponse> {
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<T>(fn: any): Promise<T> {
try {
const response: GSFResponse<T> = 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<T>(response: GSFResponse<T>): response is FailedResponse {
return (response as FailedResponse).status.code !== 0;
}
}
10 changes: 10 additions & 0 deletions src/core/lib/keybase/responses/failed.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface FailedResponse {
status: {
code: number;
desc: string;
fields: {
key_suffix: string;
};
name: string;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { FailedResponse } from './failed.response';

export type GSFResponse<T> = T | FailedResponse; // Generic Success / Failed Response for Osmosis
17 changes: 17 additions & 0 deletions src/core/lib/keybase/responses/user-lookup.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export interface UserLookupResponse {
status: {
code: number;
name: string;
},
them: [
{
id: string;
pictures: {
primary: {
url: string;
source: string;
}
}
}
]
}
1 change: 1 addition & 0 deletions src/core/lib/okp4/enums/endpoints.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
VALIDATOR_DELEGATIONS = 'cosmos/staking/v1beta1/validators/:validator_addr/delegations',
}
1 change: 1 addition & 0 deletions src/core/lib/okp4/enums/route-param.enum.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum RouteParam {
DELEGATOR_ADDRES = ':delegator_addr',
VALIDATOR_ADDRES = ':validator_addr',
}
21 changes: 21 additions & 0 deletions src/core/lib/okp4/okp4.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -95,6 +96,26 @@ export class Okp4Service {
return this.getWithErrorHandling(url);
}

async getValidatorDelegations(validatorAddr: string, limit?: number, offset?: number): Promise<ValidatorDelegationsResponse> {
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
)
);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private async errorHandleWrapper<T>(fn: any): Promise<T> {
try {
Expand Down
15 changes: 15 additions & 0 deletions src/core/lib/okp4/responses/validator-delegations.response.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 4 additions & 0 deletions src/modules/staking/dtos/my-validator-delegation.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface MyValidatorDelegationDto {
address: string;
validatorAddress: string;
}
5 changes: 5 additions & 0 deletions src/modules/staking/dtos/validator-delegations.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface ValidatorDelegationsDto {
address: string;
limit?: number;
offset?: number;
}
18 changes: 16 additions & 2 deletions src/modules/staking/dtos/validators-view.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
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;
uptime: number;
votingPower: number;
}
1 change: 1 addition & 0 deletions src/modules/staking/enums/query-param.enum.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export enum QueryParam {
ADDRESS = 'address',
VALIDATOR_ADDRESS = 'validatorAddress',
}
6 changes: 6 additions & 0 deletions src/modules/staking/enums/staking-cache-prefix.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export enum StakingCachePrefix {
STAKING = 'staking',
GLOBAL_OVERVIEW = 'global_overview',
VALIDATORS = 'validators',
VALIDATOR_IMG = 'validator_img',
}
2 changes: 2 additions & 0 deletions src/modules/staking/enums/staking-endpoints.enum.ts
Original file line number Diff line number Diff line change
@@ -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',
}
6 changes: 6 additions & 0 deletions src/modules/staking/schemas/my-validator-delegation.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as Joi from 'joi';

export const MyValidatorDelegationSchema = Joi.object({
address: Joi.string().required(),
validatorAddress: Joi.string().required(),
}).required()
10 changes: 10 additions & 0 deletions src/modules/staking/schemas/validator-delegations.schema.ts
Original file line number Diff line number Diff line change
@@ -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();
47 changes: 37 additions & 10 deletions src/modules/staking/services/staking.cache.ts
Original file line number Diff line number Diff line change
@@ -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,
) { }
Expand All @@ -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}`);
}
}
Loading

0 comments on commit 7514183

Please sign in to comment.