Skip to content

Commit

Permalink
feat: store eraRewardPoints from recent eras (#1529)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ross Bulat authored Oct 18, 2023
1 parent 4bc62a3 commit 690ba51
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 69 deletions.
1 change: 1 addition & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const ListItemsPerPage = 50;
export const ListItemsPerBatch = 30;
export const MinBondPrecision = 3;
export const MaxPayoutDays = 60;
export const MaxEraRewardPointsEras = 7;

/*
* Third party API keys and endpoints
Expand Down
45 changes: 44 additions & 1 deletion src/contexts/Validators/Utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import type BigNumber from 'bignumber.js';
import BigNumber from 'bignumber.js';
import type { LocalMeta } from 'contexts/FastUnstake/types';
import type {
EraRewardPoints,
LocalValidatorEntriesData,
Validator,
} from 'contexts/Validators/types';
Expand Down Expand Up @@ -94,3 +95,45 @@ export const validateLocalExposure = (
checked: localChecked,
};
};

// Check if era reward points entry exists for an era.
export const hasLocalEraRewardPoints = (network: NetworkName, era: string) => {
const current = JSON.parse(
localStorage.getItem(`${network}_era_reward_points`) || '{}'
);
return !!current?.[era];
};

// Get local era reward points entry for an era.
export const getLocalEraRewardPoints = (network: NetworkName, era: string) => {
const current = JSON.parse(
localStorage.getItem(`${network}_era_reward_points`) || '{}'
);
return current?.[era] || {};
};

// Set local era reward points entry for an era.
export const setLocalEraRewardPoints = (
network: NetworkName,
era: string,
eraRewardPoints: EraRewardPoints | null,
endEra: string
) => {
const current = JSON.parse(
localStorage.getItem(`${network}_era_reward_points`) || '{}'
);

const removeStaleEras = Object.fromEntries(
Object.entries(current || {}).filter(([k]: [string, unknown]) =>
new BigNumber(k).isGreaterThanOrEqualTo(endEra)
)
);

localStorage.setItem(
`${network}_era_reward_points`,
JSON.stringify({
...removeStaleEras,
[era]: eraRewardPoints,
})
);
};
1 change: 1 addition & 0 deletions src/contexts/Validators/ValidatorEntries/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export const defaultValidatorsContext: ValidatorsContextInterface = {
nominated: null,
poolNominated: null,
validatorCommunity: [],
erasRewardPoints: {},
validatorsFetched: 'unsynced',
};
227 changes: 159 additions & 68 deletions src/contexts/Validators/ValidatorEntries/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { greaterThanZero, shuffle } from '@polkadot-cloud/utils';
import { greaterThanZero, rmCommas, shuffle } from '@polkadot-cloud/utils';
import BigNumber from 'bignumber.js';
import React, { useEffect, useRef, useState } from 'react';
import { ValidatorCommunity } from '@polkadot-cloud/assets/validators';
import type { AnyApi, Fn, Sync } from 'types';
import type { AnyApi, AnyJson, Fn, Sync } from 'types';
import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks';
import { useBonded } from 'contexts/Bonded';
import { useNetworkMetrics } from 'contexts/NetworkMetrics';
import { useActivePools } from 'contexts/Pools/ActivePools';
import { useNetwork } from 'contexts/Network';
import { useApi } from 'contexts/Api';
import { useActiveAccounts } from 'contexts/ActiveAccounts';
import { MaxEraRewardPointsEras } from 'consts';
import type {
EraRewardPoints,
ErasRewardPoints,
Identity,
Validator,
ValidatorAddresses,
ValidatorSuper,
ValidatorsContextInterface,
} from '../types';
import { defaultValidatorsData, defaultValidatorsContext } from './defaults';
import { getLocalEraValidators, setLocalEraValidators } from '../Utils';
import {
getLocalEraRewardPoints,
getLocalEraValidators,
hasLocalEraRewardPoints,
setLocalEraRewardPoints,
setLocalEraValidators,
} from '../Utils';

export const ValidatorsProvider = ({
children,
Expand Down Expand Up @@ -54,7 +63,6 @@ export const ValidatorsProvider = ({

// Stores the currently active validator set.
const [sessionValidators, setSessionValidators] = useState<string[]>([]);
const sessionUnsub = useRef<Fn>();

// Stores the currently active parachain validator set.
const [sessionParaValidators, setSessionParaValidators] = useState<string[]>(
Expand All @@ -72,75 +80,114 @@ export const ValidatorsProvider = ({
const [poolNominated, setPoolNominated] = useState<Validator[] | null>(null);

// Stores a randomised validator community dataset.
const [validatorCommunity] = useState<any>([...shuffle(ValidatorCommunity)]);
const [validatorCommunity] = useState([...shuffle(ValidatorCommunity)]);

// Reset validators list on network change.
useEffectIgnoreInitial(() => {
setValidatorsFetched('unsynced');
setSessionValidators([]);
setSessionParaValidators([]);
setAvgCommission(0);
setValidators([]);
setValidatorIdentities({});
setValidatorSupers({});
}, [network]);
// Store era reward points, keyed by era.
const [erasRewardPoints, setErasRewardPoints] = useState<ErasRewardPoints>(
{}
);

// fetch validators and session validators when activeEra ready
useEffectIgnoreInitial(() => {
if (isReady && activeEra.index.isGreaterThan(0)) {
fetchValidators();
subscribeSessionValidators();
}
}, [isReady, activeEra]);
// Processes reward points for a given era.
const processEraRewardPoints = (
result: AnyJson,
era: BigNumber,
endEra: BigNumber
) => {
if (!api || erasRewardPoints[era.toString()]) return false;

const formatted = {
total: rmCommas(result.total),
individual: Object.fromEntries(
Object.entries(result.individual).map(([key, value]) => [
key,
rmCommas(value as string),
])
),
};

// fetch parachain session validators when `earliestStoredSession` ready
useEffectIgnoreInitial(() => {
if (isReady && greaterThanZero(earliestStoredSession)) {
subscribeParachainValidators();
}
}, [isReady, earliestStoredSession]);
// Store era in local storage.
setLocalEraRewardPoints(
network,
era.toString(),
formatted,
endEra.toString()
);

// fetch active account's nominations in validator list format
useEffectIgnoreInitial(() => {
if (isReady && activeAccount) {
fetchNominatedList();
return formatted;
};

// Fetches era reward points for eligible eras.
const fetchErasRewardPoints = async () => {
if (activeEra.index.isZero() || !api) return;

// start fetching from the current era.
let currentEra = activeEra.index;
const endEra = BigNumber.max(
currentEra.minus(MaxEraRewardPointsEras - 1),
1
);

// Introduce additional safeguard againt looping forever.
const totalEras = new BigNumber(MaxEraRewardPointsEras);
let erasProcessed = new BigNumber(0);

// Iterate eras and process reward points.
const calls = [];
const eras = [];
const localEras: Record<string, EraRewardPoints> = {};
do {
// If already in local storage, get data from there. Otherwise, add to calls.
if (hasLocalEraRewardPoints(network, currentEra.toString())) {
localEras[currentEra.toString()] = getLocalEraRewardPoints(
network,
currentEra.toString()
);
} else {
calls.push(api.query.staking.erasRewardPoints(currentEra.toString()));
eras.push(currentEra);
}

currentEra = currentEra.minus(1);
erasProcessed = erasProcessed.plus(1);
} while (
currentEra.isGreaterThanOrEqualTo(endEra) &&
erasProcessed.isLessThan(totalEras)
);

// Make calls and format reward point results.
const newErasRewardPoints: ErasRewardPoints = {};
let i = 0;
for (const result of await Promise.all(calls)) {
const formatted = processEraRewardPoints(
result.toHuman(),
eras[i],
endEra
);
if (formatted) newErasRewardPoints[eras[i].toString()] = formatted;
i++;
}
}, [isReady, activeAccount, bondedAccounts]);

// Commit results to state.
setErasRewardPoints({
...localEras,
...newErasRewardPoints,
});
};

// Fetches the active account's nominees.
const fetchNominatedList = async () => {
if (!activeAccount) return;

// get raw targets list
const targets = getAccountNominations(activeAccount);

// format to list format
const targetsFormatted = targets.map((item: any) => ({ address: item }));
const targetsFormatted = getAccountNominations(activeAccount).map(
(item) => ({ address: item })
);
// fetch preferences
const nominationsWithPrefs = await fetchValidatorPrefs(targetsFormatted);
setNominated(nominationsWithPrefs || []);
};

// fetch active account's pool nominations in validator list format
useEffectIgnoreInitial(() => {
if (isReady && poolNominations) {
fetchPoolNominatedList();
}
}, [isReady, poolNominations]);

// Unsubscribe on network change and component unmount.
useEffect(() => {
if (sessionValidators.length) {
sessionUnsub.current?.();
}
if (sessionParaValidators.length) {
sessionParaUnsub.current?.();
}
return () => {
sessionUnsub.current?.();
sessionParaUnsub.current?.();
};
}, [network]);

// Fetches the active pool's nominees.
const fetchPoolNominatedList = async () => {
// get raw nominations list
let n = poolNominations.targets;
Expand All @@ -165,11 +212,9 @@ export const ValidatorsProvider = ({
const prefs = p.toHuman();
const commission = new BigNumber(prefs.commission.replace(/%/g, ''));

if (!commission.isEqualTo(100)) {
if (!commission.isEqualTo(100))
totalNonAllCommission = totalNonAllCommission.plus(commission);
} else {
notFullCommissionCount++;
}
else notFullCommissionCount++;

entries.push({
address,
Expand Down Expand Up @@ -238,12 +283,10 @@ export const ValidatorsProvider = ({
};

// Subscribe to active session validators.
const subscribeSessionValidators = async () => {
const fetchSessionValidators = async () => {
if (!api || !isReady) return;
const unsub: AnyApi = await api.query.session.validators((v: AnyApi) => {
setSessionValidators(v.toHuman());
sessionUnsub.current = unsub;
});
const sessionValidatorsRaw: AnyApi = await api.query.session.validators();
setSessionValidators(sessionValidatorsRaw.toHuman());
};

// Subscribe to active parachain validators.
Expand Down Expand Up @@ -331,10 +374,57 @@ export const ValidatorsProvider = ({
},
])
);

return supersWithIdentity;
};

// Reset validator state data on network change.
useEffectIgnoreInitial(() => {
setValidatorsFetched('unsynced');
setSessionValidators([]);
setSessionParaValidators([]);
setAvgCommission(0);
setValidators([]);
setValidatorIdentities({});
setValidatorSupers({});
setErasRewardPoints({});
}, [network]);

// Fetch validators, session validators, and era reward points when `activeEra` ready.
useEffectIgnoreInitial(() => {
if (isReady && activeEra.index.isGreaterThan(0)) {
fetchValidators();
fetchErasRewardPoints();
fetchSessionValidators();
}
}, [isReady, activeEra]);

// Fetch parachain session validators when `earliestStoredSession` ready.
useEffectIgnoreInitial(() => {
if (isReady && greaterThanZero(earliestStoredSession))
subscribeParachainValidators();
}, [isReady, earliestStoredSession]);

// Fetch active account's nominations in validator list format.
useEffectIgnoreInitial(() => {
if (isReady && activeAccount) {
fetchNominatedList();
}
}, [isReady, activeAccount, bondedAccounts]);

// Fetch active account's pool nominations in validator list format.
useEffectIgnoreInitial(() => {
if (isReady && poolNominations) fetchPoolNominatedList();
}, [isReady, poolNominations]);

// Unsubscribe on network change and component unmount.
useEffect(() => {
if (sessionParaValidators.length) sessionParaUnsub.current?.();

return () => {
sessionParaUnsub.current?.();
};
}, [network]);

return (
<ValidatorsContext.Provider
value={{
Expand All @@ -348,6 +438,7 @@ export const ValidatorsProvider = ({
nominated,
poolNominated,
validatorCommunity,
erasRewardPoints,
validatorsFetched,
}}
>
Expand Down
Loading

0 comments on commit 690ba51

Please sign in to comment.