Skip to content

Commit

Permalink
feat(ux): Pool rewards worker & pool performance graphs (#1547)
Browse files Browse the repository at this point in the history
Co-authored-by: TingALin <[email protected]>
  • Loading branch information
Ross Bulat and TingALin authored Oct 24, 2023
1 parent 1ecf765 commit be4a556
Show file tree
Hide file tree
Showing 18 changed files with 500 additions and 70 deletions.
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

0 comments on commit be4a556

Please sign in to comment.