Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ux): Pool rewards worker & pool performance graphs #1547

Merged
merged 14 commits into from
Oct 24, 2023
Merged
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@ledgerhq/hw-transport-webhid": "^6.27.19",
"@polkadot-cloud/assets": "^0.1.24",
"@polkadot-cloud/core": "^1.0.28",
"@polkadot-cloud/core": "^1.0.29",
"@polkadot-cloud/react": "^0.1.97",
"@polkadot-cloud/utils": "^0.0.22",
"@polkadot-cloud/utils": "^0.0.23",
"@polkadot/api": "^10.10.1",
"@polkadot/keyring": "^12.1.1",
"@polkadot/rpc-provider": "^10.9.1",
Expand Down
2 changes: 2 additions & 0 deletions src/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { OtherAccountsProvider } from 'contexts/Connect/OtherAccounts';
import { useActiveAccounts } from 'contexts/ActiveAccounts';
import { DappName } from 'consts';
import { ImportedAccountsProvider } from 'contexts/Connect/ImportedAccounts';
import { PoolPerformanceProvider } from 'contexts/Pools/PoolPerformance';

// Embed providers from hook.
export const Providers = () => {
Expand Down Expand Up @@ -91,6 +92,7 @@ export const Providers = () => {
FavoriteValidatorsProvider,
FastUnstakeProvider,
PayoutsProvider,
PoolPerformanceProvider,
UIProvider,
SetupProvider,
MenuProvider,
Expand Down
4 changes: 2 additions & 2 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export const FallbackEpochDuration = new BigNumber(2400);
/*
* Misc values
*/
export const ListItemsPerPage = 30;
export const ListItemsPerBatch = 30;
export const ListItemsPerPage = 25;
export const ListItemsPerBatch = 25;
export const MinBondPrecision = 3;
export const MaxPayoutDays = 60;
export const MaxEraRewardPointsEras = 14;
Expand Down
69 changes: 29 additions & 40 deletions src/contexts/Pools/BondedPools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,44 +25,20 @@ export const BondedPoolsProvider = ({
}) => {
const { network } = useNetwork();
const { api, isReady } = useApi();
const { getNominationsStatusFromTargets } = useStaking();
const { createAccounts, stats } = usePoolsConfig();
const { getNominationsStatusFromTargets } = useStaking();
const { lastPoolId } = stats;

// stores the meta data batches for pool lists
// Stores the meta data batches for pool lists.
const [poolMetaBatches, setPoolMetaBatch]: AnyMetaBatch = useState({});
const poolMetaBatchesRef = useRef(poolMetaBatches);

// stores the meta batch subscriptions for pool lists
// Stores the meta batch subscriptions for pool lists.
const poolSubs = useRef<Record<string, Fn[]>>({});

// store bonded pools
// Store bonded pools.
const [bondedPools, setBondedPools] = useState<BondedPool[]>([]);

// clear existing state for network refresh
useEffectIgnoreInitial(() => {
setBondedPools([]);
setStateWithRef({}, setPoolMetaBatch, poolMetaBatchesRef);
}, [network]);

// initial setup for fetching bonded pools
useEffectIgnoreInitial(() => {
if (isReady) {
// fetch bonded pools
fetchBondedPools();
}
return () => {
unsubscribe();
};
}, [network, isReady, lastPoolId]);

// after bonded pools have synced, fetch metabatch
useEffectIgnoreInitial(() => {
if (bondedPools.length) {
fetchPoolsMetaBatch('bonded_pools', bondedPools, true);
}
}, [bondedPools]);

const unsubscribe = () => {
Object.values(poolSubs.current).map((batch: Fn[]) =>
Object.entries(batch).map(([, v]) => v())
Expand Down Expand Up @@ -122,12 +98,8 @@ export const BondedPoolsProvider = ({
p: AnyMetaBatch,
refetch = false
) => {
if (!isReady || !api) {
return;
}
if (!p.length) {
return;
}
if (!isReady || !api || !p.length) return;

if (!refetch) {
// if already exists, do not re-fetch
if (poolMetaBatchesRef.current[key] !== undefined) {
Expand Down Expand Up @@ -344,9 +316,8 @@ export const BondedPoolsProvider = ({
};

const updateBondedPools = (updatedPools: BondedPool[]) => {
if (!updatedPools) {
return;
}
if (!updatedPools) return;

setBondedPools(
bondedPools.map(
(original) =>
Expand All @@ -365,9 +336,7 @@ export const BondedPoolsProvider = ({
if (!pool) return;

const exists = bondedPools.find((b) => b.id === pool.id);
if (!exists) {
setBondedPools(bondedPools.concat(pool));
}
if (!exists) setBondedPools(bondedPools.concat(pool));
};

// get all the roles belonging to one pool account
Expand Down Expand Up @@ -461,6 +430,26 @@ export const BondedPoolsProvider = ({
setBondedPools(newBondedPools);
};

// Clear existing state for network refresh.
useEffectIgnoreInitial(() => {
setBondedPools([]);
setStateWithRef({}, setPoolMetaBatch, poolMetaBatchesRef);
}, [network]);

// Initial setup for fetching bonded pools.
useEffectIgnoreInitial(() => {
if (isReady) fetchBondedPools();
return () => {
unsubscribe();
};
}, [network, isReady, lastPoolId]);

// After bonded pools have synced, fetch metabatch.
useEffectIgnoreInitial(() => {
if (bondedPools.length)
fetchPoolsMetaBatch('bonded_pools', bondedPools, true);
}, [bondedPools]);

return (
<BondedPoolsContext.Provider
value={{
Expand Down
10 changes: 10 additions & 0 deletions src/contexts/Pools/PoolPerformance/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only
/* eslint-disable @typescript-eslint/no-unused-vars */

import type { PoolPerformanceContextInterface } from './types';

export const defaultPoolPerformanceContext: PoolPerformanceContextInterface = {
poolRewardPointsFetched: 'unsynced',
poolRewardPoints: {},
};
145 changes: 145 additions & 0 deletions src/contexts/Pools/PoolPerformance/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import React, { useState } from 'react';
import { MaxEraRewardPointsEras } from 'consts';
import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks';
import Worker from 'workers/poolPerformance?worker';
import { useNetwork } from 'contexts/Network';
import { useValidators } from 'contexts/Validators/ValidatorEntries';
import { useBondedPools } from 'contexts/Pools/BondedPools';
import { useNetworkMetrics } from 'contexts/NetworkMetrics';
import { useApi } from 'contexts/Api';
import type { Sync } from '@polkadot-cloud/react/types';
import BigNumber from 'bignumber.js';
import { formatRawExposures } from 'contexts/Staking/Utils';
import { mergeDeep } from '@polkadot-cloud/utils';
import type { PoolPerformanceContextInterface } from './types';
import { defaultPoolPerformanceContext } from './defaults';

const worker = new Worker();

export const PoolPerformanceProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const { api } = useApi();
const { network } = useNetwork();
const { bondedPools } = useBondedPools();
const { activeEra } = useNetworkMetrics();
const { erasRewardPointsFetched, erasRewardPoints } = useValidators();

// Store whether pool performance data is being fetched.
const [poolRewardPointsFetched, setPoolRewardPointsFetched] =
useState<Sync>('unsynced');

// Store pool performance data.
const [poolRewardPoints, setPoolRewardPoints] = useState<
Record<string, Record<string, string>>
>({});

// Store the currently active era being processed for pool performance.
const [currentEra, setCurrentEra] = useState<BigNumber>(new BigNumber(0));

// Store the earliest era that should be processed.
const [finishEra, setFinishEra] = useState<BigNumber>(new BigNumber(0));

// Handle worker message on completed exposure check.
worker.onmessage = (message: MessageEvent) => {
if (message) {
const { data } = message;
const { task } = data;
if (task !== 'processNominationPoolsRewardData') return;

// Update state with new data.
const { poolRewardData } = data;
setPoolRewardPoints(mergeDeep(poolRewardPoints, poolRewardData));

if (currentEra.isEqualTo(finishEra)) {
setPoolRewardPointsFetched('synced');
} else {
const nextEra = BigNumber.max(currentEra.minus(1), 1);
processEra(nextEra);
}
}
};

// Start fetching pool performance calls from the current era.
const startGetPoolPerformance = async () => {
setPoolRewardPointsFetched('syncing');
setFinishEra(
BigNumber.max(activeEra.index.minus(MaxEraRewardPointsEras), 1)
);
const startEra = BigNumber.max(activeEra.index.minus(1), 1);
processEra(startEra);
};

// Get era data and send to worker.
const processEra = async (era: BigNumber) => {
if (!api) return;
setCurrentEra(era);
const result = await api.query.staking.erasStakersClipped.entries(
era.toString()
);
const exposures = formatRawExposures(result);
worker.postMessage({
task: 'processNominationPoolsRewardData',
era: era.toString(),
exposures,
bondedPools: bondedPools.map((b) => b.addresses.stash),
erasRewardPoints,
});
};

// Trigger worker to calculate pool reward data for garaphs once:
//
// - active era is synced.
// - era reward points are fetched.
// - bonded pools have been fetched.
//
// Re-calculates when any of the above change.
useEffectIgnoreInitial(() => {
if (
api &&
bondedPools.length &&
activeEra.index.isGreaterThan(0) &&
erasRewardPointsFetched === 'synced' &&
poolRewardPointsFetched === 'unsynced'
) {
startGetPoolPerformance();
}
}, [
bondedPools,
activeEra,
erasRewardPointsFetched,
poolRewardPointsFetched,
]);

// Reset state data on network change.
useEffectIgnoreInitial(() => {
setPoolRewardPoints({});
setCurrentEra(new BigNumber(0));
setFinishEra(new BigNumber(0));
setPoolRewardPointsFetched('unsynced');
}, [network]);

return (
<PoolPerformanceContext.Provider
value={{
poolRewardPointsFetched,
poolRewardPoints,
}}
>
{children}
</PoolPerformanceContext.Provider>
);
};

export const PoolPerformanceContext =
React.createContext<PoolPerformanceContextInterface>(
defaultPoolPerformanceContext
);

export const usePoolPerformance = () =>
React.useContext(PoolPerformanceContext);
10 changes: 10 additions & 0 deletions src/contexts/Pools/PoolPerformance/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import type { AnyJson } from '@polkadot-cloud/react/types';
import type { Sync } from 'types';

export interface PoolPerformanceContextInterface {
poolRewardPointsFetched: Sync;
poolRewardPoints: AnyJson;
}
4 changes: 2 additions & 2 deletions src/contexts/UI/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ export const UIProvider = ({ children }: { children: React.ReactNode }) => {
setSideMenuOpen(v);
};

const [containerRefs, _setContainerRefs] = useState({});
const [containerRefs, setContainerRefsState] = useState({});
const setContainerRefs = (v: any) => {
_setContainerRefs(v);
setContainerRefsState(v);
};

return (
Expand Down
2 changes: 1 addition & 1 deletion src/library/ListItem/Labels/PoolBonded.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export const PoolBonded = ({

return (
<>
<ValidatorStatusWrapper $status={nominationStatus}>
<ValidatorStatusWrapper $status={nominationStatus} $noMargin>
<h5>
{nominationStatus === null || !eraStakers.stakers.length
? `${t('syncing')}...`
Expand Down
18 changes: 12 additions & 6 deletions src/library/ListItem/Wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ export const Wrapper = styled.div`
--height-top-row: 3.25rem;
--height-bottom-row: 5rem;

&.pool,
&.member {
--height-bottom-row: 2.75rem;
}
&.pool-join {
--height-bottom-row: 7.5rem;
}

--height-total: calc(var(--height-top-row) + var(--height-bottom-row));

Expand Down Expand Up @@ -139,16 +141,20 @@ export const Labels = styled.div`
margin-right: 0;

button {
color: var(--accent-color-primary);
background: none;
color: var(--accent-color-secondary);
font-family: InterSemiBold, sans-serif;
font-size: 0.95rem;
display: flex;
flex-flow: row wrap;
align-items: center;
width: auto;
height: auto;
border-radius: none;
border-radius: 0.75rem;
padding: 0.25rem 0.75rem;

&:hover {
opacity: 1;
}
> svg {
margin-left: 0.3rem;
}
Expand Down Expand Up @@ -338,9 +344,9 @@ export const ValidatorPulseWrapper = styled.div`
var(--shimmer-foreground) 100%
);
background-repeat: no-repeat;
background-size: 600px 104px;
background-size: 500px 104px;
animation-duration: 1.5s;
opacity: 0.15;
opacity: 0.2;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: shimmer;
Expand Down
Loading
Loading