diff --git a/apps/1kv-backend/templates/kusama-otv-backend.yaml b/apps/1kv-backend/templates/kusama-otv-backend.yaml index 45fd3fdbe..0f7cb4b67 100644 --- a/apps/1kv-backend/templates/kusama-otv-backend.yaml +++ b/apps/1kv-backend/templates/kusama-otv-backend.yaml @@ -17,7 +17,7 @@ spec: source: repoURL: https://w3f.github.io/helm-charts/ chart: otv-backend - targetRevision: v3.1.2 + targetRevision: v3.1.3 plugin: env: - name: HELM_VALUES diff --git a/apps/1kv-backend/templates/polkadot-otv-backend.yaml b/apps/1kv-backend/templates/polkadot-otv-backend.yaml index ea9948c59..d05ecf3e5 100644 --- a/apps/1kv-backend/templates/polkadot-otv-backend.yaml +++ b/apps/1kv-backend/templates/polkadot-otv-backend.yaml @@ -17,7 +17,7 @@ spec: source: repoURL: https://w3f.github.io/helm-charts/ chart: otv-backend - targetRevision: v3.1.2 + targetRevision: v3.1.3 plugin: env: - name: HELM_VALUES diff --git a/charts/otv-backend/Chart.yaml b/charts/otv-backend/Chart.yaml index ab0998573..1e78958d7 100644 --- a/charts/otv-backend/Chart.yaml +++ b/charts/otv-backend/Chart.yaml @@ -1,5 +1,5 @@ description: 1K Validators Backend name: otv-backend -version: v3.1.2 -appVersion: v3.1.2 +version: v3.1.3 +appVersion: v3.1.3 apiVersion: v2 diff --git a/package.json b/package.json index ca5008c39..81e5edade 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,8 @@ "typedoc-plugin-markdown": "^3.17.1" }, "dependencies": { - "@polkadot/api": "^10.11.2", - "@polkadot/rpc-provider": "^10.11.2", + "@polkadot/api": "^10.12.1", + "@polkadot/rpc-provider": "^10.12.1", "@types/ws": "^8.5.10", "axios": "^1.6.7", "chalk": "4.1.2", diff --git a/packages/common/package.json b/packages/common/package.json index a49a15382..5e20af2f2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/common", - "version": "3.1.2", + "version": "3.1.3", "description": "Services for running the Thousand Validator Program.", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/common/src/chaindata/queries/Era.ts b/packages/common/src/chaindata/queries/Era.ts index c4572db48..fd6f4ef1c 100644 --- a/packages/common/src/chaindata/queries/Era.ts +++ b/packages/common/src/chaindata/queries/Era.ts @@ -203,16 +203,17 @@ export const findEraBlockHash = async ( if (!blockHash) { return ["", "Block hash is null"]; } - const testEra = - await chaindata?.api?.query.staking.activeEra.at(blockHash); - if (testEra && testEra.isNone) { - logger.info(`Test era is none`); + const apiAt = await chaindata?.api?.at(blockHash); + if (!apiAt) { + return ["", "API at block hash is null"]; + } + + const testEra = await apiAt.query.staking.currentEra(); + if (testEra && testEra.isEmpty) { + logger.info(`Test era is empty: ${JSON.stringify(testEra)}`); return ["", "Test era is none"]; } - const testIndex = - testEra && testEra?.unwrap && testEra?.unwrap().index?.toNumber() - ? testEra?.unwrap().index.toNumber() - : 0; + const testIndex = testEra.unwrap().toNumber(); if (era == testIndex) { return [blockHash.toString(), null]; } diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 060e83827..8603c27d0 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -1,6 +1,8 @@ /// One week in milliseconds. import WS from "ws"; +export const TWO_DAYS_IN_MS = 2 * 24 * 60 * 60 * 1000; + export const FIVE_MINUTES = 5 * 60 * 1000; export const WEEK = 7 * 24 * 60 * 60 * 1000; @@ -45,6 +47,9 @@ export const CHAINDATA_SLEEP = 300; export const API_PROVIDER_TIMEOUT = 10000; +// The number of eras a nominator should wait until making a next nomination +export const NOMINATOR_SHOULD_NOMINATE_ERAS_THRESHOLD = 1; + /// List of Kusama endpoints we can switch between. export const KusamaEndpoints = [ "wss://kusama-rpc-tn.dwellir.com", @@ -100,8 +105,8 @@ export const TIME_DELAY_BLOCKS = 10850; // The number of blocks after a time delay proxy call was announced that we want to cancel the tx. Should be 36 hours export const CANCEL_THRESHOLD = 21700; -// Monitor Cron job for checking if clients have upgraded. This runs ever 15 minutes by default -export const MONITOR_CRON = "0 */15 * * * *"; +// Monitor Cron job for checking if clients have upgraded. This runs ever 3 minutes by default +export const MONITOR_CRON = "0 */3 * * * *"; // Clear Offline Time Cron Job. This runs once every sunday by default // export const CLEAR_OFFLINE_CRON = "0 0 0 * * 0"; diff --git a/packages/common/src/constraints/ScoreCandidates.ts b/packages/common/src/constraints/ScoreCandidates.ts index 7cf9a2f69..e9216f1d0 100644 --- a/packages/common/src/constraints/ScoreCandidates.ts +++ b/packages/common/src/constraints/ScoreCandidates.ts @@ -210,7 +210,9 @@ export const scoreCandidate = async ( const nominatorStakeScore = scaledNominatorStake * constraints.WEIGHT_CONFIG.NOMINATIONS_WEIGHT; - const isAlternativeClient = candidate?.implementation != "Parity Polkadot"; + const isAlternativeClient = candidate?.implementation + ? candidate?.implementation != "Parity Polkadot" + : false; const clientScore = isAlternativeClient ? constraints.WEIGHT_CONFIG.CLIENT_WEIGHT : 0; diff --git a/packages/common/src/constraints/ValidityChecks.ts b/packages/common/src/constraints/ValidityChecks.ts index 95444bb5f..85a507973 100644 --- a/packages/common/src/constraints/ValidityChecks.ts +++ b/packages/common/src/constraints/ValidityChecks.ts @@ -89,7 +89,7 @@ export const checkLatestClientVersion = async ( ): Promise => { try { const skipClientUpgrade = config.constraints?.skipClientUpgrade || false; - if (skipClientUpgrade!) { + if (!skipClientUpgrade) { if (candidate?.implementation == "Kagome Node") { await setLatestClientReleaseValidity(candidate.stash, true); return true; @@ -120,18 +120,19 @@ export const checkLatestClientVersion = async ( return true; } } else { + await setLatestClientReleaseValidity(candidate.stash, false); return false; } } else { await setLatestClientReleaseValidity(candidate.stash, true); return true; } - return true; } catch (e) { logger.error( `Error checking latest client version: ${e}`, constraintsLabel, ); + await setLatestClientReleaseValidity(candidate.stash, false); return false; } }; diff --git a/packages/common/src/db/models.ts b/packages/common/src/db/models.ts index c1c384e68..73550cd9d 100644 --- a/packages/common/src/db/models.ts +++ b/packages/common/src/db/models.ts @@ -463,6 +463,12 @@ export const CandidateSchema = new Schema({ export const CandidateModel = mongoose.model("Candidate", CandidateSchema); +export interface Era { + lastNominatedEraIndex: string; + nextNomination: number; + when: number; +} + export const EraSchema = new Schema({ // The last era a nomination took place lastNominatedEraIndex: { type: String, default: "0" }, @@ -726,6 +732,7 @@ export interface ValidatorScore { nominatorStake: number; // The randomness factor used to buffer the total randomness: number; + client: number; } export const ValidatorScoreSchema = new Schema({ @@ -763,6 +770,7 @@ export const ValidatorScoreSchema = new Schema({ country: Number, provider: Number, nominatorStake: Number, + client: Number, // The randomness factor used to buffer the total randomness: Number, }); diff --git a/packages/common/src/db/queries/Candidate.ts b/packages/common/src/db/queries/Candidate.ts index d780b0bb8..358554bf0 100644 --- a/packages/common/src/db/queries/Candidate.ts +++ b/packages/common/src/db/queries/Candidate.ts @@ -334,7 +334,7 @@ export const getIdentityName = async ( .lean() .select({ name: 1 }); - return identity?.name; + return identity?.name || null; } }; @@ -516,6 +516,7 @@ export const updateCandidateOnlineTelemetryDetails = async ( telemetryId: { $literal: telemetryNodeDetails.telemetryId }, onlineSince: { $literal: Date.now() }, offlineSince: { $literal: 0 }, + version: telemetryNodeDetails.version, implementation: { $literal: telemetryNodeDetails.nodeImplementation, }, diff --git a/packages/common/src/db/queries/Era.ts b/packages/common/src/db/queries/Era.ts index 9c4a61d91..fe455e94b 100644 --- a/packages/common/src/db/queries/Era.ts +++ b/packages/common/src/db/queries/Era.ts @@ -1,33 +1,39 @@ -import { EraModel } from "../models"; +import { Era, EraModel } from "../models"; +import logger from "../../logger"; export const setLastNominatedEraIndex = async ( index: number, ): Promise => { - const data = await EraModel.findOne({}).lean(); - if (!data) { - const eraIndex = new EraModel({ - lastNominatedEraIndex: index.toString(), - when: Date.now(), - }); - await eraIndex.save(); - return true; - } - - await EraModel.findOneAndUpdate( - { lastNominatedEraIndex: /.*/ }, - { - $set: { + try { + const data = await EraModel.findOne({}).lean(); + if (!data) { + const eraIndex = new EraModel({ lastNominatedEraIndex: index.toString(), when: Date.now(), - nextNomination: Date.now() + 86400000, + }); + await eraIndex.save(); + return true; + } + + await EraModel.findOneAndUpdate( + { lastNominatedEraIndex: /.*/ }, + { + $set: { + lastNominatedEraIndex: index.toString(), + when: Date.now(), + nextNomination: Date.now() + 86400000, + }, }, - }, - ).exec(); - return true; + ).exec(); + return true; + } catch (e) { + logger.error( + `Error setting last nominated era index: ${JSON.stringify(e)}`, + ); + return false; + } }; -export const getLastNominatedEraIndex = async (): Promise => { - return EraModel.findOne({ lastNominatedEraIndex: /[0-9]+/ }) - .lean() - .exec(); +export const getLastNominatedEraIndex = async (): Promise => { + return EraModel.findOne({ lastNominatedEraIndex: /[0-9]+/ }).lean(); }; diff --git a/packages/common/src/db/queries/Location.ts b/packages/common/src/db/queries/Location.ts index f3ecd9cc9..fff69cb72 100644 --- a/packages/common/src/db/queries/Location.ts +++ b/packages/common/src/db/queries/Location.ts @@ -9,6 +9,7 @@ import { logger } from "../../index"; import { getLatestSession } from "./Session"; import { HardwareSpec } from "../../types"; import { dbLabel } from "../index"; +import { TWO_DAYS_IN_MS } from "../../constants"; export const getAllLocations = async (): Promise => { return LocationModel.find({}).lean(); @@ -131,6 +132,22 @@ export const cleanBlankLocations = async (): Promise => { }).exec(); }; +// Remove all location data older than two days +export const cleanOldLocations = async (): Promise => { + const twoDaysAgo = Date.now() - TWO_DAYS_IN_MS; + + try { + await LocationModel.deleteMany({ updated: { $lt: twoDaysAgo } }).exec(); + return true; + } catch (error) { + logger.info( + `Error cleaning old locations: ${JSON.stringify(error)}`, + dbLabel, + ); + return false; + } +}; + // Sets a location from heartbeats export const iitExists = async (): Promise => { diff --git a/packages/common/src/db/queries/NominatorStake.ts b/packages/common/src/db/queries/NominatorStake.ts index 75676776e..5ced1250f 100644 --- a/packages/common/src/db/queries/NominatorStake.ts +++ b/packages/common/src/db/queries/NominatorStake.ts @@ -1,4 +1,7 @@ import { NominatorStake, NominatorStakeModel } from "../models"; +import { TWO_DAYS_IN_MS } from "../../constants"; +import { logger } from "../../index"; +import { dbLabel } from "../index"; export const setNominatorStake = async ( validator: string, @@ -84,3 +87,20 @@ export const getNominatorStake = async ( .sort("-era") .limit(limit ? limit : 100); }; + +export const cleanOldNominatorStakes = async (): Promise => { + const twoDaysAgo = Date.now() - TWO_DAYS_IN_MS; + + try { + await NominatorStakeModel.deleteMany({ + updated: { $lt: twoDaysAgo }, + }).exec(); + return true; + } catch (error) { + logger.info( + `Error cleaning old nominator stakes: ${JSON.stringify(error)}`, + dbLabel, + ); + return false; + } +}; diff --git a/packages/common/src/db/queries/ValidatorScore.ts b/packages/common/src/db/queries/ValidatorScore.ts index b0cdbeeed..9a45e5a6c 100644 --- a/packages/common/src/db/queries/ValidatorScore.ts +++ b/packages/common/src/db/queries/ValidatorScore.ts @@ -1,4 +1,5 @@ import { ValidatorScore, ValidatorScoreModel } from "../models"; +import { TWO_DAYS_IN_MS } from "../../constants"; export const setValidatorScore = async ( address: string, @@ -25,6 +26,7 @@ export const setValidatorScore = async ( nominatorStake, randomness, updated, + client, } = score; const data = await ValidatorScoreModel.findOne({ @@ -54,6 +56,7 @@ export const setValidatorScore = async ( provider, nominatorStake, randomness, + client, }); await score.save(); return true; @@ -82,6 +85,7 @@ export const setValidatorScore = async ( country, provider, nominatorStake, + client, randomness, }, ).exec(); @@ -107,7 +111,7 @@ export const getValidatorScore = async ( export const getLatestValidatorScore = async ( address: string, -): Promise => { +): Promise => { return ValidatorScoreModel.findOne({ address: address }, { _id: 0, __v: 0 }) .sort({ session: -1 }) .limit(1) @@ -115,10 +119,7 @@ export const getLatestValidatorScore = async ( }; export const deleteOldValidatorScores = async (): Promise => { - const FIVE_MINUTES = 300000; - const ONE_WEEK = 604800016.56; - const ONE_MONTH = 2629800000; - const timeWindow = Date.now() - ONE_WEEK; + const timeWindow = Date.now() - TWO_DAYS_IN_MS; const scoreToDelete = await ValidatorScoreModel.find({ updated: { $lt: timeWindow }, }).exec(); diff --git a/packages/common/src/db/queries/Validators.ts b/packages/common/src/db/queries/Validators.ts index c09522bc0..856488963 100644 --- a/packages/common/src/db/queries/Validators.ts +++ b/packages/common/src/db/queries/Validators.ts @@ -5,7 +5,7 @@ import { ValidatorSet, ValidatorSetModel, } from "../models"; -import { allCandidates } from "./Candidate"; +import { allCandidates, getIdentityAddresses } from "./Candidate"; import { NextKeys } from "../../chaindata/queries/ValidatorPref"; export const setValidatorSet = async ( @@ -44,10 +44,7 @@ export const getLatestValidatorSet = async (): Promise => { }; export const getAllValidatorSets = async (): Promise => { - return ValidatorSetModel.find({}) - .sort({ era: -1 }) - .lean() - .exec(); + return ValidatorSetModel.find({}).sort({ era: -1 }).lean(); }; export const validatorSetExistsForEra = async ( @@ -170,3 +167,36 @@ export const hasBeefyDummy = async (address: string): Promise => { const validator = await getValidator(address); return validator?.keys?.beefy?.slice(0, 10) == "0x62656566"; }; + +// TODO: add tests +// Returns the number of eras a validator stash has been active +export const getValidatorActiveEras = async ( + stash: string, +): Promise => { + let count = 0; + const validatorSets = await getAllValidatorSets(); + for (const era of validatorSets) { + if (era.validators.includes(stash)) { + count++; + } + } + return count; +}; + +// TODO: add tests +// return the number of eras +export const getIdentityValidatorActiveEras = async ( + address: string, +): Promise => { + const identityAddresses = await getIdentityAddresses(address); + let count = 0; + const validatorSets = await getAllValidatorSets(); + for (const era of validatorSets) { + if ( + era.validators.some((validator) => identityAddresses.includes(validator)) + ) { + count++; + } + } + return count; +}; diff --git a/packages/common/src/nominator/NominatorChainInfo.ts b/packages/common/src/nominator/NominatorChainInfo.ts new file mode 100644 index 000000000..26f1e8e14 --- /dev/null +++ b/packages/common/src/nominator/NominatorChainInfo.ts @@ -0,0 +1,126 @@ +import Nominator from "./nominator"; +import { queries } from "../index"; +import { NOMINATOR_SHOULD_NOMINATE_ERAS_THRESHOLD } from "../constants"; +import { NominatorState } from "../types"; + +// Query on-chain info for a nominator +export const getNominatorChainInfo = async (nominator: Nominator) => { + const stash = await nominator.stash(); + const isBonded = await nominator.chaindata.isBonded(stash); + const [bonded, err] = await nominator.chaindata.getDenomBondedAmount(stash); + const currentBlock = (await nominator.chaindata.getLatestBlock()) || 0; + + const currentEra = (await nominator.chaindata.getCurrentEra()) || 0; + const lastNominationEra = + (await nominator.chaindata.getNominatorLastNominationEra(stash)) || 0; + nominator.lastEraNomination = lastNominationEra; + const currentTargets = + (await nominator.chaindata.getNominatorCurrentTargets(stash)) || []; + const currentNamedTargets = await Promise.all( + currentTargets.map(async (target) => { + const kyc = await queries.isKYC(target); + let name = await queries.getIdentityName(target); + if (!name) { + name = + (await nominator.chaindata.getFormattedIdentity(target))?.name || ""; + } + + const scoreResult = await queries.getLatestValidatorScore(target); + const score = scoreResult && scoreResult.total ? scoreResult.total : 0; + + return { + stash: target, + name: name || "", + kyc: kyc || false, + score: score, + }; + }), + ); + + const proxyAnnouncements = await queries.getAccountDelayedTx( + nominator.bondedAddress, + ); + + const namedProxyTargets = await Promise.all( + (proxyAnnouncements || []).map(async (announcement) => { + const namedTargets = await Promise.all( + announcement.targets.map(async (target) => { + const kyc = await queries.isKYC(target); + let name = await queries.getIdentityName(target); + + if (!name) { + const formattedIdentity = + await nominator.chaindata.getFormattedIdentity(target); + name = formattedIdentity?.name || ""; + } + + const scoreResult = await queries.getLatestValidatorScore(target); + const score = + scoreResult && scoreResult.total ? scoreResult.total : 0; + + return { + stash: target, + name: name || "", + kyc: kyc || false, + score: score, + }; + }), + ); + const executionMsTime = + (nominator.proxyDelay + currentBlock - announcement.number) * 6 * 1000; + return { + ...announcement, + targets: namedTargets, + executionTime: executionMsTime, + }; + }), + ); + + const shouldNominate = + bonded > 50 && + isBonded && + currentEra - lastNominationEra >= + NOMINATOR_SHOULD_NOMINATE_ERAS_THRESHOLD && + proxyAnnouncements.length == 0; + + let status; + + if (namedProxyTargets.length > 0) { + `Pending Proxy Execution at #${namedProxyTargets[0].number}`; + } else if (shouldNominate) { + status = "Awaiting New Nomination"; + } else if (lastNominationEra == 0) { + status = "Not Nominating Anyone"; + } else { + status = `Nominating, last nomination era: ${lastNominationEra} current era: ${currentEra}`; + } + + let state; + if (shouldNominate) { + state = NominatorState.ReadyToNominate; + } else if (namedProxyTargets.length > 0) { + state = NominatorState.AwaitingProxyExecution; + } else if (lastNominationEra == 0) { + state = NominatorState.NotNominating; + } else if (namedProxyTargets.length == 0 && lastNominationEra > 0) { + state = NominatorState.Nominated; + } + + const stale = + isBonded && + currentEra - lastNominationEra > 8 && + proxyAnnouncements.length == 0 && + bonded > 50; + + return { + state: state, + status: status, + isBonded: isBonded, + bondedAmount: Number(bonded), + lastNominationEra: lastNominationEra, + currentTargets: currentNamedTargets, + proxyAnnouncements: namedProxyTargets, + shouldNominate: shouldNominate, + stale: stale, + }; +}; diff --git a/packages/common/src/nominator/NominatorTx.ts b/packages/common/src/nominator/NominatorTx.ts index e45361f67..7a1273c5a 100644 --- a/packages/common/src/nominator/NominatorTx.ts +++ b/packages/common/src/nominator/NominatorTx.ts @@ -2,9 +2,10 @@ import logger from "../logger"; import { blake2AsHex } from "@polkadot/util-crypto"; import { DelayedTx } from "../db"; import { ChainData, queries } from "../index"; -import Nominator, { nominatorLabel } from "./nominator"; import { ApiPromise } from "@polkadot/api"; import MatrixBot from "../matrix"; +import Nominator, { nominatorLabel } from "./nominator"; +import { NominatorState } from "../types"; // Sends a Proxy Delay Nominate Tx for a given nominator // TODO: unit tests @@ -20,7 +21,8 @@ export const sendProxyDelayTx = async ( `{Nominator::nominate::proxy} starting tx for ${nominator.address} with proxy delay ${nominator.proxyDelay} blocks`, nominatorLabel, ); - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ + state: NominatorState.Nominating, status: `[noninate] starting proxy delay tx`, updated: Date.now(), stale: false, @@ -34,7 +36,7 @@ export const sendProxyDelayTx = async ( `{Nominator::nominate} there was an error getting the current block`, nominatorLabel, ); - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ status: `[noninate] err: no current block`, updated: Date.now(), stale: false, @@ -55,7 +57,8 @@ export const sendProxyDelayTx = async ( callHash, }; await queries.addDelayedTx(delayedTx); - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ + state: NominatorState.Nominating, status: `[noninate] tx: ${JSON.stringify(delayedTx)}`, updated: Date.now(), stale: false, @@ -64,7 +67,8 @@ export const sendProxyDelayTx = async ( const allProxyTxs = await queries.getAllDelayedTxs(); const didSend = await nominator.signAndSendTx(tx); - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ + state: NominatorState.AwaitingProxyExecution, status: `Announced Proxy Tx: ${didSend}`, nextTargets: targets, updated: Date.now(), @@ -79,7 +83,7 @@ export const sendProxyDelayTx = async ( nominatorLabel, ); logger.error(JSON.stringify(e), nominatorLabel); - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ status: `Proxy Delay Error: ${JSON.stringify(e)}`, updated: Date.now(), }); @@ -144,16 +148,20 @@ export const sendProxyTx = async ( targets.map(async (val) => { const name = await queries.getIdentityName(val); const kyc = await queries.isKYC(val); + const scoreResult = await queries.getLatestValidatorScore(val); + const score = scoreResult && scoreResult.total ? scoreResult.total : 0; return { address: val, - name: name, - kyc: kyc, + name: name || "", + kyc: kyc || false, + score: score, }; }), ); - const currentEra = await chaindata.getCurrentEra(); + const currentEra = (await chaindata.getCurrentEra()) || 0; - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ + state: NominatorState.AwaitingProxyExecution, status: "Submitted Proxy Tx", currentTargets: namedTargets, updated: Date.now(), @@ -172,7 +180,7 @@ export const sendProxyTx = async ( nominatorLabel, ); logger.error(JSON.stringify(e), nominatorLabel); - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ status: `Proxy Error: ${JSON.stringify(e)}`, updated: Date.now(), }); diff --git a/packages/common/src/nominator/__mocks__/nominator.ts b/packages/common/src/nominator/__mocks__/nominator.ts index a226b9e99..ed8e876b4 100644 --- a/packages/common/src/nominator/__mocks__/nominator.ts +++ b/packages/common/src/nominator/__mocks__/nominator.ts @@ -100,6 +100,10 @@ class NominatorMock { async sendStakingTx(tx: any, targets: string[]): Promise { return true; } + + async updateNominatorStatus(): Promise { + return true; + } } export default NominatorMock; diff --git a/packages/common/src/nominator/nominator.ts b/packages/common/src/nominator/nominator.ts index e8fe7d9fb..9197440ac 100644 --- a/packages/common/src/nominator/nominator.ts +++ b/packages/common/src/nominator/nominator.ts @@ -7,38 +7,11 @@ import { ChainData, Constants, queries, Types } from "../index"; import logger from "../logger"; import EventEmitter from "eventemitter3"; import { sendProxyDelayTx, sendProxyTx } from "./NominatorTx"; +import { getNominatorChainInfo } from "./NominatorChainInfo"; +import { NominatorState, NominatorStatus } from "../types"; export const nominatorLabel = { label: "Nominator" }; -export interface NominatorStatus { - status?: string; - isBonded?: boolean; - bondedAddress?: string; - bondedAmount?: number; - stashAddress?: string; - proxyAddress?: string; - isProxy?: boolean; - proxyDelay?: number; - isNominating?: boolean; - lastNominationEra?: number; - lastNominationTime?: number; - currentTargets?: - | string[] - | { - stash?: string; - name?: string; - kyc?: boolean; - score?: string | number; - }[]; - nextTargets?: string[]; - proxyTxs?: any[]; - updated: number; - rewardDestination?: string; - stale?: boolean; - dryRun?: boolean; - shouldNominate?: boolean; -} - export default class Nominator extends EventEmitter { public currentlyNominating: Types.Stash[] = []; @@ -57,12 +30,7 @@ export default class Nominator extends EventEmitter { // The amount of blocks for a time delay proxy private _proxyDelay: number; - private _canNominate: { canNominate: boolean; reason: string } = { - canNominate: false, - reason: "", - }; - - public lastEraNomination: number; + public lastEraNomination = 0; public _shouldNominate = false; @@ -99,15 +67,6 @@ export default class Nominator extends EventEmitter { this._proxyDelay = cfg.proxyDelay == 0 ? cfg.proxyDelay : Constants.TIME_DELAY_BLOCKS; - logger.info( - `{nominator::proxyDelay} config proxy delay: ${cfg.proxyDelay}`, - nominatorLabel, - ); - logger.info( - `{nominator::proxy} nominator proxy delay: ${this._proxyDelay}`, - nominatorLabel, - ); - const keyring = new Keyring({ type: "sr25519", }); @@ -120,9 +79,9 @@ export default class Nominator extends EventEmitter { : this.signer.address; logger.info( - `(Nominator::constructor) Nominator signer spawned: ${this.address} | ${ + `(Nominator::constructor) Nominator spawned: ${this.address} | ${ this._isProxy ? "Proxy" : "Controller" - }`, + } ${this._proxyDelay ? `| Delay: ${this._proxyDelay}` : ""} bonded address: ${this._bondedAddress}`, nominatorLabel, ); } @@ -131,143 +90,80 @@ export default class Nominator extends EventEmitter { return this._status; }; - public updateNominatorStatus = (newStatus: NominatorStatus) => { - this._status = { ...this._status, ...newStatus }; - }; + public async updateNominatorStatus(newStatus: NominatorStatus) { + // Always update on-chain data for status + const nominatorInfo = await getNominatorChainInfo(this); + const { + isBonded, + bondedAmount, + lastNominationEra, + proxyAnnouncements, + stale, + } = nominatorInfo; + + this._status = { + ...this._status, + ...newStatus, + isBonded, + bondedAmount, + lastNominationEra, + proxyTxs: proxyAnnouncements, + stale, + }; + } public async shouldNominate(): Promise { const stash = await this.stash(); const isBonded = await this.chaindata.isBonded(stash); const [bonded, err] = await this.chaindata.getDenomBondedAmount(stash); const proxyTxs = await queries.getAccountDelayedTx(this.bondedAddress); + const lastNominationEra = + (await this.chaindata.getNominatorLastNominationEra(stash)) || 0; + this.lastEraNomination = lastNominationEra; const currentEra = (await this.chaindata.getCurrentEra()) || 0; this._shouldNominate = isBonded && bonded > 50 && - currentEra - this.lastEraNomination >= 1 && + currentEra - lastNominationEra >= 1 && proxyTxs.length == 0; return this._shouldNominate; } - public async init(): Promise { + public async init(): Promise { try { - const stash = await this.stash(); - const isBonded = await this.chaindata.isBonded(stash); - const [bonded, err] = await this.chaindata.getDenomBondedAmount(stash); - const currentBlock = await this.chaindata.getLatestBlock(); - - const currentEra = (await this.chaindata.getCurrentEra()) || 0; - const lastNominationEra = - (await this.chaindata.getNominatorLastNominationEra(stash)) || 0; - this.lastEraNomination = lastNominationEra; - const currentTargets = - (await this.chaindata.getNominatorCurrentTargets(stash)) || []; - const currentNamedTargets = await Promise.all( - currentTargets.map(async (target) => { - const kyc = await queries.isKYC(target); - let name = await queries.getIdentityName(target); - if (!name) { - name = (await this.chaindata.getFormattedIdentity(target))?.name; - } - - const scoreResult = await queries.getLatestValidatorScore(target); - const score = - scoreResult && scoreResult.total ? scoreResult.total : 0; - - return { - stash: target, - name: name, - kyc: kyc, - score: score, - }; - }), - ); - - const proxyAnnouncements = await queries.getAccountDelayedTx( - this.bondedAddress, - ); - - const namedProxyTargets = await Promise.all( - (proxyAnnouncements || []).map(async (announcement) => { - const namedTargets = await Promise.all( - announcement.targets.map(async (target) => { - const kyc = await queries.isKYC(target); - let name = await queries.getIdentityName(target); - - if (!name) { - const formattedIdentity = - await this.chaindata.getFormattedIdentity(target); - name = formattedIdentity?.name; - } - - const scoreResult = await queries.getLatestValidatorScore(target); - const score = - scoreResult && scoreResult.total ? scoreResult.total : 0; - - return { - stash: target, - name: name, - kyc: kyc, - score: score, - }; - }), - ); - const executionMsTime = - (this._proxyDelay + currentBlock - announcement.number) * 6 * 1000; - return { - ...announcement, - targets: namedTargets, - executionTime: executionMsTime, - }; - }), - ); - - this._shouldNominate = - bonded > 50 && - isBonded && - currentEra - lastNominationEra >= 1 && - proxyAnnouncements.length == 0; - - const rewardDestination = await this.payee(); - - let nominationStatus; - if (proxyAnnouncements.length > 0) { - nominationStatus = "Announced Proxy Tx"; - } else if (this._shouldNominate) { - nominationStatus = "Initialized"; - } else { - nominationStatus = "Existing Recent Nomination"; - } - - const stale = - isBonded && - currentEra - lastNominationEra > 8 && - proxyAnnouncements.length == 0 && - bonded > 50; + const nominatorInfo = await getNominatorChainInfo(this); + const { + state, + status: nominatorStatus, + isBonded, + bondedAmount, + currentTargets, + lastNominationEra, + proxyAnnouncements, + stale, + } = nominatorInfo; const status: NominatorStatus = { - status: nominationStatus, + state: state, + status: nominatorStatus, bondedAddress: this.bondedAddress, stashAddress: await this.stash(), - bondedAmount: Number(bonded), + bondedAmount: bondedAmount, isBonded: isBonded, isProxy: this._isProxy, proxyDelay: this._proxyDelay, proxyAddress: this.signer.address, - rewardDestination: rewardDestination, + rewardDestination: await this.payee(), lastNominationEra: lastNominationEra, - currentTargets: currentNamedTargets, - proxyTxs: namedProxyTargets, + currentTargets: currentTargets, + proxyTxs: proxyAnnouncements, stale: stale, dryRun: this._dryRun, updated: Date.now(), shouldNominate: this._shouldNominate, }; - this.updateNominatorStatus(status); - this._canNominate = { - canNominate: isBonded, - reason: isBonded ? "Bonded" : "Not bonded", - }; + await this.updateNominatorStatus(status); + return status; } catch (e) { logger.error(`Error getting status for ${this.bondedAddress}: ${e}`); @@ -362,7 +258,8 @@ export default class Nominator extends EventEmitter { try { if (this._dryRun) { logger.info(`DRY RUN ENABLED, SKIPPING TX`, nominatorLabel); - this.updateNominatorStatus({ + await this.updateNominatorStatus({ + state: NominatorState.Nominating, status: `[signAndSend] DRY RUN TX`, updated: Date.now(), stale: false, @@ -371,7 +268,8 @@ export default class Nominator extends EventEmitter { } else { logger.info(`Sending tx: ${tx.method.toString()}`, nominatorLabel); await tx.signAndSend(this.signer); - this.updateNominatorStatus({ + await this.updateNominatorStatus({ + state: NominatorState.Nominated, status: `[signAndSend] signed and sent tx`, updated: Date.now(), stale: false, @@ -382,7 +280,7 @@ export default class Nominator extends EventEmitter { } catch (e) { logger.error(`Error sending tx: `, nominatorLabel); logger.error(JSON.stringify(e), nominatorLabel); - this.updateNominatorStatus({ + await this.updateNominatorStatus({ status: `[signAndSend] Error signing and sending tx: ${JSON.stringify(e)}`, updated: Date.now(), stale: false, @@ -402,14 +300,15 @@ export default class Nominator extends EventEmitter { return false; } - const currentEra = await this.chaindata.getCurrentEra(); + const currentEra = (await this.chaindata.getCurrentEra()) || 0; const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominating, status: `[nominate] start`, updated: Date.now(), stale: false, }; - this.updateNominatorStatus(nominatorStatus); + await this.updateNominatorStatus(nominatorStatus); let isBonded; try { const stash = await this.stash(); @@ -421,7 +320,8 @@ export default class Nominator extends EventEmitter { } logger.info(`nominator is bonded: ${isBonded}`, nominatorLabel); - this.updateNominatorStatus({ + await this.updateNominatorStatus({ + state: NominatorState.Nominating, status: `[nominate] bonded; ${isBonded}`, updated: Date.now(), stale: false, @@ -438,7 +338,8 @@ export default class Nominator extends EventEmitter { ); // Start an announcement for a delayed proxy tx if (this._isProxy && this._proxyDelay > 0) { - this.updateNominatorStatus({ + await this.updateNominatorStatus({ + state: NominatorState.Nominating, status: `[nominate] proxy ${this._isProxy}; delay ${this._proxyDelay}`, updated: Date.now(), stale: false, @@ -519,36 +420,37 @@ export default class Nominator extends EventEmitter { // If Dry Run is enabled in the config, nominations will be stubbed but not executed if (this._dryRun) { logger.info(`DRY RUN ENABLED, SKIPPING TX`, nominatorLabel); - const currentEra = await this.chaindata.getCurrentEra(); + const currentEra = (await this.chaindata.getCurrentEra()) || 0; const namedTargets = await Promise.all( targets.map(async (target) => { - const kyc = await queries.isKYC(target); - let name = await queries.getIdentityName(target); + const kyc = (await queries.isKYC(target)) || false; + let name = (await queries.getIdentityName(target)) || ""; // Fetch name using chaindata.getFormattedIdentity only if the name wasn't found initially if (!name) { const formattedIdentity = await this.chaindata.getFormattedIdentity(target); - name = formattedIdentity?.name; + name = formattedIdentity?.name || ""; } return { stash: target, - name, // shorthand for name: name - kyc, // shorthand for kyc: kyc + name: name || "", + kyc: kyc || false, score: 0, }; }), ); const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominating, status: `Dry Run: Nominated ${targets.length} validators`, updated: Date.now(), stale: false, currentTargets: namedTargets, lastNominationEra: currentEra, }; - this.updateNominatorStatus(nominatorStatus); + await this.updateNominatorStatus(nominatorStatus); // `dryRun` return as blockhash is checked elsewhere to finish the hook of writing db entries return [false, "dryRun"]; } @@ -683,37 +585,39 @@ export default class Nominator extends EventEmitter { break; } }); - const currentEra = await this.chaindata.getCurrentEra(); + const currentEra = (await this.chaindata.getCurrentEra()) || 0; const namedTargets = await Promise.all( targets.map(async (target) => { const kyc = await queries.isKYC(target); let name = await queries.getIdentityName(target); if (!name) { - name = (await this.chaindata.getFormattedIdentity(target))?.name; + name = + (await this.chaindata.getFormattedIdentity(target))?.name || ""; } const score = await queries.getLatestValidatorScore(target); return { stash: target, - name: name, - kyc: kyc, - score: score && score[0] && score[0].total ? score[0].total : 0, + name: name || "", + kyc: kyc || false, + score: score && score && score?.total ? score?.total : 0, }; }), ); const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominated, status: `Nominated ${targets.length} validators: ${didSend} ${finalizedBlockHash}`, updated: Date.now(), stale: false, currentTargets: namedTargets, lastNominationEra: currentEra, }; - this.updateNominatorStatus(nominatorStatus); + await this.updateNominatorStatus(nominatorStatus); return [didSend, finalizedBlockHash || null]; // Change to return undefined } catch (e) { logger.error(`Error sending tx: ${JSON.stringify(e)}`, nominatorLabel); - return [false, e]; + return [false, JSON.stringify(e)]; } }; } diff --git a/packages/common/src/scorekeeper/Nominating.ts b/packages/common/src/scorekeeper/Nominating.ts index eab542e52..4e793ef87 100644 --- a/packages/common/src/scorekeeper/Nominating.ts +++ b/packages/common/src/scorekeeper/Nominating.ts @@ -10,7 +10,8 @@ import { ChainData, queries, Util } from "../index"; import ApiHandler from "../ApiHandler/ApiHandler"; import MatrixBot from "../matrix"; import { ConfigSchema } from "../config"; -import Nominator, { NominatorStatus } from "../nominator/nominator"; +import Nominator from "../nominator/nominator"; +import { NominatorState, NominatorStatus } from "../types"; // Takes in a list of valid Candidates, and will nominate them based on the nominator groups export const doNominations = async ( @@ -31,20 +32,6 @@ export const doNominations = async ( return null; } - for (const nom of nominatorGroups) { - const nominatorStatus: NominatorStatus = { - status: `Doing Nominations.....`, - updated: Date.now(), - stale: false, - }; - nom.updateNominatorStatus(nominatorStatus); - } - - const allTargets = candidates.map((c) => { - return { stash: c.stash }; - }); - let counter = 0; - const currentEra = await chaindata.getCurrentEra(); if (!currentEra) { logger.error( @@ -54,22 +41,32 @@ export const doNominations = async ( return null; } + // The list of all valid Validators to nominate + const allTargets = candidates.map((c) => { + return { stash: c.stash }; + }); + + // A counter to keep track of the number of nominations + let counter = 0; + for (const nominator of nominatorGroups) { - const nomStash = await nominator.stash(); - const nominatorLastNominated = - await chaindata.getNominatorLastNominationEra(nomStash); - if (nominatorLastNominated + 4 > currentEra) { + const stash = await nominator.stash(); + const shouldNominate = await nominator.shouldNominate(); + if (!shouldNominate) { logger.info( - `Nominator ${nomStash} has already nominated this era: ${nominatorLastNominated}`, + `Nominator ${stash} has already nominated in era: ${nominator.lastEraNomination} (current era: ${currentEra}) - Skipping`, ); continue; } + const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominating, status: `Nominating...`, updated: Date.now(), stale: false, }; - nominator.updateNominatorStatus(nominatorStatus); + await nominator.updateNominatorStatus(nominatorStatus); + // The number of nominations to do per nominator account // This is either hard coded, or set to "auto", meaning it will find a dynamic amount of validators // to nominate based on the lowest staked validator in the validator set @@ -78,7 +75,6 @@ export const doNominations = async ( if (!api || !denom) return null; const autoNom = await autoNumNominations(api, nominator); const { nominationNum } = autoNom; - const stash = await nominator.stash(); logger.info( `Nominator ${stash} ${nominator.isProxy ? "Proxy" : "Non-Proxy"} with delay ${nominator.proxyDelay} blocks nominate ${nominationNum} validators`, diff --git a/packages/common/src/scorekeeper/NumNominations.ts b/packages/common/src/scorekeeper/NumNominations.ts index 25df39f0c..53eaa3d2c 100644 --- a/packages/common/src/scorekeeper/NumNominations.ts +++ b/packages/common/src/scorekeeper/NumNominations.ts @@ -5,9 +5,10 @@ */ import { ApiPromise } from "@polkadot/api"; import { scorekeeperLabel } from "./scorekeeper"; -import Nominator, { NominatorStatus } from "../nominator/nominator"; +import Nominator from "../nominator/nominator"; import { Constants } from "../index"; import logger from "../logger"; +import { NominatorState, NominatorStatus } from "../types"; /** * Automatically determines the number of validators a nominator can nominate based on their available balance @@ -36,11 +37,12 @@ export const autoNumNominations = async ( nominator: Nominator, ): Promise => { const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominating, status: `Calculating how many validators to nominate...`, updated: Date.now(), stale: false, }; - nominator.updateNominatorStatus(nominatorStatus); + await nominator.updateNominatorStatus(nominatorStatus); const denom = (await nominator?.chaindata?.getDenom()) || 0; diff --git a/packages/common/src/scorekeeper/Round.ts b/packages/common/src/scorekeeper/Round.ts index 1191caf5a..8507c87e5 100644 --- a/packages/common/src/scorekeeper/Round.ts +++ b/packages/common/src/scorekeeper/Round.ts @@ -11,9 +11,10 @@ import { OTV } from "../constraints/constraints"; import { ConfigSchema } from "../config"; import MatrixBot from "../matrix"; import ApiHandler from "../ApiHandler/ApiHandler"; -import Nominator, { NominatorStatus } from "../nominator/nominator"; +import Nominator from "../nominator/nominator"; import { jobStatusEmitter } from "../Events"; import { JobNames } from "./jobs/JobConfigs"; +import { NominatorState, NominatorStatus } from "../types"; /// Handles the beginning of a new round. // - Gets the current era @@ -34,16 +35,6 @@ export const startRound = async ( if (nominating) return []; nominating = true; - const shouldNominatePromises = nominatorGroups.map(async (nom) => { - return { - nominator: nom, - shouldNominate: await nom.shouldNominate(), - }; - }); - const resolvedNominators = await Promise.all(shouldNominatePromises); - const filteredNominators = resolvedNominators - .filter((nom) => nom.shouldNominate) - .map((nom) => nom.nominator); const now = new Date().getTime(); // The nominations sent now won't be active until the next era. @@ -58,13 +49,14 @@ export const startRound = async ( `New round is starting! Era ${newEra} will begin new nominations.`, ); - for (const nom of filteredNominators) { + for (const nom of nominatorGroups) { const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominating, status: `Round Started`, updated: Date.now(), stale: false, }; - nom.updateNominatorStatus(nominatorStatus); + await nom.updateNominatorStatus(nominatorStatus); } const proxyTxs = await queries.getAllDelayedTxs(); @@ -97,23 +89,25 @@ export const startRound = async ( `[${index}/${allCandidates.length}] checked ${candidate.name} ${isValid ? "Valid" : "Invalid"} [${index}/${allCandidates.length}]`, scorekeeperLabel, ); - for (const nom of filteredNominators) { + for (const nom of nominatorGroups) { const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominating, status: `[${index}/${allCandidates.length}] ${candidate.name} ${isValid ? "✅ " : "❌"}`, updated: Date.now(), stale: false, }; - nom.updateNominatorStatus(nominatorStatus); + await nom.updateNominatorStatus(nominatorStatus); } } - for (const nom of filteredNominators) { + for (const nom of nominatorGroups) { const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominating, status: `Scoring Candidates...`, updated: Date.now(), stale: false, }; - nom.updateNominatorStatus(nominatorStatus); + await nom.updateNominatorStatus(nominatorStatus); } // Score all candidates @@ -126,7 +120,7 @@ export const startRound = async ( const scoredCandidate = { name: candidate.name, stash: candidate.stash, - total: score.total, + total: score?.total || 0, }; return scoredCandidate; }), @@ -143,7 +137,7 @@ export const startRound = async ( // TODO unit test that assets this value const numValidatorsNominated = await doNominations( sortedCandidates, - filteredNominators, + nominatorGroups, chaindata, handler, bot, @@ -157,29 +151,21 @@ export const startRound = async ( scorekeeperLabel, ); await queries.setLastNominatedEraIndex(newEra); - for (const nom of filteredNominators) { + for (const nom of nominatorGroups) { const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominated, status: `Nominated!`, updated: Date.now(), stale: false, lastNominationEra: newEra, }; - nom.updateNominatorStatus(nominatorStatus); + await nom.updateNominatorStatus(nominatorStatus); } } else { logger.info( `${numValidatorsNominated} nominated this round, lastNominatedEra not set...`, scorekeeperLabel, ); - for (const nom of filteredNominators) { - const nominatorStatus: NominatorStatus = { - status: `${numValidatorsNominated} nominated, era not set!`, - updated: Date.now(), - stale: false, - lastNominationEra: newEra, - }; - nom.updateNominatorStatus(nominatorStatus); - } } nominating = false; diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/CancelJob.ts b/packages/common/src/scorekeeper/jobs/specificJobs/CancelJob.ts index b901114a2..69b145873 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/CancelJob.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/CancelJob.ts @@ -20,7 +20,7 @@ export const cancelJob = async ( const latestBlock = await chaindata.getLatestBlock(); if (!latestBlock) { logger.error(`latest block is null`, cronLabel); - return; + return false; } const threshold = latestBlock - 1.2 * config?.proxy?.timeDelayBlocks; diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/ConstraintsJob.ts b/packages/common/src/scorekeeper/jobs/specificJobs/ConstraintsJob.ts index ed81ec78b..520d5c5be 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/ConstraintsJob.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/ConstraintsJob.ts @@ -162,7 +162,7 @@ export const scoreJob = async ( name: JobNames.Score, progress, updated: Date.now(), - iteration: `[${score.toFixed(1)}] ${candidate.name}`, + iteration: `[${score?.toFixed(1)}] ${candidate.name}`, }); logger.info( diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/EraPointsJob.ts b/packages/common/src/scorekeeper/jobs/specificJobs/EraPointsJob.ts index abafe06bc..fffa5305a 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/EraPointsJob.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/EraPointsJob.ts @@ -16,18 +16,39 @@ export class EraPointsJob extends Job { export const individualEraPointsJob = async ( chaindata: ChainData, eraIndex: number, -) => { - const erapoints = await queries.getTotalEraPoints(eraIndex); +): Promise => { + try { + const erapoints = await queries.getTotalEraPoints(eraIndex); - // If Era Points for the era exist, and are what the total should be, skip - if (!!erapoints && erapoints.totalEraPoints >= 0 && erapoints.median) { - return; - } else { - const data = await chaindata.getTotalEraPoints(eraIndex); - if (data) { - const { era, total, validators } = data; - await queries.setTotalEraPoints(era, total, validators); + // If Era Points for the era exist, and are what the total should be, skip + if (!!erapoints && erapoints.totalEraPoints >= 0 && erapoints.median) { + return false; + } else { + const data = await chaindata.getTotalEraPoints(eraIndex); + if ( + data && + data.era && + data.total && + data.validators && + data.validators.length > 0 + ) { + const { era, total, validators } = data; + await queries.setTotalEraPoints(era, total, validators); + } else { + logger.error( + `Error getting total era points for era: ${JSON.stringify(data)} is null`, + erapointsLabel, + ); + return false; + } } + return true; + } catch (e) { + logger.error( + `Error running individual era points job: ${JSON.stringify(e)}`, + erapointsLabel, + ); + return false; } }; export const eraPointsJob = async ( diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/EraStatsJob.ts b/packages/common/src/scorekeeper/jobs/specificJobs/EraStatsJob.ts index 7ab67ff49..d26aac695 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/EraStatsJob.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/EraStatsJob.ts @@ -17,6 +17,9 @@ export const eraStatsJob = async ( ): Promise => { try { const { chaindata } = metadata; + + await setValidatorRanks(); + const currentSession = await chaindata.getSession(); const currentEra = await chaindata.getCurrentEra(); const validators = await chaindata.currentValidators(); diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/ExecutionJob.ts b/packages/common/src/scorekeeper/jobs/specificJobs/ExecutionJob.ts index b8cd0d429..c6cb1bd3f 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/ExecutionJob.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/ExecutionJob.ts @@ -4,7 +4,7 @@ import { Constants, queries, Util } from "../../../index"; import { cronLabel } from "../cron/StartCronJobs"; import { jobStatusEmitter } from "../../../Events"; import { JobNames } from "../JobConfigs"; -import { NominatorStatus } from "../../../nominator/nominator"; +import { NominatorState, NominatorStatus } from "../../../types"; export class ExecutionJob extends Job { constructor(jobConfig: JobConfig, jobRunnerMetadata: JobRunnerMetadata) { @@ -28,19 +28,19 @@ export const executionJob = async ( const latestBlock = await chaindata.getLatestBlock(); if (!latestBlock) { logger.error(`latest block is null`, cronLabel); - return; + return false; } const api = handler.getApi(); if (!api) { logger.error(`api is null`, cronLabel); - return; + return false; } const era = await chaindata.getCurrentEra(); if (!era) { logger.error(`current era is null`, cronLabel); - return; + return false; } const allDelayed = await queries.getAllDelayedTxs(); @@ -93,7 +93,7 @@ export const executionJob = async ( updated: Date.now(), stale: false, }; - nominator.updateNominatorStatus(nominatorStatus); + await nominator.updateNominatorStatus(nominatorStatus); if (bot) { await bot.sendMessage( `@room ${target} has invalid commission: ${commission}`, @@ -117,7 +117,7 @@ export const executionJob = async ( updated: Date.now(), stale: false, }; - nominator.updateNominatorStatus(nominatorStatus); + await nominator.updateNominatorStatus(nominatorStatus); await nominator.cancelTx(announcement); } } @@ -128,7 +128,8 @@ export const executionJob = async ( (validCommission && dataNum + Number(timeDelayBlocks) <= latestBlock); if (shouldExecute) { - nominator.updateNominatorStatus({ + await nominator.updateNominatorStatus({ + state: NominatorState.Nominating, status: `Starting Delayed Execution for ${callHash} - ${dataNum}`, updated: Date.now(), stale: false, @@ -139,11 +140,12 @@ export const executionJob = async ( ); const nominatorStatus: NominatorStatus = { + state: NominatorState.NotNominating, status: `${isDryRun ? "DRY RUN: " : ""} Executing Valid Proxy Tx: ${data.callHash}`, updated: Date.now(), stale: false, }; - nominator.updateNominatorStatus(nominatorStatus); + await nominator.updateNominatorStatus(nominatorStatus); // time to execute @@ -168,11 +170,12 @@ export const executionJob = async ( // `dryRun` is a special value for the returned block hash that is used to test the execution job without actually sending the transaction if (didSend || finalizedBlockHash == "dryRun") { const nominatorStatus: NominatorStatus = { + state: NominatorState.Nominated, status: `Executed Proxy Tx: ${finalizedBlockHash == "dryRun" ? "" : didSend} ${finalizedBlockHash}`, updated: Date.now(), stale: false, }; - nominator.updateNominatorStatus(nominatorStatus); + await nominator.updateNominatorStatus(nominatorStatus); nominator.lastEraNomination = era; // Create a Nomination Object diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/LocationStatsJob.ts b/packages/common/src/scorekeeper/jobs/specificJobs/LocationStatsJob.ts index 39cfb0761..1b10450a3 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/LocationStatsJob.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/LocationStatsJob.ts @@ -16,6 +16,7 @@ export const locationStatsJob = async (metadata: JobRunnerMetadata) => { try { const { chaindata } = metadata; await queries.cleanBlankLocations(); + await queries.cleanOldLocations(); jobStatusEmitter.emit("jobProgress", { name: JobNames.LocationStats, diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/MainScorekeeperJob.ts b/packages/common/src/scorekeeper/jobs/specificJobs/MainScorekeeperJob.ts index fbb19c956..67be1a52d 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/MainScorekeeperJob.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/MainScorekeeperJob.ts @@ -4,6 +4,7 @@ import { queries } from "../../../index"; import { startRound } from "../../Round"; import { jobStatusEmitter } from "../../../Events"; import { JobNames } from "../JobConfigs"; +import { NOMINATOR_SHOULD_NOMINATE_ERAS_THRESHOLD } from "../../../constants"; export class MainScorekeeperJob extends Job { constructor(jobConfig: JobConfig, jobRunnerMetadata: JobRunnerMetadata) { @@ -41,7 +42,8 @@ export const mainScorekeeperJob = async ( return; } - const { lastNominatedEraIndex } = await queries.getLastNominatedEraIndex(); + const lastNominatedEra = await queries.getLastNominatedEraIndex(); + const lastNominatedEraIndex = lastNominatedEra?.lastNominatedEraIndex || 0; const eraBuffer = config.global.networkPrefix == 0 ? 1 : 4; const isNominationRound = Number(lastNominatedEraIndex) <= activeEra - eraBuffer; @@ -55,8 +57,10 @@ export const mainScorekeeperJob = async ( const stash = await nom.stash(); if (!stash || stash === "0x") return false; const lastNominatedEra = - await chaindata.getNominatorLastNominationEra(stash); - return lastNominatedEra <= activeEra - 1; + (await chaindata.getNominatorLastNominationEra(stash)) || 0; + return ( + activeEra - lastNominatedEra >= NOMINATOR_SHOULD_NOMINATE_ERAS_THRESHOLD + ); }), ); diff --git a/packages/common/src/scorekeeper/jobs/specificJobs/StaleNomination.ts b/packages/common/src/scorekeeper/jobs/specificJobs/StaleNomination.ts index 444ecfb57..85f2651a5 100644 --- a/packages/common/src/scorekeeper/jobs/specificJobs/StaleNomination.ts +++ b/packages/common/src/scorekeeper/jobs/specificJobs/StaleNomination.ts @@ -38,7 +38,7 @@ export const staleNominationJob = async ( if (!stash || stash === "0x") continue; const lastNominatedEra = - await chaindata.getNominatorLastNominationEra(stash); + (await chaindata.getNominatorLastNominationEra(stash)) || 0; if (lastNominatedEra < Number(currentEra) - threshold) { const message = `Nominator ${stash} has a stale nomination. Last nomination was in era ${nom.getStatus()?.lastNominationEra} (it is now era ${currentEra})`; diff --git a/packages/common/src/scorekeeper/scorekeeper.ts b/packages/common/src/scorekeeper/scorekeeper.ts index c9e23c1a0..05ca7a017 100644 --- a/packages/common/src/scorekeeper/scorekeeper.ts +++ b/packages/common/src/scorekeeper/scorekeeper.ts @@ -9,7 +9,7 @@ import { Util, } from "../index"; -import Nominator, { NominatorStatus } from "../nominator/nominator"; +import Nominator from "../nominator/nominator"; import { registerAPIHandler, registerEventEmitterHandler, @@ -17,6 +17,7 @@ import { import { Job, JobRunnerMetadata, JobStatus } from "./jobs/JobsClass"; import { JobsRunnerFactory } from "./jobs/JobsRunnerFactory"; import { startRound } from "./Round"; +import { NominatorStatus } from "../types"; // import { monitorJob } from "./jobs"; export type NominatorGroup = Config.NominatorConfig[]; @@ -240,7 +241,7 @@ export default class ScoreKeeper { scorekeeperLabel, ); - const currentEra = await this.chaindata.getCurrentEra(); + const currentEra = (await this.chaindata.getCurrentEra()) || 0; this.currentEra = currentEra; // await setAllIdentities(this.chaindata, scorekeeperLabel); diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 7936559dd..6095a0aee 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -111,3 +111,42 @@ export interface TelemetryWsPayload extends Array { 6: any; // location 7: any; // startupTime } + +export enum NominatorState { + Nominated = "Nominated", + ReadyToNominate = "Ready to Nominate", + Nominating = "Nominating", + AwaitingProxyExecution = "Awaiting Proxy Execution", + NotNominating = "Not Nominating", + Stale = "Stale", +} + +export interface NominatorStatus { + state?: NominatorState; + status?: string; + isBonded?: boolean; + bondedAddress?: string; + bondedAmount?: number; + stashAddress?: string; + proxyAddress?: string; + isProxy?: boolean; + proxyDelay?: number; + isNominating?: boolean; + lastNominationEra?: number; + lastNominationTime?: number; + currentTargets?: + | string[] + | { + stash?: string; + name?: string; + kyc?: boolean; + score?: string | number; + }[]; + nextTargets?: string[]; + proxyTxs?: any[]; + updated: number; + rewardDestination?: string; + stale?: boolean; + dryRun?: boolean; + shouldNominate?: boolean; +} diff --git a/packages/common/src/utils/Validators.ts b/packages/common/src/utils/Validators.ts index 7811d16a6..b296e6b59 100644 --- a/packages/common/src/utils/Validators.ts +++ b/packages/common/src/utils/Validators.ts @@ -1,51 +1,13 @@ -import { - allCandidates, - getAllValidatorSets, - getIdentityAddresses, - setRank, -} from "../db"; +import { allCandidates, setRank } from "../db/queries"; +import { queries } from "../index"; +// Sets all validators ranks export const setValidatorRanks = async () => { - const rankMap: Map = new Map(); const candidates = await allCandidates(); - const candidateAddresses = candidates.map((candidate) => candidate.stash); - const validatorSets = await getAllValidatorSets(); - if (validatorSets) { - for (const era of validatorSets) { - const validators = era.validators || []; - - for (const validator of validators) { - const candidateExists = candidateAddresses.includes(validator); - if (candidateExists) { - if (rankMap.has(validator)) { - const val = rankMap.get(validator) || 0; - rankMap.set(validator, val + 1); - } else { - rankMap.set(validator, 1); - } - } - } - } - await processRankMap(rankMap); + for (const candidate of candidates) { + const identityRank = await queries.getIdentityValidatorActiveEras( + candidate.stash, + ); + await setRank(candidate.stash, identityRank); } }; - -export const processRankMap = async ( - rankMap: Map, -): Promise => { - return await Promise.all( - Array.from(rankMap.entries()).map(async ([validator, rank]) => { - const rankList: { address: string; rank: number }[] = []; - const identityAddresses: string[] = await getIdentityAddresses(validator); - for (const identityAddress of identityAddresses) { - rankList.push({ address: identityAddress, rank: rank }); - } - const sortedRankList = rankList.sort((a, b) => b.rank - a.rank); - const maxRank: number = Math.max( - ...sortedRankList.map((entry) => entry.rank), - ); - - await setRank(validator, maxRank); - }), - ); -}; diff --git a/packages/common/test/db/queries/EraStats.unit.test.ts b/packages/common/test/db/queries/EraStats.unit.test.ts index 72b8d62cd..c0ba80d47 100644 --- a/packages/common/test/db/queries/EraStats.unit.test.ts +++ b/packages/common/test/db/queries/EraStats.unit.test.ts @@ -10,13 +10,15 @@ describe("setEraStats", () => { const totalNodes = 10; const valid = 8; const active = 6; - await setEraStats(era, totalNodes, valid, active); + const kyc = 3; + await setEraStats(era, totalNodes, valid, active, kyc); const eraStats = await EraStatsModel.findOne({ era }).lean(); expect(eraStats).toBeDefined(); expect(eraStats?.totalNodes).toBe(totalNodes); expect(eraStats?.valid).toBe(valid); expect(eraStats?.active).toBe(active); + expect(eraStats?.kyc).toBe(kyc); }); it("should update existing era stats with different values", async () => { @@ -24,6 +26,7 @@ describe("setEraStats", () => { const initialTotalNodes = 5; const initialValid = 4; const initialActive = 3; + const kyc = 2; await new EraStatsModel({ era, totalNodes: initialTotalNodes, @@ -34,13 +37,14 @@ describe("setEraStats", () => { const updatedTotalNodes = 12; const updatedValid = 10; const updatedActive = 8; - await setEraStats(era, updatedTotalNodes, updatedValid, updatedActive); + await setEraStats(era, updatedTotalNodes, updatedValid, updatedActive, kyc); const eraStats = await EraStatsModel.findOne({ era }).lean(); expect(eraStats).toBeDefined(); expect(eraStats?.totalNodes).toBe(updatedTotalNodes); expect(eraStats?.valid).toBe(updatedValid); expect(eraStats?.active).toBe(updatedActive); + expect(eraStats?.kyc).toBe(kyc); }); it("should not update existing era stats if values are the same", async () => { @@ -48,10 +52,11 @@ describe("setEraStats", () => { const totalNodes = 20; const valid = 15; const active = 10; - await new EraStatsModel({ era, totalNodes, valid, active }).save(); + const kyc = 5; + await new EraStatsModel({ era, totalNodes, valid, active, kyc }).save(); // Call setEraStats with the same values - await setEraStats(era, totalNodes, valid, active); + await setEraStats(era, totalNodes, valid, active, kyc); const eraStats = await EraStatsModel.findOne({ era }).lean(); expect(eraStats).toBeDefined(); @@ -59,6 +64,7 @@ describe("setEraStats", () => { expect(eraStats?.totalNodes).toBe(totalNodes); expect(eraStats?.valid).toBe(valid); expect(eraStats?.active).toBe(active); + expect(eraStats?.kyc).toBe(kyc); }); }); @@ -70,6 +76,7 @@ describe("getLatestEraStats", () => { totalNodes: era * 2, valid: era * 1.5, active: era, + kyc: 1, })); await EraStatsModel.create(eraStatsData); diff --git a/packages/common/test/scorekeeper/NumNominations.int.test.ts b/packages/common/test/scorekeeper/NumNominations.int.test.ts index c43c7ff56..1a58ba2e7 100644 --- a/packages/common/test/scorekeeper/NumNominations.int.test.ts +++ b/packages/common/test/scorekeeper/NumNominations.int.test.ts @@ -2,6 +2,8 @@ import { ApiPromise, WsProvider } from "@polkadot/api"; import { autoNumNominations } from "../../src/scorekeeper/NumNominations"; import { KusamaEndpoints } from "../../src/constants"; +import Nominator from "../../src/nominator/nominator"; +import ApiHandler from "../../src/ApiHandler/ApiHandler"; describe("autoNumNominations Integration Test", () => { it("queries the real API and retrieves data", async () => { @@ -10,11 +12,18 @@ describe("autoNumNominations Integration Test", () => { }); await api.isReadyOrError; - const nom = { - stash: () => "EX9uchmfeSqKTM7cMMg8DkH49XV8i4R7a7rqCn8btpZBHDP", + const handler = new ApiHandler(KusamaEndpoints); + + const nominatorConfig = { + isProxy: false, + seed: "0x" + "00".repeat(32), + proxyDelay: 10800, + proxyFor: "EX9uchmfeSqKTM7cMMg8DkH49XV8i4R7a7rqCn8btpZBHDP", }; - const result = await autoNumNominations(api, nom as any); + const nominator = new Nominator(handler, nominatorConfig, 2, null); + + const result = await autoNumNominations(api, nominator); expect(result).toBeDefined(); diff --git a/packages/common/test/utils/Validators.unit.test.ts b/packages/common/test/utils/Validators.unit.test.ts new file mode 100644 index 000000000..9cb61e41b --- /dev/null +++ b/packages/common/test/utils/Validators.unit.test.ts @@ -0,0 +1,101 @@ +// import { setValidatorRanks } from "../../src/utils"; +import { addKusamaCandidates } from "../testUtils/candidate"; +import { Identity } from "../../src/types"; +import { + addCandidate, + getCandidate, + getIdentityValidatorActiveEras, + getValidatorActiveEras, + setCandidateIdentity, + setValidatorSet, +} from "../../src/db/queries"; +import { initTestServerBeforeAll } from "../testUtils/dbUtils"; +import { ValidatorSetModel } from "../../src/db"; +import { setValidatorRanks } from "../../src/utils/Validators"; + +initTestServerBeforeAll(); +describe("setValidatorRanks", () => { + it("should set ranks for all candidates", async () => { + await addKusamaCandidates(); + + await addCandidate( + 2398, + "Blockshard2", + "HkJjBkX8fPBFJvTtAbUDKWZSsMrNFuMc7TrT8BqVS5YhZXg", + "", + false, + "matrixhandle", + false, + ); + + const identity1: Identity = { + address: "Cp4U5UYg2FaVUpyEtQgfBm9aqge6EEPkJxEFVZFYy7L1AZF", + name: "Blockshard", + display: "Blockshard", + subIdentities: [ + { + name: "Blockshard2", + address: "HkJjBkX8fPBFJvTtAbUDKWZSsMrNFuMc7TrT8BqVS5YhZXg", + }, + ], + }; + const identity2: Identity = { + address: "D9rwRxuG8xm8TZf5tgkbPxhhTJK5frCJU9wvp59VRjcMkUf", + name: "🎠 Forbole GP01 🇭🇰", + display: "🎠 Forbole GP01 🇭🇰", + }; + const identity3: Identity = { + address: "J4hAvZoHCviZSoPHoSwLida8cEkZR1NXJcGrcfx9saHTk7D", + name: "Anonstake", + display: "Anonstake", + }; + const identity4: Identity = { + address: "EPhtbjecJ9P2SQEGEJ4XmFS4xN7JioBFarSrbqjhj8BuJ2v", + name: "Indigo One", + display: "Indigo One", + }; + const identity5: Identity = { + address: "HhcrzHdB5iBx823XNfBUukjj4TUGzS9oXS8brwLm4ovMuVp", + name: "KIRA Staking", + display: "KIRA Staking", + }; + await setCandidateIdentity(identity1?.address, identity1); + + await setValidatorSet(1, 1, [identity1?.address, identity2?.address]); + await setValidatorSet(5, 2, [identity1?.address, identity2?.address]); + await setValidatorSet(8, 3, [ + identity1?.address, + identity3?.address, + identity5?.address, + "HkJjBkX8fPBFJvTtAbUDKWZSsMrNFuMc7TrT8BqVS5YhZXg", + ]); + await setValidatorSet(16, 4, [ + identity1?.address, + identity3?.address, + identity4?.address, + "HkJjBkX8fPBFJvTtAbUDKWZSsMrNFuMc7TrT8BqVS5YhZXg", + identity5?.address, + ]); + await setValidatorSet(100, 5, [identity1?.address, identity4?.address]); + + const validatorSets = await ValidatorSetModel.find({}).exec(); + expect(validatorSets.length).toBe(5); + + const numEras = await getValidatorActiveEras(identity1?.address); + expect(numEras).toBe(5); + + const subNumEras = await getIdentityValidatorActiveEras( + "HkJjBkX8fPBFJvTtAbUDKWZSsMrNFuMc7TrT8BqVS5YhZXg", + ); + expect(subNumEras).toBe(5); + + await setValidatorRanks(); + const candidate = await getCandidate(identity1?.address); + expect(candidate?.rank).toBe(5); + + const secondNode = await getCandidate( + "HkJjBkX8fPBFJvTtAbUDKWZSsMrNFuMc7TrT8BqVS5YhZXg", + ); + expect(secondNode?.rank).toBe(5); + }, 10000); +}); diff --git a/packages/core/package.json b/packages/core/package.json index a20cfd76d..ed17917ff 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/core", - "version": "3.1.2", + "version": "3.1.3", "description": "Services for running the Thousand Validator Program.", "main": "index.js", "scripts": { @@ -40,7 +40,7 @@ "@1kv/worker": "workspace:^", "@koa/router": "^12.0.1", "@octokit/rest": "^20.0.2", - "@polkadot/api": "^10.11.2", + "@polkadot/api": "^10.12.1", "@polkadot/keyring": "^12.6.2", "@types/cron": "^2.4.0", "@types/jest": "^29.5.12", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 94d0ba36e..0fe42ffc1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -120,6 +120,8 @@ export const clean = async (scorekeeper) => { try { // Clean locations with None await queries.cleanBlankLocations(); + await queries.cleanOldLocations(); + await queries.cleanOldNominatorStakes(); // Delete all on-chain identities so they get fetched new on startup. await queries.deleteAllIdentities(); diff --git a/packages/gateway/package.json b/packages/gateway/package.json index 922afb509..ebb5e15e2 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -1,6 +1,6 @@ { "name": "@1kv/gateway", - "version": "3.1.2", + "version": "3.1.3", "description": "Services for running the Thousand Validator Program.", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/packages/gateway/src/controllers/Validators.ts b/packages/gateway/src/controllers/Validators.ts index caeae6a3a..7323a09c9 100644 --- a/packages/gateway/src/controllers/Validators.ts +++ b/packages/gateway/src/controllers/Validators.ts @@ -55,4 +55,17 @@ export default class ValidatorController { } response(context, 200, await ValidatorService.getBeefyDummy()); } + + public static async getValidatorsNumActiveEras(context: any): Promise { + requestEmitter.emit("requestReceived"); + if (await context.cashed()) { + logger.info(`getValidatorsNumActiveEras is cached`, gatewayLabel); + return; + } + response( + context, + 200, + await ValidatorService.getValidatorsNumActiveEras(context.params.address), + ); + } } diff --git a/packages/gateway/src/routes/index.ts b/packages/gateway/src/routes/index.ts index 01189c3ae..9f1a1492a 100644 --- a/packages/gateway/src/routes/index.ts +++ b/packages/gateway/src/routes/index.ts @@ -52,6 +52,7 @@ const API = { CurrentValidatorSet: "/validators/current", Validators: "/validators", Validator: "/validator/:address", + ValidatorsNumActiveEras: "/validators/activeeras/:address", ValidatorsBeefyStats: "/validators/beefy", ValidatorsBeefyDummy: "/validators/beefy/dummy", RewardsValidator: "/rewards/validator/:address", @@ -97,6 +98,7 @@ router.get(API.ScoreMetadata, Score.getLatestScoreMetadata); router.get(API.SessionScoreMetadata, Score.getSessionScoreMetadata); router.get(API.CurrentValidatorSet, Validator.getLatestValidatorSet); +router.get(API.ValidatorsNumActiveEras, Validator.getValidatorsNumActiveEras); router.get( API.LocationsCurrentValidatorSet, @@ -129,20 +131,4 @@ router.get(API.BlockIndex, Block.getBlockIndex); router.get(API.StatsTotalReqeusts, Stats.getTotalRequests); router.get(API.StatsEndpointCounts, Stats.getEndpointCounts); -// router.get("/stats/totalRequests", (ctx) => { -// ctx.body = { totalRequests: requestEmitter.listenerCount("requestReceived") }; -// }); -// -// // Endpoint to retrieve the count of requests per endpoint -// router.get("/stats/endpointCounts", (ctx) => { -// const endpointCounts = {}; -// -// // Iterate over all registered endpoints -// requestEmitter.eventNames().forEach((endpoint) => { -// endpointCounts[endpoint] = requestEmitter.listenerCount(endpoint); -// }); -// -// ctx.body = { endpointCounts }; -// }); - export default router; diff --git a/packages/gateway/src/services/Candidate.ts b/packages/gateway/src/services/Candidate.ts index 8ca169701..1da5ba7fc 100644 --- a/packages/gateway/src/services/Candidate.ts +++ b/packages/gateway/src/services/Candidate.ts @@ -3,11 +3,15 @@ import { logger, queries } from "@1kv/common"; const label = { label: "Gateway" }; export const getCandidateData = async (candidate: any): Promise => { - const metadata = await queries.getChainMetadata(); + const [metadata, score, nominations, location] = await Promise.all([ + queries.getChainMetadata(), + queries.getLatestValidatorScore(candidate.stash), + queries.getLatestNominatorStake(candidate.stash), + queries.getCandidateLocation(candidate.name), + ]); + const denom = Math.pow(10, metadata.decimals); - const score = await queries.getLatestValidatorScore(candidate.stash); - const nominations = await queries.getLatestNominatorStake(candidate.stash); - const location = await queries.getCandidateLocation(candidate.name); + return { slotId: candidate.slotId, kyc: candidate.kyc, diff --git a/packages/gateway/src/services/Validator.ts b/packages/gateway/src/services/Validator.ts index 8e0f08692..ebf8bd029 100644 --- a/packages/gateway/src/services/Validator.ts +++ b/packages/gateway/src/services/Validator.ts @@ -24,3 +24,17 @@ export const getBeefyDummy = async (): Promise => { const validators = await queries.getValidatorsBeefyDummy(); return validators; }; + +export const getValidatorsNumActiveEras = async ( + stash: string, +): Promise => { + const eras = await queries.getValidatorActiveEras(stash); + return eras; +}; + +export const getIdentityValidatorNumActiveEras = async ( + stash: string, +): Promise => { + const eras = await queries.getIdentityValidatorActiveEras(stash); + return eras; +}; diff --git a/packages/gateway/src/swagger.yml b/packages/gateway/src/swagger.yml index a7d5d882b..e9993719a 100644 --- a/packages/gateway/src/swagger.yml +++ b/packages/gateway/src/swagger.yml @@ -32,6 +32,8 @@ tags: description: Querying score data paths: + + /candidate/{candidateStash}: get: tags: @@ -58,6 +60,33 @@ paths: description: Successful response with a list of candidates + /candidates/rank: + get: + tags: + - Candidates + summary: Retrieve a list of candidates ordered by rank + responses: + 200: + description: Successful response with a list of candidates + + /candidates/valid: + get: + tags: + - Candidates + summary: Retrieve a list of candidates that are valid + responses: + 200: + description: Successful response with a list of candidates + + /candidates/invalid: + get: + tags: + - Candidates + summary: Retrieve a list of candidates that are invalid + responses: + 200: + description: Successful response with a list of candidates + /rewards/validator/{stash}: get: tags: @@ -148,12 +177,7 @@ paths: responses: 200: description: Rewards. - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Reward' + /rewards/nominator/{address}/total: get: @@ -171,13 +195,19 @@ paths: responses: '200': description: "Total rewards for the specified nominator." - content: - application/json: - schema: - $ref: '#/components/schemas/RewardTotal' '404': description: "Validator not found." + + /healthcheck: + get: + summary: "health check of the backend" + description: "Health check of the backend" + responses: + '200': + description: "health check of the backend" + + /nominators: get: summary: "Get All Nominators" @@ -207,6 +237,31 @@ paths: '404': description: "Nominator not found." + /nominator/{address}/{last}: + get: + summary: "Get Nominator by Address" + tags: + - Nominator + description: "Retrieve details of a specific nominator by address." + parameters: + - in: path + name: address + required: true + schema: + type: string + description: "The unique address of the nominator." + - in: path + name: last + required: true + schema: + type: string + description: "Number of eras to retrieve." + responses: + '200': + description: "Successful response with nominator details." + '404': + description: "Nominator not found." + /nominations: get: summary: "Get All Nominations" @@ -385,12 +440,12 @@ paths: '404': description: "Data not found." - /delegations/{address}: + + + /erapoints/{address}: get: - summary: "Delegation Data" - tags: - - Candidates - description: "Retrieve details of a specific validator's delegations'" + summary: "Era Points" + description: "Retrieve details of a specific validator's era points'" parameters: - in: path name: address @@ -404,100 +459,178 @@ paths: '404': description: "Data not found." - /opengov/votes/address/{address}: + /totalerapoints: get: - summary: "Open Gov Vote Data" - tags: - - Candidates - description: "Retrieve details of a specific validator's votes" + summary: "Total Era Points" + description: "Retrieves total era points" + responses: + '200': + description: "Successful response." + + /erastats: + get: + summary: "Era Stats" + description: "Retrieves era stats" + responses: + '200': + description: "Successful response." + + /scoremetadata: + get: + summary: "Score Metadata" + description: "Retrieves score metadata" + responses: + '200': + description: "Successful response." + + /scoremetadata/{session}: + get: + summary: "Score Metadata" + description: "Retrieve details of a score metadata for a session'" parameters: - in: path - name: address + name: session required: true schema: type: string - description: "The unique address of the validator." + description: "Session" + responses: + '200': + description: "Successful response." + '404': + description: "Data not found." + + /release: + get: + summary: "Latest Release" + description: "Retrieves latest tagged release" + responses: + '200': + description: "Successful response." + + /location/currentvalidatorset: + get: + summary: "location stats of the current validator set" + description: "location stats of the current validator set" + responses: + '200': + description: "Successful response." + + /locationstats: + get: + summary: "location stats" + description: "location stats" + responses: + '200': + description: "Successful response." + + /locationstats/valid: + get: + summary: "location stats of valid nodes" + description: "location stats of valid nodes" + responses: + '200': + description: "Successful response." + + /locationstats/{session}: + get: + summary: "location stats for a session" + description: "Retrieve details of location stats for a session'" + parameters: + - in: path + name: session + required: true + schema: + type: string + description: "Session" responses: '200': description: "Successful response." '404': description: "Data not found." -# -# -#components: -# schemas: -# Reward: -# type: object -# properties: -# role: -# type: string -# exposurePercentage: -# type: integer -# totalStake: -# type: integer -# commission: -# type: integer -# era: -# type: integer -# validator: -# type: string -# nominator: -# type: string -# rewardAmount: -# type: string -# rewardDestination: -# type: string -# erasMinStake: -# type: number -# format: float -# validatorStakeEfficiency: -# type: number -# format: float -# blockHash: -# type: string -# blockNumber: -# type: integer -# timestamp: -# type: integer -# date: -# type: string -# format: date -# chf: -# type: number -# format: float -# usd: -# type: number -# format: float -# eur: -# type: number -# format: float -# RewardTotal: -# type: object -# properties: -# validator: -# type: string -# example: "ESNMjpEWcenAbCqGEsHVdbbRai79VQYMmV1fNW1kRZogmzx" -# total: -# type: number -# format: float -# example: 0.879469625343 -# rewardCount: -# type: integer -# example: 1 -# RewardStats: -# type: object -# properties: -# total: -# type: number -# format: float -# example: 0.235934990397 -# rewardCount: -# type: integer -# example: 1 -# avgEfficiency: -# type: number -# format: float -# example: 84.46114961271203 -# avgStake: -# type: integer -# example: 7985 \ No newline at end of file + + /validators/current: + get: + summary: "Current Validator Set" + description: "Current Validator Set" + responses: + '200': + description: "Successful response." + + /validators: + get: + summary: "Validator Set Keys" + description: "Validator Set Keys" + responses: + '200': + description: "Successful response." + + /validators/beefy: + get: + summary: "Validator Set Beefy Keys" + description: "Validator Set Beefy Keys" + responses: + '200': + description: "Successful response." + + /validators/beefy/dummmy: + get: + summary: "Validator Set Beefy Dummy Keys" + description: "Validator Set Beefy Dummy Keys" + responses: + '200': + description: "Successful response." + + /validators/{address}: + get: + summary: "Validator Set Keys" + description: "Validator Set Keys" + parameters: + - in: path + name: address + required: true + schema: + type: string + description: "address" + responses: + '200': + description: "Successful response." + + /validators/activeeras/{address}: + get: + summary: "Number of active eras for an address" + description: "Number of active eras for an address" + parameters: + - in: path + name: address + required: true + schema: + type: string + description: "address" + responses: + '200': + description: "Successful response." + + /blockindex: + get: + summary: "The block index of the backend" + description: "block index of the backend" + responses: + '200': + description: "Successful response." + + /stats/totalReqeusts: + get: + summary: "The total number of api requests of the backend" + description: "The total number of api requests of the backend" + responses: + '200': + description: "Successful response." + + /stats/endpointCount: + get: + summary: "The total number of api requests of the backend for an endpoint" + description: "The total number of api requests of the backend for an endpoint" + responses: + '200': + description: "Successful response." \ No newline at end of file diff --git a/packages/scorekeeper-status-ui/package.json b/packages/scorekeeper-status-ui/package.json index ba6471168..4d364377d 100644 --- a/packages/scorekeeper-status-ui/package.json +++ b/packages/scorekeeper-status-ui/package.json @@ -1,7 +1,7 @@ { "name": "@1kv/scorekeeper-status-ui", "private": true, - "version": "3.1.2", + "version": "3.1.3", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/scorekeeper-status-ui/src/App.css b/packages/scorekeeper-status-ui/src/App.css index ee356628a..5f87bce34 100644 --- a/packages/scorekeeper-status-ui/src/App.css +++ b/packages/scorekeeper-status-ui/src/App.css @@ -479,6 +479,7 @@ h1 { } .proxyTransactionItem { + color: #0f0; display: flex; flex-direction: column; align-items: center; @@ -536,3 +537,37 @@ h1 { white-space: nowrap; /* Keep the text on a single line */ max-width: 70%; /* Adjust this value based on your container's width */ } + + +.nominatorStateContainer { + width: 80%; + box-shadow: 0 0 10px rgba(0, 255, 0, 0.5); + background: rgba(0, 255, 0, 0.05); + border: 1px solid rgba(0, 255, 0, 0.5); + margin: 0 auto; + display: flex; + flex-direction: column; /* Align children vertically */ + align-items: center; /* Center children horizontally */ + gap: 10px; + padding: 10px; + border-radius: 5px; + margin-top: 7%; + margin-bottom: 4%; +} + +.nominatorStateContainer hr { + width: 100%; /* Match the width of the container */ + border-color: rgba(0, 255, 0, 0.5); /* Match the border color */ + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5); /* Softer glow for the line */ +} + +.nominatorStateContainer > div, .nominatorStateContainer > p { + width: 100%; /* Ensure full width for center alignment */ + text-align: center; /* Center the text */ +} + +.parentContainer { + display: flex; + justify-content: center; + align-items: center; +} diff --git a/packages/scorekeeper-status-ui/src/App.tsx b/packages/scorekeeper-status-ui/src/App.tsx index 543105b29..8f2e1c56a 100644 --- a/packages/scorekeeper-status-ui/src/App.tsx +++ b/packages/scorekeeper-status-ui/src/App.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; +import axios from "axios"; import { FiActivity, FiAlertTriangle, @@ -6,7 +7,6 @@ import { FiCheckCircle, FiClock, FiDollarSign, - FiInfo, FiPlay, FiRefreshCcw, FiSend, @@ -16,15 +16,13 @@ import { FiUserCheck, FiXCircle, } from "react-icons/fi"; - import { BeatLoader } from "react-spinners"; import { motion } from "framer-motion"; import "./App.css"; -import axios from "axios"; // Ensure the path to your CSS file is correct -import { debounce } from "lodash"; import HealthCheckBar from "./HealthCheckBar"; import { Identicon } from "@polkadot/react-identicon"; import EraStatsBar from "./EraStatsBar"; +import { debounce } from "lodash"; interface Job { name: string; @@ -265,6 +263,66 @@ const App = () => { } } + const getStateColor = (state) => { + switch (state) { + case "Nominated": + return "green"; + case "Ready to Nominate": + return "blue"; + case "Nominating": + return "orange"; + case "Awaiting Proxy Execution": + return "purple"; + case "Not Nominating": + return "red"; + case "Stale": + return "grey"; + default: + return "black"; + } + }; + + const renderNominatorStateIcon = (state: string) => { + let iconComponent; + const iconSize = 24; + + switch (state) { + case "Nominated": + iconComponent = ; + break; + case "Nominating": + iconComponent = ; + break; + case "Stale": + iconComponent = ; + break; + case "Not Nominating": + iconComponent = ; + break; + case "Ready to Nominate": + case "Awaiting Proxy Execution": + iconComponent = ; + break; + default: + iconComponent = <>; + break; + } + + return ( +
+ {iconComponent} + {state} +
+ ); + }; + return (