Skip to content

Commit

Permalink
feat: Add validators collection job (#1184)
Browse files Browse the repository at this point in the history
* feat: Add validators endpoint. Add validators collections job with ValidatorService.

* feat: Add committee cache and validators/stats endpoint

* feat: Add inCommittee flag to IValidatorsResponse

* fix: Change stakers field to validators now that is fixed

* feat: Add validator stats endpoint

* feat: Fix validator stats computation

* fix: Ignore eslint unresolved nova SDK import

* chore: Ignore more eslint no-unsafe for SDK (CI)
  • Loading branch information
msarcev authored Mar 8, 2024
1 parent d83fe4d commit be8f1bf
Show file tree
Hide file tree
Showing 7 changed files with 209 additions and 0 deletions.
5 changes: 5 additions & 0 deletions api/src/initServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { InfluxServiceNova } from "./services/nova/influx/influxServiceNova";
import { NodeInfoService as NodeInfoServiceNova } from "./services/nova/nodeInfoService";
import { NovaApiService } from "./services/nova/novaApiService";
import { NovaStatsService } from "./services/nova/stats/novaStatsService";
import { ValidatorService } from "./services/nova/validatorService";
import { ChronicleService as ChronicleServiceStardust } from "./services/stardust/chronicleService";
import { StardustFeed } from "./services/stardust/feed/stardustFeed";
import { InfluxServiceStardust } from "./services/stardust/influx/influxServiceStardust";
Expand Down Expand Up @@ -244,6 +245,10 @@ function initNovaServices(networkConfig: INetwork): void {
const novaFeed = new NovaFeed(networkConfig);
ServiceFactory.register(`feed-${networkConfig.network}`, () => novaFeed);
});

const validatorService = new ValidatorService(networkConfig);
validatorService.setupValidatorsCollection();
ServiceFactory.register(`validator-service-${networkConfig.network}`, () => validatorService);
});

const influxDBService = new InfluxServiceNova(networkConfig);
Expand Down
14 changes: 14 additions & 0 deletions api/src/models/api/nova/IValidatorStatsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { IResponse } from "./IResponse";

export interface IValidatorStatsResponse extends IResponse {
/**
* totalPoolStake (BigInt)
*/
totalPoolStake?: string;
/**
* totalValidatorStake (BigInt)
*/
totalValidatorStake?: string;
}
13 changes: 13 additions & 0 deletions api/src/models/api/nova/IValidatorsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { ValidatorResponse } from "@iota/sdk-nova";
import { IResponse } from "./IResponse";

export interface IValidator {
validator: ValidatorResponse;
inCommittee: boolean;
}

export interface IValidatorsResponse extends IResponse {
validators?: IValidator[];
}
2 changes: 2 additions & 0 deletions api/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ export const routes: IRoute[] = [
},
{ path: "/nova/block/:network/:blockId", method: "get", folder: "nova/block", func: "get" },
{ path: "/nova/block/metadata/:network/:blockId", method: "get", folder: "nova/block/metadata", func: "get" },
{ path: "/nova/validators/:network", method: "get", folder: "nova/validators", func: "get" },
{ path: "/nova/validators/stats/:network", method: "get", folder: "nova/validators/stats", func: "get" },
{
path: "/nova/commitment/latest/:network",
method: "get",
Expand Down
37 changes: 37 additions & 0 deletions api/src/routes/nova/validators/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ServiceFactory } from "../../../factories/serviceFactory";
import { INetworkBoundGetRequest } from "../../../models/api/INetworkBoundGetRequest";
import { IValidator, IValidatorsResponse } from "../../../models/api/nova/IValidatorsResponse";
import { IConfiguration } from "../../../models/configuration/IConfiguration";
import { NOVA } from "../../../models/db/protocolVersion";
import { NetworkService } from "../../../services/networkService";
import { ValidatorService } from "../../../services/nova/validatorService";
import { ValidationHelper } from "../../../utils/validationHelper";

/**
* Fetch the current validators (cached).
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(_: IConfiguration, request: INetworkBoundGetRequest): Promise<IValidatorsResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const validatorService = ServiceFactory.get<ValidatorService>(`validator-service-${networkConfig.network}`);

const committeeAddresses = validatorService.committee?.committee.map((member) => member.address) ?? [];
const validators: IValidator[] = validatorService?.validators.map((validator) => ({
validator,
inCommittee: committeeAddresses.includes(validator.address),
}));

return { validators };
}
41 changes: 41 additions & 0 deletions api/src/routes/nova/validators/stats/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { ServiceFactory } from "../../../../factories/serviceFactory";
import { INetworkBoundGetRequest } from "../../../../models/api/INetworkBoundGetRequest";
import { IValidatorStatsResponse } from "../../../../models/api/nova/IValidatorStatsResponse";
import { IConfiguration } from "../../../../models/configuration/IConfiguration";
import { NOVA } from "../../../../models/db/protocolVersion";
import { NetworkService } from "../../../../services/networkService";
import { ValidatorService } from "../../../../services/nova/validatorService";
import { ValidationHelper } from "../../../../utils/validationHelper";

/**
* Fetch the current validator stats (cached).
* @param _ The configuration.
* @param request The request.
* @returns The response.
*/
export async function get(_: IConfiguration, request: INetworkBoundGetRequest): Promise<IValidatorStatsResponse> {
const networkService = ServiceFactory.get<NetworkService>("network");
const networks = networkService.networkNames();
ValidationHelper.oneOf(request.network, networks, "network");

const networkConfig = networkService.get(request.network);

if (networkConfig.protocolVersion !== NOVA) {
return {};
}

const validatorService = ServiceFactory.get<ValidatorService>(`validator-service-${networkConfig.network}`);

const validators = validatorService.validators;
const committee = validatorService.committee;

const totalPoolStake = validators.reduce((acc, cur) => BigInt(cur.poolStake) + acc, BigInt(0));
const totalValidatorStake = committee ? BigInt(committee.totalValidatorStake) : undefined;

return {
totalPoolStake: totalPoolStake.toString(),
totalValidatorStake: totalValidatorStake.toString(),
};
}
97 changes: 97 additions & 0 deletions api/src/services/nova/validatorService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* eslint-disable import/no-unresolved */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-redundant-type-constituents */
import { Client, ValidatorResponse, CommitteeResponse } from "@iota/sdk-nova";
import cron from "node-cron";
import { ServiceFactory } from "../../factories/serviceFactory";
import logger from "../../logger";
import { INetwork } from "../../models/db/INetwork";

/**
* The collect validators interval cron expression.
* Every hour at 55 min 55 sec
*/
const COLLECT_VALIDATORS_CRON = "55 55 * * * *";

export class ValidatorService {
/**
* The current validators cache state.
*/
protected _validatorsCache: ValidatorResponse[] = [];

/**
* The current committee cache state.
*/
protected _committeeCache: CommitteeResponse | null = null;

/**
* The network in context for this client.
*/
private readonly _network: INetwork;

/**
* The client to use for requests.
*/
private readonly client: Client;

/**
* Create a new instance of ValidatorService.
* @param network The network configuration.
*/
constructor(network: INetwork) {
this._network = network;
this.client = ServiceFactory.get<Client>(`client-${network.network}`);
}

public get validators(): ValidatorResponse[] {
return this._validatorsCache;
}

public get committee(): CommitteeResponse | null {
return this._committeeCache;
}

public setupValidatorsCollection() {
const network = this._network.network;
logger.verbose(`[ValidatorService] Setting up validator collection for (${network})]`);

// eslint-disable-next-line no-void
void this.updateValidatorsCache();
// eslint-disable-next-line no-void
void this.updateCommitteeCache();

cron.schedule(COLLECT_VALIDATORS_CRON, async () => {
logger.verbose(`[ValidatorService] Collecting validators for "${this._network.network}"`);
// eslint-disable-next-line no-void
void this.updateValidatorsCache();
// eslint-disable-next-line no-void
void this.updateCommitteeCache();
});
}

private async updateValidatorsCache() {
let cursor: string | undefined;
let validators: ValidatorResponse[] = [];

do {
try {
const validatorsResponse = await this.client.getValidators(undefined, cursor);

validators = validators.concat(validatorsResponse.validators);
cursor = validatorsResponse.cursor;
} catch (e) {
logger.error(`Fetching validators failed. Cause: ${e}`);
}
} while (cursor);

this._validatorsCache = validators;
}

private async updateCommitteeCache() {
try {
this._committeeCache = await this.client.getCommittee();
} catch (e) {
logger.error(`Fetching committee failed. Cause: ${e}`);
}
}
}

0 comments on commit be8f1bf

Please sign in to comment.