From 965d3e182c77e0b6d46c2d1c603e74a30cd7be92 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Thu, 4 Apr 2024 15:14:30 +0700 Subject: [PATCH] feat: Pool performance data to batches. Per-page fetching, pool join subset. (#2057) Co-authored-by: Ting A Lin --- src/Providers.tsx | 2 + src/canvas/JoinPool/Header.tsx | 11 +- .../JoinPool/Overview/PerformanceGraph.tsx | 13 +- src/canvas/JoinPool/index.tsx | 50 ++-- src/canvas/JoinPool/types.ts | 1 - src/canvas/PoolMembers/Lists/Default.tsx | 38 +-- src/canvas/PoolMembers/Lists/FetchPage.tsx | 36 +-- src/canvas/PoolMembers/Lists/types.ts | 1 - src/consts.ts | 2 +- src/contexts/Filters/defaults.ts | 16 +- src/contexts/Filters/index.tsx | 10 +- src/contexts/Pools/BondedPools/defaults.ts | 4 +- src/contexts/Pools/BondedPools/index.tsx | 6 + src/contexts/Pools/BondedPools/types.ts | 7 +- src/contexts/Pools/JoinPools/defaults.ts | 12 + src/contexts/Pools/JoinPools/index.tsx | 69 +++++ src/contexts/Pools/JoinPools/types.ts | 9 + .../Pools/PoolPerformance/defaults.ts | 22 +- src/contexts/Pools/PoolPerformance/index.tsx | 255 +++++++++++++----- src/contexts/Pools/PoolPerformance/types.ts | 59 +++- src/controllers/SubscanController/index.ts | 4 +- src/kits/Structure/Tx/Signer.tsx | 2 +- src/kits/Structure/Tx/Wrapper.ts | 12 +- src/library/CallToAction/index.tsx | 78 +++++- src/library/Filter/Tabs.tsx | 56 ++-- src/library/Filter/types.ts | 1 - src/library/List/defaults.ts | 14 +- src/library/Nominations/index.tsx | 1 - src/library/Pool/Rewards.tsx | 7 +- .../PoolList/{Default.tsx => index.tsx} | 138 ++++------ src/library/PoolList/types.ts | 5 - .../ValidatorList/ValidatorItem/Utils.tsx | 2 +- src/library/ValidatorList/index.tsx | 46 +--- src/library/ValidatorList/types.ts | 1 - src/locale/cn/library.json | 3 +- src/locale/cn/pages.json | 1 - src/locale/en/library.json | 3 +- src/locale/en/pages.json | 1 - src/pages/Payouts/PayoutList/index.tsx | 42 +-- src/pages/Payouts/types.ts | 1 - src/pages/Pools/Home/Favorites/index.tsx | 2 +- .../Pools/Home/Status/FindingPoolPercent.tsx | 30 +++ src/pages/Pools/Home/Status/NewMember.tsx | 84 +++--- .../Pools/Home/Status/useStatusButtons.tsx | 6 +- src/pages/Pools/Home/index.tsx | 21 +- src/theme/accents/polkadot-relay.css | 2 +- src/workers/poolPerformance.ts | 58 ++-- 47 files changed, 785 insertions(+), 459 deletions(-) create mode 100644 src/contexts/Pools/JoinPools/defaults.ts create mode 100644 src/contexts/Pools/JoinPools/index.tsx create mode 100644 src/contexts/Pools/JoinPools/types.ts rename src/library/PoolList/{Default.tsx => index.tsx} (70%) create mode 100644 src/pages/Pools/Home/Status/FindingPoolPercent.tsx diff --git a/src/Providers.tsx b/src/Providers.tsx index 53f0b9b206..b76e4e3d55 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -44,6 +44,7 @@ import type { Provider } from 'hooks/withProviders'; import { withProviders } from 'hooks/withProviders'; import { CommunityProvider } from 'contexts/Community'; import { OverlayProvider } from 'kits/Overlay/Provider'; +import { JoinPoolsProvider } from 'contexts/Pools/JoinPools'; export const Providers = () => { const { @@ -83,6 +84,7 @@ export const Providers = () => { FastUnstakeProvider, PayoutsProvider, PoolPerformanceProvider, + JoinPoolsProvider, SetupProvider, MenuProvider, TooltipProvider, diff --git a/src/canvas/JoinPool/Header.tsx b/src/canvas/JoinPool/Header.tsx index 0215f53d48..4b17920a72 100644 --- a/src/canvas/JoinPool/Header.tsx +++ b/src/canvas/JoinPool/Header.tsx @@ -25,19 +25,20 @@ export const Header = ({ autoSelected, setActiveTab, setSelectedPoolId, - setSelectedPoolCount, }: JoinPoolHeaderProps) => { const { t } = useTranslation(); const { closeCanvas } = useOverlay().canvas; // Randomly select a new pool to display. const handleChooseNewPool = () => { - // Trigger refresh of memoied selected bonded pool. - setSelectedPoolCount((prev: number) => prev + 1); + // Remove current pool from filtered so it is not selected again. + const filteredPools = filteredBondedPools.filter( + (pool) => String(pool.id) !== String(bondedPool.id) + ); // Randomly select a filtered bonded pool and set it as the selected pool. - const index = Math.ceil(Math.random() * filteredBondedPools.length - 1); - setSelectedPoolId(filteredBondedPools[index].id); + const index = Math.ceil(Math.random() * filteredPools.length - 1); + setSelectedPoolId(filteredPools[index].id); }; return ( diff --git a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx index 8e551ed3d0..f2562b61ee 100644 --- a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx +++ b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx @@ -14,7 +14,7 @@ import { } from 'chart.js'; import { useNetwork } from 'contexts/Network'; import { GraphWrapper, HeadingWrapper } from '../Wrappers'; -import { Bar } from 'react-chartjs-2'; +import { Line } from 'react-chartjs-2'; import BigNumber from 'bignumber.js'; import type { AnyJson } from 'types'; import { graphColors } from 'theme/graphs'; @@ -44,7 +44,9 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { const { mode } = useTheme(); const { openHelp } = useHelp(); const { colors } = useNetwork().networkData; - const { poolRewardPoints } = usePoolPerformance(); + const { getPoolRewardPoints } = usePoolPerformance(); + + const poolRewardPoints = getPoolRewardPoints('pool_join'); const rawEraRewardPoints = poolRewardPoints[bondedPool.addresses.stash] || {}; // Ref to the graph container. @@ -100,6 +102,7 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { }, y: { stacked: true, + beginAtZero: true, ticks: { font: { size: 10, @@ -133,6 +136,10 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { label: (context: AnyJson) => `${new BigNumber(context.parsed.y).decimalPlaces(0).toFormat()} ${t('eraPoints', { ns: 'library' })}`, }, + intersect: false, + interaction: { + mode: 'nearest', + }, }, }, }; @@ -172,7 +179,7 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { height, }} > - + diff --git a/src/canvas/JoinPool/index.tsx b/src/canvas/JoinPool/index.tsx index 7f31fbb023..94d908ab6b 100644 --- a/src/canvas/JoinPool/index.tsx +++ b/src/canvas/JoinPool/index.tsx @@ -5,13 +5,14 @@ import { CanvasFullScreenWrapper } from 'canvas/Wrappers'; import { useOverlay } from 'kits/Overlay/Provider'; import { JoinPoolInterfaceWrapper } from './Wrappers'; import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Header } from './Header'; import { Overview } from './Overview'; import { Nominations } from './Nominations'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; import { MaxEraRewardPointsEras } from 'consts'; import { useStaking } from 'contexts/Staking'; +import { useJoinPools } from 'contexts/Pools/JoinPools'; export const JoinPool = () => { const { @@ -19,21 +20,20 @@ export const JoinPool = () => { config: { options }, } = useOverlay().canvas; const { eraStakers } = useStaking(); - const { poolRewardPoints } = usePoolPerformance(); + const { poolsForJoin } = useJoinPools(); + const { getPoolRewardPoints } = usePoolPerformance(); const { poolsMetaData, bondedPools } = useBondedPools(); + const poolRewardPoints = getPoolRewardPoints('pool_join'); // The active canvas tab. const [activeTab, setActiveTab] = useState(0); - // Trigger re-render when chosen selected pool is incremented. - const [selectedPoolCount, setSelectedPoolCount] = useState(0); - // Filter bonded pools to only those that are open and that have active daily rewards for the last // `MaxEraRewardPointsEras` eras. The second filter checks if the pool is in `eraStakers` for the // active era. const filteredBondedPools = useMemo( () => - bondedPools + poolsForJoin .filter((pool) => { // Fetch reward point data for the pool. const rawEraRewardPoints = @@ -53,28 +53,39 @@ export const JoinPool = () => { staker.others.find(({ who }) => who !== pool.addresses.stash) ) ), - [bondedPools, poolRewardPoints] + [poolsForJoin, poolRewardPoints] ); - // The bonded pool to display. Use the provided `poolId`, or assign a random eligible filtered - // pool otherwise. Re-fetches when the selected pool count is incremented. - const bondedPool = useMemo( + const initialSelectedPoolId = useMemo( () => - options?.poolId - ? bondedPools.find(({ id }) => id === options.poolId) - : filteredBondedPools[ - (filteredBondedPools.length * Math.random()) << 0 - ], - [selectedPoolCount] + options?.poolId || + filteredBondedPools[(filteredBondedPools.length * Math.random()) << 0] + .id || + 0, + [] ); - // The selected bonded pool id. + // The selected bonded pool id. Assigns a random id if one is not provided. const [selectedPoolId, setSelectedPoolId] = useState( - bondedPool?.id || 0 + initialSelectedPoolId + ); + + // The bonded pool to display. Use the provided `poolId`, or assign a random eligible filtered + // pool otherwise. Re-fetches when the selected pool count is incremented. + const bondedPool = useMemo( + () => bondedPools.find(({ id }) => id === selectedPoolId), + [selectedPoolId] ); + // Close canvas if no pool id is selected. + useEffect(() => { + if (selectedPoolId === 0) { + closeCanvas(); + } + }, [selectedPoolId]); + + // Ensure bonded pool exists before rendering. Canvas should close if this is the case. if (!bondedPool) { - closeCanvas(); return null; } @@ -86,7 +97,6 @@ export const JoinPool = () => { setSelectedPoolId={setSelectedPoolId} bondedPool={bondedPool} metadata={poolsMetaData[selectedPoolId]} - setSelectedPoolCount={setSelectedPoolCount} autoSelected={options?.poolId === undefined} filteredBondedPools={filteredBondedPools} /> diff --git a/src/canvas/JoinPool/types.ts b/src/canvas/JoinPool/types.ts index b7d2dcedac..0317be1315 100644 --- a/src/canvas/JoinPool/types.ts +++ b/src/canvas/JoinPool/types.ts @@ -12,7 +12,6 @@ export interface JoinPoolHeaderProps { autoSelected: boolean; setActiveTab: (tab: number) => void; setSelectedPoolId: Dispatch>; - setSelectedPoolCount: Dispatch>; } export interface NominationsProps { diff --git a/src/canvas/PoolMembers/Lists/Default.tsx b/src/canvas/PoolMembers/Lists/Default.tsx index aab68aa687..b8d7d676bd 100644 --- a/src/canvas/PoolMembers/Lists/Default.tsx +++ b/src/canvas/PoolMembers/Lists/Default.tsx @@ -2,9 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only import { isNotZero } from '@w3ux/utils'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; +import { poolMembersPerPage } from 'library/List/defaults'; import { useApi } from 'contexts/Api'; import { usePoolMembers } from 'contexts/Pools/PoolMembers'; import { List, ListStatusHeader, Wrapper as ListWrapper } from 'library/List'; @@ -20,7 +20,6 @@ export const MembersListInner = ({ pagination, batchKey, members: initialMembers, - disableThrottle = false, }: DefaultMembersListProps) => { const { t } = useTranslation('pages'); const { isReady, activeEra } = useApi(); @@ -29,9 +28,6 @@ export const MembersListInner = ({ // current page const [page, setPage] = useState(1); - // current render iteration - const [renderIteration, setRenderIterationState] = useState(1); - // default list of validators const [membersDefault, setMembersDefault] = useState(initialMembers); @@ -42,26 +38,13 @@ export const MembersListInner = ({ // is this the initial fetch const [fetched, setFetched] = useState('unsynced'); - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; - // pagination - const totalPages = Math.ceil(members.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(members.length / poolMembersPerPage); + const pageEnd = page * poolMembersPerPage - 1; + const pageStart = pageEnd - (poolMembersPerPage - 1); // get throttled subset or entire list - const listMembers = members.slice(pageStart).slice(0, listItemsPerPage); + const listMembers = members.slice(pageStart).slice(0, poolMembersPerPage); // handle validator list bootstrapping const setupMembersList = () => { @@ -85,15 +68,6 @@ export const MembersListInner = ({ } }, [isReady, fetched, activeEra.index]); - // Render throttle. - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - return !members.length ? null : ( diff --git a/src/canvas/PoolMembers/Lists/FetchPage.tsx b/src/canvas/PoolMembers/Lists/FetchPage.tsx index 0f56e0f7ea..7962735cad 100644 --- a/src/canvas/PoolMembers/Lists/FetchPage.tsx +++ b/src/canvas/PoolMembers/Lists/FetchPage.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; +import { poolMembersPerPage } from 'library/List/defaults'; import { usePlugins } from 'contexts/Plugins'; import { useActivePool } from 'contexts/Pools/ActivePool'; import { usePoolMembers } from 'contexts/Pools/PoolMembers'; @@ -21,7 +21,6 @@ import { SubscanController } from 'controllers/SubscanController'; export const MembersListInner = ({ pagination, batchKey, - disableThrottle = false, memberCount, }: FetchpageMembersListProps) => { const { t } = useTranslation('pages'); @@ -40,26 +39,10 @@ export const MembersListInner = ({ // current page. const [page, setPage] = useState(1); - // current render iteration. - const [renderIteration, setRenderIterationState] = useState(1); - - // render throttle iteration. - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; - // pagination - const totalPages = Math.ceil(Number(memberCount) / listItemsPerPage); - const pageEnd = listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(Number(memberCount) / poolMembersPerPage); + const pageEnd = poolMembersPerPage - 1; + const pageStart = pageEnd - (poolMembersPerPage - 1); // handle validator list bootstrapping const fetchingMemberList = useRef(false); @@ -85,7 +68,7 @@ export const MembersListInner = ({ // get throttled subset or entire list const listMembers = poolMembersApi .slice(pageStart) - .slice(0, listItemsPerPage); + .slice(0, poolMembersPerPage); // Refetch list when page changes. useEffect(() => { @@ -109,15 +92,6 @@ export const MembersListInner = ({ } }, [fetchedPoolMembersApi, activePool]); - // Render throttle. - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - return ( diff --git a/src/canvas/PoolMembers/Lists/types.ts b/src/canvas/PoolMembers/Lists/types.ts index c6f74832ef..ea4d49b511 100644 --- a/src/canvas/PoolMembers/Lists/types.ts +++ b/src/canvas/PoolMembers/Lists/types.ts @@ -6,7 +6,6 @@ import type { AnyJson } from 'types'; export interface MembersListProps { pagination: boolean; batchKey: string; - disableThrottle?: boolean; selectToggleable?: boolean; } diff --git a/src/consts.ts b/src/consts.ts index 752b99aca4..a5eb722df6 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -37,4 +37,4 @@ export const TipsThresholdMedium = 1200; * Misc Values */ export const MaxPayoutDays = 60; -export const MaxEraRewardPointsEras = 14; +export const MaxEraRewardPointsEras = 10; diff --git a/src/contexts/Filters/defaults.ts b/src/contexts/Filters/defaults.ts index 32c3ed62b8..5ebda7af35 100644 --- a/src/contexts/Filters/defaults.ts +++ b/src/contexts/Filters/defaults.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */ -import type { FiltersContextInterface } from './types'; +import type { FilterItem, FiltersContextInterface } from './types'; export const defaultFiltersInterface: FiltersContextInterface = { getFilters: (type, group) => [], @@ -18,3 +18,17 @@ export const defaultFiltersInterface: FiltersContextInterface = { applyFilters: (type, g, l, f) => {}, applyOrder: (g, l, f) => {}, }; + +export const defaultIncludes: FilterItem[] = [ + { + key: 'pools', + filters: ['active'], + }, +]; + +export const defaultExcludes: FilterItem[] = [ + { + key: 'pools', + filters: ['locked', 'destroying'], + }, +]; diff --git a/src/contexts/Filters/index.tsx b/src/contexts/Filters/index.tsx index b587f7e069..1d2a62a0f0 100644 --- a/src/contexts/Filters/index.tsx +++ b/src/contexts/Filters/index.tsx @@ -4,7 +4,11 @@ import type { ReactNode } from 'react'; import { createContext, useContext, useState } from 'react'; import type { AnyFunction, AnyJson } from 'types'; -import { defaultFiltersInterface } from './defaults'; +import { + defaultExcludes, + defaultFiltersInterface, + defaultIncludes, +} from './defaults'; import type { FilterItem, FilterItems, @@ -24,10 +28,10 @@ export const useFilters = () => useContext(FiltersContext); export const FiltersProvider = ({ children }: { children: ReactNode }) => { // groups along with their includes - const [includes, setIncludes] = useState([]); + const [includes, setIncludes] = useState(defaultIncludes); // groups along with their excludes. - const [excludes, setExcludes] = useState([]); + const [excludes, setExcludes] = useState(defaultExcludes); // groups along with their order. const [orders, setOrders] = useState([]); diff --git a/src/contexts/Pools/BondedPools/defaults.ts b/src/contexts/Pools/BondedPools/defaults.ts index 1859dc2279..b87911defc 100644 --- a/src/contexts/Pools/BondedPools/defaults.ts +++ b/src/contexts/Pools/BondedPools/defaults.ts @@ -14,9 +14,11 @@ export const defaultBondedPoolsContext: BondedPoolsContextState = { getPoolNominationStatusCode: (statuses) => '', getAccountPoolRoles: (address) => null, replacePoolRoles: (poolId, roleEdits) => {}, - poolSearchFilter: (filteredPools, searchTerm) => {}, + poolSearchFilter: (filteredPools, searchTerm) => [], bondedPools: [], poolsMetaData: {}, poolsNominations: {}, updatePoolNominations: (id, nominations) => {}, + poolListActiveTab: 'Active', + setPoolListActiveTab: (tab) => {}, }; diff --git a/src/contexts/Pools/BondedPools/index.tsx b/src/contexts/Pools/BondedPools/index.tsx index c070f79a8f..f064555b52 100644 --- a/src/contexts/Pools/BondedPools/index.tsx +++ b/src/contexts/Pools/BondedPools/index.tsx @@ -12,6 +12,7 @@ import type { MaybePool, NominationStatuses, PoolNominations, + PoolTab, } from './types'; import { useStaking } from 'contexts/Staking'; import type { AnyApi, AnyJson, MaybeAddress, Sync } from 'types'; @@ -56,6 +57,9 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { Record >({}); + // Store pool list active tab. Defaults to `Active` tab. + const [poolListActiveTab, setPoolListActiveTab] = useState('Active'); + // Fetch all bonded pool entries and their metadata. const fetchBondedPools = async () => { if (!api || bondedPoolsSynced.current !== 'unsynced') { @@ -425,6 +429,8 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { poolsMetaData, poolsNominations, updatePoolNominations, + poolListActiveTab, + setPoolListActiveTab, }} > {children} diff --git a/src/contexts/Pools/BondedPools/types.ts b/src/contexts/Pools/BondedPools/types.ts index 030c39d615..547cdbb5ab 100644 --- a/src/contexts/Pools/BondedPools/types.ts +++ b/src/contexts/Pools/BondedPools/types.ts @@ -4,6 +4,7 @@ import type { AnyApi, AnyJson, MaybeAddress } from 'types'; import type { ActiveBondedPool } from '../ActivePool/types'; import type { AnyFilter } from 'library/Filter/types'; +import type { Dispatch, SetStateAction } from 'react'; export interface BondedPoolsContextState { queryBondedPool: (poolId: number) => AnyApi; @@ -18,11 +19,13 @@ export interface BondedPoolsContextState { getPoolNominationStatusCode: (statuses: NominationStatuses | null) => string; getAccountPoolRoles: (address: MaybeAddress) => AnyApi; replacePoolRoles: (poolId: number, roleEdits: AnyJson) => void; - poolSearchFilter: (filteredPools: AnyFilter, searchTerm: string) => void; + poolSearchFilter: (filteredPools: AnyFilter, searchTerm: string) => AnyJson[]; bondedPools: BondedPool[]; poolsMetaData: Record; poolsNominations: Record; updatePoolNominations: (id: number, nominations: string[]) => void; + poolListActiveTab: PoolTab; + setPoolListActiveTab: Dispatch>; } export type BondedPool = ActiveBondedPool & { @@ -60,3 +63,5 @@ export type AccountPoolRoles = { nominator: number[]; bouncer: number[]; } | null; + +export type PoolTab = 'All' | 'Active' | 'Locked' | 'Destroying'; diff --git a/src/contexts/Pools/JoinPools/defaults.ts b/src/contexts/Pools/JoinPools/defaults.ts new file mode 100644 index 0000000000..342f1ca12a --- /dev/null +++ b/src/contexts/Pools/JoinPools/defaults.ts @@ -0,0 +1,12 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */ + +import type { JoinPoolsContextInterface } from './types'; + +export const defaultJoinPoolsContext: JoinPoolsContextInterface = { + poolsForJoin: [], + startJoinPoolFetch: () => {}, +}; + +export const MaxPoolsForJoin = 8; diff --git a/src/contexts/Pools/JoinPools/index.tsx b/src/contexts/Pools/JoinPools/index.tsx new file mode 100644 index 0000000000..32f6342896 --- /dev/null +++ b/src/contexts/Pools/JoinPools/index.tsx @@ -0,0 +1,69 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactNode } from 'react'; +import { createContext, useContext, useState } from 'react'; +import type { JoinPoolsContextInterface } from './types'; +import { MaxPoolsForJoin, defaultJoinPoolsContext } from './defaults'; +import { useEffectIgnoreInitial } from '@w3ux/hooks'; +import { useBondedPools } from '../BondedPools'; +import { useApi } from 'contexts/Api'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { usePoolPerformance } from '../PoolPerformance'; +import type { BondedPool } from '../BondedPools/types'; +import { shuffle } from '@w3ux/utils'; + +export const JoinPoolsContext = createContext( + defaultJoinPoolsContext +); + +export const useJoinPools = () => useContext(JoinPoolsContext); + +export const JoinPoolsProvider = ({ children }: { children: ReactNode }) => { + const { api, activeEra } = useApi(); + const { bondedPools } = useBondedPools(); + const { erasRewardPointsFetched } = useValidators(); + const { getPoolPerformanceTask, startPoolRewardPointsFetch } = + usePoolPerformance(); + + // Save the bonded pools subset for pool joining. + const [poolsForJoin, setPoolsToJoin] = useState([]); + + // Start finding pools to join. + const startJoinPoolFetch = () => { + startPoolRewardPointsFetch( + 'pool_join', + poolsForJoin.map(({ addresses }) => addresses.stash) + ); + }; + + // Trigger worker to calculate join pool performance data. + useEffectIgnoreInitial(() => { + if ( + api && + bondedPools.length && + activeEra.index.isGreaterThan(0) && + erasRewardPointsFetched === 'synced' && + getPoolPerformanceTask('pool_join')?.status === 'unsynced' + ) { + // Generate a subset of pools to fetch performance data for. TODO: Send pools to JoinPool + // canvas and only select those. Move this logic to a separate context. + const poolJoinSelection = shuffle( + bondedPools.filter(({ state }) => state === 'Open') + ).slice(0, MaxPoolsForJoin); + + setPoolsToJoin(poolJoinSelection); + } + }, [ + bondedPools, + activeEra, + erasRewardPointsFetched, + getPoolPerformanceTask('pool_join'), + ]); + + return ( + + {children} + + ); +}; diff --git a/src/contexts/Pools/JoinPools/types.ts b/src/contexts/Pools/JoinPools/types.ts new file mode 100644 index 0000000000..fe49a8aa8f --- /dev/null +++ b/src/contexts/Pools/JoinPools/types.ts @@ -0,0 +1,9 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { BondedPool } from '../BondedPools/types'; + +export interface JoinPoolsContextInterface { + poolsForJoin: BondedPool[]; + startJoinPoolFetch: () => void; +} diff --git a/src/contexts/Pools/PoolPerformance/defaults.ts b/src/contexts/Pools/PoolPerformance/defaults.ts index 26e9287fc6..3d839fb70f 100644 --- a/src/contexts/Pools/PoolPerformance/defaults.ts +++ b/src/contexts/Pools/PoolPerformance/defaults.ts @@ -1,10 +1,24 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */ -import type { PoolPerformanceContextInterface } from './types'; +import BigNumber from 'bignumber.js'; +import type { + PoolPerformanceContextInterface, + PoolPerformanceTaskStatus, +} from './types'; +export const defaultPoolPerformanceTask: PoolPerformanceTaskStatus = { + status: 'unsynced', + addresses: [], + startEra: BigNumber(0), + currentEra: BigNumber(0), + endEra: BigNumber(0), +}; export const defaultPoolPerformanceContext: PoolPerformanceContextInterface = { - poolRewardPointsFetched: 'unsynced', - poolRewardPoints: {}, + getPoolRewardPoints: () => ({}), + getPoolPerformanceTask: (key) => defaultPoolPerformanceTask, + setNewPoolPerformanceTask: (key, status, addresses) => {}, + updatePoolPerformanceTask: (key, status) => {}, + startPoolRewardPointsFetch: (key, addresses) => {}, }; diff --git a/src/contexts/Pools/PoolPerformance/index.tsx b/src/contexts/Pools/PoolPerformance/index.tsx index 304c30bc51..3efa8aff97 100644 --- a/src/contexts/Pools/PoolPerformance/index.tsx +++ b/src/contexts/Pools/PoolPerformance/index.tsx @@ -2,20 +2,28 @@ // SPDX-License-Identifier: GPL-3.0-only import type { ReactNode } from 'react'; -import { createContext, useContext, useState } from 'react'; +import { createContext, useContext, useRef, useState } from 'react'; import { MaxEraRewardPointsEras } from 'consts'; import { useEffectIgnoreInitial } from '@w3ux/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 { useApi } from 'contexts/Api'; import BigNumber from 'bignumber.js'; -import { mergeDeep } from '@w3ux/utils'; +import { mergeDeep, setStateWithRef } from '@w3ux/utils'; import { useStaking } from 'contexts/Staking'; import { formatRawExposures } from 'contexts/Staking/Utils'; -import type { PoolPerformanceContextInterface } from './types'; -import { defaultPoolPerformanceContext } from './defaults'; +import type { + PoolPerformanceContextInterface, + PoolPerformanceTasks, + PoolRewardPoints, + PoolRewardPointsMap, + PoolRewardPointsKey, +} from './types'; +import { + defaultPoolPerformanceTask, + defaultPoolPerformanceContext, +} from './defaults'; import type { Sync } from 'types'; const worker = new Worker(); @@ -31,64 +39,153 @@ export const PoolPerformanceProvider = ({ children: ReactNode; }) => { const { network } = useNetwork(); - const { bondedPools } = useBondedPools(); const { getPagedErasStakers } = useStaking(); + const { erasRewardPoints } = useValidators(); const { api, activeEra, isPagedRewardsActive } = useApi(); - const { erasRewardPointsFetched, erasRewardPoints } = useValidators(); - // Store whether pool performance data is being fetched. - const [poolRewardPointsFetched, setPoolRewardPointsFetched] = - useState('unsynced'); + // Store pool performance task data under a given key as it is being fetched . NOTE: Requires a + // ref to be accessed in `processEra` before re-render. + const [tasks, setTasks] = useState({}); + const tasksRef = useRef(tasks); - // Store pool performance data. - const [poolRewardPoints, setPoolRewardPoints] = useState< - Record> - >({}); + // Store pool performance data. NOTE: Requires a ref to update state with current data. + const [poolRewardPoints, setPoolRewardPointsState] = + useState({}); + const poolRewardPointsRef = useRef(poolRewardPoints); - // Store the currently active era being processed for pool performance. - const [currentEra, setCurrentEra] = useState(new BigNumber(0)); + // Gets a batch of pool reward points, or returns an empty object otherwise. + const getPoolRewardPoints = (key: PoolRewardPointsKey) => + poolRewardPoints?.[key] || {}; - // Store the earliest era that should be processed. - const [finishEra, setFinishEra] = useState(new BigNumber(0)); + // Sets a batch of pool reward points. + const setPoolRewardPoints = ( + key: PoolRewardPointsKey, + batch: PoolRewardPoints + ) => { + const newRewardPoints = { + ...poolRewardPointsRef.current, + [key]: batch, + }; - // Handle worker message on completed exposure check. - worker.onmessage = (message: MessageEvent) => { - if (message) { - const { data } = message; - const { task } = data; - if (task !== 'processNominationPoolsRewardData') { - return; - } + setStateWithRef( + newRewardPoints, + setPoolRewardPointsState, + poolRewardPointsRef + ); + }; - // Update state with new data. - const { poolRewardData } = data; - setPoolRewardPoints(mergeDeep(poolRewardPoints, poolRewardData)); + // Gets whether pool performance data is being fetched under a given key. + const getPoolPerformanceTask = (key: PoolRewardPointsKey) => + tasks[key] || defaultPoolPerformanceTask; - if (currentEra.isEqualTo(finishEra)) { - setPoolRewardPointsFetched('synced'); - } else { - const nextEra = BigNumber.max(currentEra.minus(1), 1); - processEra(nextEra); - } + // Sets a pool performance task under a given key. + const setNewPoolPerformanceTask = ( + key: PoolRewardPointsKey, + status: Sync, + addresses: string[], + currentEra: BigNumber, + endEra: BigNumber + ) => { + const startEra = activeEra.index; + + setStateWithRef( + { + ...tasksRef.current, + [key]: { status, addresses, startEra, endEra, currentEra }, + }, + setTasks, + tasksRef + ); + + // Reset pool reward points for the given key. + if (status === 'syncing') { + setStateWithRef( + { + ...poolRewardPointsRef.current, + [key]: {}, + }, + setPoolRewardPointsState, + poolRewardPointsRef + ); + } + }; + + // Set current era for performance fetched key. + const updateTaskCurrentEra = (key: PoolRewardPointsKey, era: BigNumber) => { + if (!getPoolPerformanceTask(key)) { + return; + } + setStateWithRef( + { + ...tasksRef.current, + [key]: { ...tasksRef.current[key], currentEra: era }, + }, + setTasks, + tasksRef + ); + }; + + // Updates an existing performance fetched key with a new status. + const updatePoolPerformanceTask = ( + key: PoolRewardPointsKey, + status: Sync + ) => { + if (!getPoolPerformanceTask(key)) { + return; } + setStateWithRef( + { + ...tasksRef.current, + [key]: { ...tasksRef.current[key], status }, + }, + setTasks, + tasksRef + ); }; - // Start fetching pool performance calls from the current era. - const startGetPoolPerformance = async () => { - setPoolRewardPointsFetched('syncing'); - setFinishEra( - BigNumber.max(activeEra.index.minus(MaxEraRewardPointsEras), 1) + // Start fetching pool performance data, starting from the current era. + const startPoolRewardPointsFetch = async ( + key: PoolRewardPointsKey, + addresses: string[] + ) => { + // Set as synced and exit early if there are no addresses to process. + if (!addresses.length) { + setNewPoolPerformanceTask( + key, + 'synced', + addresses, + activeEra.index, + activeEra.index + ); + return; + } + + // If the addresses have not changed for this key, exit early. + const current = getPoolPerformanceTask(key); + if (current.addresses.toString() === addresses.toString()) { + return; + } + + const currentEra = BigNumber.max(activeEra.index.minus(1)); + const endEra = BigNumber.max( + activeEra.index.minus(MaxEraRewardPointsEras), + 1 ); - const startEra = BigNumber.max(activeEra.index.minus(1), 1); - processEra(startEra); + // Set as syncing and start processing. + setNewPoolPerformanceTask(key, 'syncing', addresses, currentEra, endEra); + + // Start processing from the previous active era. + processEra(key, currentEra); }; // Get era data and send to worker. - const processEra = async (era: BigNumber) => { + const processEra = async (key: PoolRewardPointsKey, era: BigNumber) => { if (!api) { return; } - setCurrentEra(era); + + // NOTE: This will not make any difference on the first run. + updateTaskCurrentEra(key, era); let exposures; if (isPagedRewardsActive(era)) { @@ -103,52 +200,64 @@ export const PoolPerformanceProvider = ({ exposures = formatRawExposures(result); } + const addresses = tasksRef.current[key]?.addresses || []; + worker.postMessage({ task: 'processNominationPoolsRewardData', + key, era: era.toString(), exposures, - bondedPools: bondedPools.map((b) => b.addresses.stash), + addresses, 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(); + // Handle worker message on completed exposure check. + worker.onmessage = (message: MessageEvent) => { + if (message) { + const { data } = message; + const { task, key, addresses } = data; + + if (task !== 'processNominationPoolsRewardData') { + return; + } + + // If addresses for the given key have changed or been removed, ignore the result. + const current = getPoolPerformanceTask(key); + + if (current.addresses.toString() !== addresses.toString()) { + return; + } + + // Update state with new data. + setPoolRewardPoints( + key, + mergeDeep(getPoolRewardPoints(key), data.poolRewardData) + ); + + if (current.currentEra.isEqualTo(current.endEra)) { + updatePoolPerformanceTask(key, 'synced'); + } else { + const nextEra = BigNumber.max(current.currentEra.minus(1), 1); + processEra(key, nextEra); + } } - }, [ - bondedPools, - activeEra, - erasRewardPointsFetched, - poolRewardPointsFetched, - ]); + }; // Reset state data on network change. useEffectIgnoreInitial(() => { - setPoolRewardPoints({}); - setCurrentEra(new BigNumber(0)); - setFinishEra(new BigNumber(0)); - setPoolRewardPointsFetched('unsynced'); + setStateWithRef({}, setPoolRewardPointsState, poolRewardPointsRef); + setStateWithRef({}, setTasks, tasksRef); }, [network]); return ( {children} diff --git a/src/contexts/Pools/PoolPerformance/types.ts b/src/contexts/Pools/PoolPerformance/types.ts index 16ce7f6272..62828a25f8 100644 --- a/src/contexts/Pools/PoolPerformance/types.ts +++ b/src/contexts/Pools/PoolPerformance/types.ts @@ -1,9 +1,62 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import type { AnyJson, Sync } from 'types'; +import type BigNumber from 'bignumber.js'; +import type { Sync } from 'types'; export interface PoolPerformanceContextInterface { - poolRewardPointsFetched: Sync; - poolRewardPoints: AnyJson; + getPoolRewardPoints: (key: PoolRewardPointsKey) => PoolRewardPoints; + getPoolPerformanceTask: ( + key: PoolRewardPointsKey + ) => PoolPerformanceTaskStatus; + setNewPoolPerformanceTask: ( + key: PoolRewardPointsKey, + status: Sync, + addresses: string[], + currentEra: BigNumber, + endEra: BigNumber + ) => void; + updatePoolPerformanceTask: (key: PoolRewardPointsKey, status: Sync) => void; + startPoolRewardPointsFetch: ( + key: PoolRewardPointsKey, + addresses: string[] + ) => void; } + +// Fetching status for keys. +export type PoolPerformanceTasks = Partial< + Record +>; + +// Performance fetching status. +export interface PoolPerformanceTaskStatus { + status: Sync; + addresses: string[]; + startEra: BigNumber; + currentEra: BigNumber; + endEra: BigNumber; +} + +/* + * Batch Key -> Pool Address -> Era -> Points. + */ + +// Supported reward points batch keys. +export type PoolRewardPointsKey = 'pool_join' | 'pool_page'; + +// Pool reward batches, keyed by batch key. +export type PoolRewardPointsMap = Partial>; + +// Pool reward points are keyed by era, then by pool address. + +export type PoolRewardPoints = Record; + +export type PointsByEra = Record; + +// Type aliases to better understand pool reward records. + +export type PoolAddress = string; + +export type EraKey = number; + +export type EraPoints = string; diff --git a/src/controllers/SubscanController/index.ts b/src/controllers/SubscanController/index.ts index 4d30175e9c..b19a08431d 100644 --- a/src/controllers/SubscanController/index.ts +++ b/src/controllers/SubscanController/index.ts @@ -12,7 +12,7 @@ import type { import type { Locale } from 'date-fns'; import { format, fromUnixTime, getUnixTime, subDays } from 'date-fns'; import type { PoolMember } from 'contexts/Pools/PoolMembers/types'; -import { listItemsPerPage } from 'library/List/defaults'; +import { poolMembersPerPage } from 'library/List/defaults'; export class SubscanController { // ------------------------------------------------------ @@ -160,7 +160,7 @@ export class SubscanController { ): Promise => { const result = await this.makeRequest(this.ENDPOINTS.poolMembers, { pool_id: poolId, - row: listItemsPerPage, + row: poolMembersPerPage, page: page - 1, }); if (!result?.list) { diff --git a/src/kits/Structure/Tx/Signer.tsx b/src/kits/Structure/Tx/Signer.tsx index b763f2d700..4a35574c79 100644 --- a/src/kits/Structure/Tx/Signer.tsx +++ b/src/kits/Structure/Tx/Signer.tsx @@ -23,7 +23,7 @@ export const Signer = ({ /   {dangerMessage} diff --git a/src/kits/Structure/Tx/Wrapper.ts b/src/kits/Structure/Tx/Wrapper.ts index 89ff719b76..927c5010ce 100644 --- a/src/kits/Structure/Tx/Wrapper.ts +++ b/src/kits/Structure/Tx/Wrapper.ts @@ -127,13 +127,13 @@ export const SignerWrapper = styled.p` .not-enough { margin-left: 0.5rem; - } - .danger { - color: var(--status-danger-color); - } + > .danger { + color: var(--status-danger-color); + } - > .icon { - margin-right: 0.3rem; + > .icon { + margin-right: 0.3rem; + } } `; diff --git a/src/library/CallToAction/index.tsx b/src/library/CallToAction/index.tsx index 259285960a..6c6ac900d8 100644 --- a/src/library/CallToAction/index.tsx +++ b/src/library/CallToAction/index.tsx @@ -37,10 +37,20 @@ export const CallToActionWrapper = styled.div` &:nth-child(1) { flex-grow: 1; - @media (min-width: 651px) { border-right: 1px solid var(--border-primary-color); padding-right: 1rem; + + &.fixedWidth { + flex-grow: 0; + flex-basis: 70%; + } + } + + @media (max-width: 650px) { + &.fixedWidth { + flex-basis: 100%; + } } } @@ -170,8 +180,68 @@ export const CallToActionWrapper = styled.div` justify-content: center; flex-wrap: nowrap; font-size: 1.3rem; + line-height: 1.3rem; width: 100%; + .counter { + font-family: InterBold, sans-serif; + font-size: 1.1rem; + margin-left: 0.75rem; + } + + .loader { + height: 0.8rem; + margin-left: 1.6rem; + aspect-ratio: 5; + --_g: no-repeat radial-gradient(farthest-side, white 94%, #0000); + background: var(--_g), var(--_g), var(--_g), var(--_g); + background-size: 20% 100%; + animation: + l40-1 0.75s infinite alternate, + l40-2 1.5s infinite alternate; + } + @keyframes l40-1 { + 0%, + 10% { + background-position: + 0 0, + 0 0, + 0 0, + 0 0; + } + 33% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(100% / 3) 0, + calc(100% / 3) 0; + } + 66% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(2 * 100% / 3) 0, + calc(2 * 100% / 3) 0; + } + 90%, + 100% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(2 * 100% / 3) 0, + 100% 0; + } + } + @keyframes l40-2 { + 0%, + 49.99% { + transform: scale(1); + } + 50%, + 100% { + transform: scale(-1); + } + } &:disabled { cursor: default; } @@ -180,6 +250,12 @@ export const CallToActionWrapper = styled.div` margin: 0 0.75rem; } } + + &.inactive { + > button { + cursor: default; + } + } } } } diff --git a/src/library/Filter/Tabs.tsx b/src/library/Filter/Tabs.tsx index 51f0bfaf6e..48d4e14748 100644 --- a/src/library/Filter/Tabs.tsx +++ b/src/library/Filter/Tabs.tsx @@ -1,42 +1,46 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { useState } from 'react'; import { useFilters } from 'contexts/Filters'; import { TabsWrapper, TabWrapper } from './Wrappers'; import type { FilterTabsProps } from './types'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import type { PoolTab } from 'contexts/Pools/BondedPools/types'; -export const Tabs = ({ config, activeIndex }: FilterTabsProps) => { +export const Tabs = ({ config }: FilterTabsProps) => { const { resetFilters, setMultiFilters } = useFilters(); - - const [active, setActive] = useState(activeIndex); + const { poolListActiveTab, setPoolListActiveTab } = useBondedPools(); return ( - {config.map((c, i) => ( - { - if (c.includes?.length) { - setMultiFilters('include', 'pools', c.includes, true); - } else { - resetFilters('include', 'pools'); - } + {config.map((c, i) => { + const label = c.label as PoolTab; + + return ( + { + if (c.includes?.length) { + setMultiFilters('include', 'pools', c.includes, true); + } else { + resetFilters('include', 'pools'); + } - if (c.excludes?.length) { - setMultiFilters('exclude', 'pools', c.excludes, true); - } else { - resetFilters('exclude', 'pools'); - } + if (c.excludes?.length) { + setMultiFilters('exclude', 'pools', c.excludes, true); + } else { + resetFilters('exclude', 'pools'); + } - setActive(i); - }} - > - {c.label} - - ))} + setPoolListActiveTab(label); + }} + > + {label} + + ); + })} ); }; diff --git a/src/library/Filter/types.ts b/src/library/Filter/types.ts index dbc2e83277..aa6a4ca3aa 100644 --- a/src/library/Filter/types.ts +++ b/src/library/Filter/types.ts @@ -19,7 +19,6 @@ export interface LargerFilterItemProps { } export interface FilterTabsProps { config: FilterConfig[]; - activeIndex: number; } export interface FilterConfig { diff --git a/src/library/List/defaults.ts b/src/library/List/defaults.ts index 0aa87f0e92..1cfbba438e 100644 --- a/src/library/List/defaults.ts +++ b/src/library/List/defaults.ts @@ -16,8 +16,14 @@ export const defaultContext: ListContextInterface = { selectToggleable: true, }; -// Total list items to show per page. -export const listItemsPerPage = 25; +// The amount of pools per page. +export const poolsPerPage = 21; -// If throttling a list of items, how many items to show per batch. -export const listItemsPerBatch = 25; +// The amount of validators per page. +export const validatorsPerPage = 30; + +// The amount of payouts per page. +export const payoutsPerPage = 50; + +// The amount of pool members per page. +export const poolMembersPerPage = 50; diff --git a/src/library/Nominations/index.tsx b/src/library/Nominations/index.tsx index 442a523faa..5fd5f5e2c2 100644 --- a/src/library/Nominations/index.tsx +++ b/src/library/Nominations/index.tsx @@ -143,7 +143,6 @@ export const Nominations = ({ format="nomination" refetchOnListUpdate allowMoreCols - disableThrottle allowListFormat={false} /> ) : poolDestroying ? ( diff --git a/src/library/Pool/Rewards.tsx b/src/library/Pool/Rewards.tsx index 18bd3f9bae..ec18e8c2a4 100644 --- a/src/library/Pool/Rewards.tsx +++ b/src/library/Pool/Rewards.tsx @@ -24,7 +24,9 @@ export const Rewards = ({ address, displayFor = 'default' }: RewardProps) => { const { isReady } = useApi(); const { setTooltipTextAndOpen } = useTooltip(); const { eraPointsBoundaries } = useValidators(); - const { poolRewardPoints, poolRewardPointsFetched } = usePoolPerformance(); + const { getPoolRewardPoints, getPoolPerformanceTask } = usePoolPerformance(); + + const poolRewardPoints = getPoolRewardPoints('pool_page'); const eraRewardPoints = Object.fromEntries( Object.entries(poolRewardPoints[address] || {}).map(([k, v]: AnyJson) => [ @@ -38,7 +40,8 @@ export const Rewards = ({ address, displayFor = 'default' }: RewardProps) => { const prefilledPoints = prefillEraPoints(Object.values(normalisedPoints)); const empty = Object.values(poolRewardPoints).length === 0; - const syncing = !isReady || poolRewardPointsFetched !== 'synced'; + const syncing = + !isReady || getPoolPerformanceTask('pool_page').status !== 'synced'; const tooltipText = `${MaxEraRewardPointsEras} ${t('dayPoolPerformance')}`; return ( diff --git a/src/library/PoolList/Default.tsx b/src/library/PoolList/index.tsx similarity index 70% rename from src/library/PoolList/Default.tsx rename to src/library/PoolList/index.tsx index a281d147fb..a348e6f6b7 100644 --- a/src/library/PoolList/Default.tsx +++ b/src/library/PoolList/index.tsx @@ -3,13 +3,11 @@ import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { isNotZero } from '@w3ux/utils'; import { motion } from 'framer-motion'; import type { FormEvent } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; -import { useApi } from 'contexts/Api'; +import { poolsPerPage } from 'library/List/defaults'; import { useFilters } from 'contexts/Filters'; import { useBondedPools } from 'contexts/Pools/BondedPools'; import { useTheme } from 'contexts/Themes'; @@ -30,130 +28,114 @@ import { usePoolList } from './context'; import type { PoolListProps } from './types'; import type { BondedPool } from 'contexts/Pools/BondedPools/types'; import { useSyncing } from 'hooks/useSyncing'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useApi } from 'contexts/Api'; +import { useEffectIgnoreInitial } from '@w3ux/hooks'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; export const PoolList = ({ allowMoreCols, pagination, - disableThrottle, allowSearch, pools, - defaultFilters, allowListFormat = true, }: PoolListProps) => { const { t } = useTranslation('library'); - const { mode } = useTheme(); - const { isReady, activeEra } = useApi(); const { + network, networkData: { colors }, } = useNetwork(); + const { mode } = useTheme(); + const { activeEra } = useApi(); const { syncing } = useSyncing(); const { applyFilter } = usePoolFilters(); + const { erasRewardPointsFetched } = useValidators(); const { listFormat, setListFormat } = usePoolList(); - const { getFilters, setMultiFilters, getSearchTerm, setSearchTerm } = - useFilters(); - const { poolSearchFilter, poolsNominations } = useBondedPools(); + const { startPoolRewardPointsFetch } = usePoolPerformance(); + const { getFilters, getSearchTerm, setSearchTerm } = useFilters(); + const { poolSearchFilter, poolsNominations, bondedPools } = useBondedPools(); const includes = getFilters('include', 'pools'); const excludes = getFilters('exclude', 'pools'); const searchTerm = getSearchTerm('pools'); - // current page - const [page, setPage] = useState(1); + // Carry out filter of pool list. + const filterPoolList = () => { + let filteredPools = Object.assign(poolsDefault); + filteredPools = applyFilter(includes, excludes, filteredPools); + if (searchTerm) { + filteredPools = poolSearchFilter(filteredPools, searchTerm); + } + return filteredPools; + }; - // current render iteration - const [renderIteration, setRenderIterationState] = useState(1); + // The current page of pool list. + const [page, setPage] = useState(1); - // default list of pools + // Default pool list items before filtering. const [poolsDefault, setPoolsDefault] = useState(pools || []); - // manipulated list (ordering, filtering) of pools - const [listPools, setListPools] = useState(pools || []); + // Manipulated pool list items after filtering. + const [listPools, setListPools] = useState(filterPoolList()); - // is this the initial fetch - const [fetched, setFetched] = useState(false); - - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; + // Whether this the initial render. + const [synced, setSynced] = useState(false); // pagination - const totalPages = Math.ceil(listPools.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); + const totalPages = Math.ceil(listPools.length / poolsPerPage); + const pageEnd = page * poolsPerPage - 1; + const pageStart = pageEnd - (poolsPerPage - 1); - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + // get paged subset of list items. + const poolsToDisplay = listPools.slice(pageStart).slice(0, poolsPerPage); - // get throttled subset or entire list - const poolsToDisplay = disableThrottle - ? listPools - : listPools.slice(pageStart).slice(0, listItemsPerPage); - - // handle pool list bootstrapping - const setupPoolList = () => { + // Handle resetting of pool list when provided pools change. + const resetPoolList = () => { setPoolsDefault(pools || []); setListPools(pools || []); - setFetched(true); + setSynced(true); }; // handle filter / order update - const handlePoolsFilterUpdate = ( - filteredPools = Object.assign(poolsDefault) - ) => { - filteredPools = applyFilter(includes, excludes, filteredPools); - if (searchTerm) { - filteredPools = poolSearchFilter(filteredPools, searchTerm); - } + const handlePoolsFilterUpdate = () => { + const filteredPools = filterPoolList(); setListPools(filteredPools); setPage(1); - setRenderIteration(1); }; const handleSearchChange = (e: FormEvent) => { const newValue = e.currentTarget.value; - let filteredPools = Object.assign(poolsDefault); + let filteredPools: BondedPool[] = Object.assign(poolsDefault); filteredPools = applyFilter(includes, excludes, filteredPools); filteredPools = poolSearchFilter(filteredPools, newValue); // ensure no duplicates filteredPools = filteredPools.filter( - (value: BondedPool, index: number, self: BondedPool[]) => + (value, index: number, self) => index === self.findIndex((i) => i.id === value.id) ); setPage(1); - setRenderIteration(1); setListPools(filteredPools); setSearchTerm('pools', newValue); }; - // Refetch list when pool list changes. - useEffect(() => { - if (pools !== poolsDefault) { - setFetched(false); - } - }, [pools]); - - // Configure pool list when network is ready to fetch. + // Fetch pool performance data when list items or page changes. Requires `erasRewardPoints` and + // `bondedPools` to be fetched. useEffect(() => { - if (isReady && isNotZero(activeEra.index) && !fetched) { - setupPoolList(); + if (erasRewardPointsFetched && bondedPools.length) { + startPoolRewardPointsFetch( + 'pool_page', + poolsToDisplay.map(({ addresses }) => addresses.stash) + ); } - }, [isReady, fetched, activeEra.index]); + }, [JSON.stringify(listPools), page, erasRewardPointsFetched, bondedPools]); - // Render throttling. Only render a batch of pools at a time. + // Refetch list when pool list changes. useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); + if (JSON.stringify(pools) !== JSON.stringify(poolsDefault) && synced) { + resetPoolList(); } - }, [renderIterationRef.current]); + }, [JSON.stringify(pools)]); // List ui changes / validator changes trigger re-render of list. useEffect(() => { @@ -168,15 +150,10 @@ export const PoolList = ({ window.scrollTo(0, 0); }, [includes, excludes]); - // Set default filters. - useEffect(() => { - if (defaultFilters?.includes?.length) { - setMultiFilters('include', 'pools', defaultFilters?.includes, false); - } - if (defaultFilters?.excludes?.length) { - setMultiFilters('exclude', 'pools', defaultFilters?.excludes, false); - } - }, []); + // Reset list on network change or active era change. + useEffectIgnoreInitial(() => { + resetPoolList(); + }, [network, activeEra.index.toString()]); return ( @@ -213,7 +190,6 @@ export const PoolList = ({ excludes: [], }, ]} - activeIndex={1} />
diff --git a/src/library/PoolList/types.ts b/src/library/PoolList/types.ts index e3e819078d..1621c5e1f3 100644 --- a/src/library/PoolList/types.ts +++ b/src/library/PoolList/types.ts @@ -13,12 +13,7 @@ export interface PoolListProps { allowMoreCols?: boolean; allowSearch?: boolean; pagination?: boolean; - disableThrottle?: boolean; refetchOnListUpdate?: string; allowListFormat?: boolean; pools?: BondedPool[]; - defaultFilters?: { - includes: string[] | null; - excludes: string[] | null; - }; } diff --git a/src/library/ValidatorList/ValidatorItem/Utils.tsx b/src/library/ValidatorList/ValidatorItem/Utils.tsx index 1ba2a553a5..47239d7663 100644 --- a/src/library/ValidatorList/ValidatorItem/Utils.tsx +++ b/src/library/ValidatorList/ValidatorItem/Utils.tsx @@ -72,7 +72,7 @@ export const normaliseEraPoints = ( return Object.fromEntries( Object.entries(eraPoints).map(([era, points]) => [ era, - points.dividedBy(percentile).multipliedBy(0.01).toNumber(), + Math.min(points.dividedBy(percentile).multipliedBy(0.01).toNumber(), 1), ]) ); }; diff --git a/src/library/ValidatorList/index.tsx b/src/library/ValidatorList/index.tsx index 807ea03a2d..298c06b656 100644 --- a/src/library/ValidatorList/index.tsx +++ b/src/library/ValidatorList/index.tsx @@ -35,7 +35,7 @@ import { FilterHeaders } from './Filters/FilterHeaders'; import { FilterBadges } from './Filters/FilterBadges'; import type { NominationStatus } from './ValidatorItem/types'; import { useSyncing } from 'hooks/useSyncing'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; +import { validatorsPerPage } from 'library/List/defaults'; export const ValidatorListInner = ({ // Default list values. @@ -57,9 +57,8 @@ export const ValidatorListInner = ({ allowListFormat = true, defaultOrder = undefined, defaultFilters = undefined, - // Throttling and re-fetching. + // Re-fetching. alwaysRefetchValidators = false, - disableThrottle = false, }: ValidatorListProps) => { const { t } = useTranslation('library'); const { @@ -163,26 +162,10 @@ export const ValidatorListInner = ({ // Store whether the search bar is being used. const [isSearching, setIsSearching] = useState(false); - // Current render iteration. - const [renderIteration, setRenderIterationState] = useState(1); - - // Render throttle iteration. - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; - // Pagination. - const totalPages = Math.ceil(validators.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // Render batch. - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(validators.length / validatorsPerPage); + const pageEnd = page * validatorsPerPage - 1; + const pageStart = pageEnd - (validatorsPerPage - 1); // handle filter / order update const handleValidatorsFilterUpdate = ( @@ -198,14 +181,13 @@ export const ValidatorListInner = ({ } setValidators(filteredValidators); setPage(1); - setRenderIteration(1); } }; // get throttled subset or entire list - const listValidators = disableThrottle - ? validators - : validators.slice(pageStart).slice(0, listItemsPerPage); + const listValidators = validators + .slice(pageStart) + .slice(0, validatorsPerPage); // if in modal, handle resize const maybeHandleModalResize = () => { @@ -233,7 +215,6 @@ export const ValidatorListInner = ({ setValidators(filteredValidators); setPage(1); setIsSearching(e.currentTarget.value !== ''); - setRenderIteration(1); setSearchTerm('validators', newValue); }; @@ -300,15 +281,6 @@ export const ValidatorListInner = ({ } }, [isReady, activeEra.index, syncing, fetched]); - // Control render throttle. - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 50); - } - }, [renderIterationRef.current]); - // Trigger `onSelected` when selection changes. useEffect(() => { if (onSelected) { @@ -326,7 +298,7 @@ export const ValidatorListInner = ({ // Handle modal resize on list format change. useEffect(() => { maybeHandleModalResize(); - }, [listFormat, renderIteration, validators, page]); + }, [listFormat, validators, page]); return ( diff --git a/src/library/ValidatorList/types.ts b/src/library/ValidatorList/types.ts index f18d552e7f..ddd4ad01c3 100644 --- a/src/library/ValidatorList/types.ts +++ b/src/library/ValidatorList/types.ts @@ -31,7 +31,6 @@ export interface ValidatorListProps { alwaysRefetchValidators?: boolean; defaultFilters?: AnyJson; defaultOrder?: string; - disableThrottle?: boolean; selectActive?: boolean; selectToggleable?: boolean; refetchOnListUpdate?: boolean; diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json index 3802a7e018..a1aab456a0 100644 --- a/src/locale/cn/library.json +++ b/src/locale/cn/library.json @@ -165,6 +165,7 @@ "proxy": "代理账户", "randomValidator": "随机验证人", "reGenerate": "重新生成", + "readyToJoinPool": "可加入提名池", "recentPerformance": "最近表现", "remove": "删除", "removeSelected": "移除选定项", @@ -182,7 +183,7 @@ "signing": "签署中", "submitTransaction": "准备提交交易", "syncing": "正在同步", - "syncingPoolData": "同步提名池数据中", + "syncingPoolData": "查找提名池中", "syncingPoolList": "同步提名池列表", "tooSmall": "质押金额太少", "top": "首", diff --git a/src/locale/cn/pages.json b/src/locale/cn/pages.json index b0934d3feb..8745641b97 100644 --- a/src/locale/cn/pages.json +++ b/src/locale/cn/pages.json @@ -137,7 +137,6 @@ "bondedFunds": "己质押金额", "bouncer": "守护人", "browseMembers": "浏览成员", - "browsePools": "浏览提名池", "cancel": "取消", "closePool": "可提取己解锁金额并关闭池", "compound": "复利", diff --git a/src/locale/en/library.json b/src/locale/en/library.json index 084462da4d..29d1faaa69 100644 --- a/src/locale/en/library.json +++ b/src/locale/en/library.json @@ -168,6 +168,7 @@ "proxy": "Proxy", "randomValidator": "Random Validator", "reGenerate": "Re-Generate", + "readyToJoinPool": "Ready to Join Pool", "recentPerformance": "Recent Performance", "remove": "Remove", "removeSelected": "Remove Selected", @@ -185,7 +186,7 @@ "signing": "Signing", "submitTransaction": "Ready to submit transaction.", "syncing": "Syncing", - "syncingPoolData": "Syncing Pool Data", + "syncingPoolData": "Finding Pools to Join", "syncingPoolList": "Syncing Pool list", "tooSmall": "Bond amount is too small.", "top": "Top", diff --git a/src/locale/en/pages.json b/src/locale/en/pages.json index b833194a2b..eae84374ad 100644 --- a/src/locale/en/pages.json +++ b/src/locale/en/pages.json @@ -139,7 +139,6 @@ "bondedFunds": "Bonded Funds", "bouncer": "Bouncer", "browseMembers": "Browse Members", - "browsePools": "Browse Pools", "cancel": "Cancel", "closePool": "You can now withdraw and close the pool.", "compound": "Compound", diff --git a/src/pages/Payouts/PayoutList/index.tsx b/src/pages/Payouts/PayoutList/index.tsx index 6e44c0c0f1..331792b21c 100644 --- a/src/pages/Payouts/PayoutList/index.tsx +++ b/src/pages/Payouts/PayoutList/index.tsx @@ -7,7 +7,7 @@ import { ellipsisFn, isNotZero, planckToUnit } from '@w3ux/utils'; import BigNumber from 'bignumber.js'; import { formatDistance, fromUnixTime } from 'date-fns'; import { motion } from 'framer-motion'; -import { Component, useEffect, useRef, useState } from 'react'; +import { Component, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useBondedPools } from 'contexts/Pools/BondedPools'; @@ -25,14 +25,13 @@ import { useNetwork } from 'contexts/Network'; import { ItemWrapper } from '../Wrappers'; import type { PayoutListProps } from '../types'; import { PayoutListProvider, usePayoutList } from './context'; -import { listItemsPerPage, listItemsPerBatch } from 'library/List/defaults'; +import { payoutsPerPage } from 'library/List/defaults'; export const PayoutListInner = ({ allowMoreCols, pagination, title, payouts: initialPayouts, - disableThrottle = false, }: PayoutListProps) => { const { i18n, t } = useTranslation('pages'); const { mode } = useTheme(); @@ -47,32 +46,16 @@ export const PayoutListInner = ({ // current page const [page, setPage] = useState(1); - // current render iteration - const [renderIteration, _setRenderIteration] = useState(1); - // manipulated list (ordering, filtering) of payouts const [payouts, setPayouts] = useState(initialPayouts); // is this the initial fetch const [fetched, setFetched] = useState(false); - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - _setRenderIteration(iter); - }; - // pagination - const totalPages = Math.ceil(payouts.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(payouts.length / payoutsPerPage); + const pageEnd = page * payoutsPerPage - 1; + const pageStart = pageEnd - (payoutsPerPage - 1); // refetch list when list changes useEffect(() => { @@ -87,24 +70,11 @@ export const PayoutListInner = ({ } }, [isReady, fetched, activeEra.index]); - // render throttle - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - // get list items to render let listPayouts = []; // get throttled subset or entire list - if (!disableThrottle) { - listPayouts = payouts.slice(pageStart).slice(0, listItemsPerPage); - } else { - listPayouts = payouts; - } + listPayouts = payouts.slice(pageStart).slice(0, payoutsPerPage); if (!payouts.length) { return null; diff --git a/src/pages/Payouts/types.ts b/src/pages/Payouts/types.ts index b9ff5d9650..745245ce51 100644 --- a/src/pages/Payouts/types.ts +++ b/src/pages/Payouts/types.ts @@ -6,7 +6,6 @@ import type { AnySubscan } from 'types'; export interface PayoutListProps { allowMoreCols?: boolean; pagination?: boolean; - disableThrottle?: boolean; title?: string | null; payoutsList?: AnySubscan; payouts?: AnySubscan; diff --git a/src/pages/Pools/Home/Favorites/index.tsx b/src/pages/Pools/Home/Favorites/index.tsx index 6f98398d59..e2603f5868 100644 --- a/src/pages/Pools/Home/Favorites/index.tsx +++ b/src/pages/Pools/Home/Favorites/index.tsx @@ -8,7 +8,7 @@ import { useApi } from 'contexts/Api'; import { useBondedPools } from 'contexts/Pools/BondedPools'; import { useFavoritePools } from 'contexts/Pools/FavoritePools'; import { CardWrapper } from 'library/Card/Wrappers'; -import { PoolList } from 'library/PoolList/Default'; +import { PoolList } from 'library/PoolList'; import { ListStatusHeader } from 'library/List'; import { PoolListProvider } from 'library/PoolList/context'; import type { BondedPool } from 'contexts/Pools/BondedPools/types'; diff --git a/src/pages/Pools/Home/Status/FindingPoolPercent.tsx b/src/pages/Pools/Home/Status/FindingPoolPercent.tsx new file mode 100644 index 0000000000..668c087e28 --- /dev/null +++ b/src/pages/Pools/Home/Status/FindingPoolPercent.tsx @@ -0,0 +1,30 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; + +export const FindingPoolsPercent = () => { + const { getPoolPerformanceTask } = usePoolPerformance(); + + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); + + if (poolJoinPerformanceTask.status !== 'syncing') { + return null; + } + + // Calculate syncing status. + const { startEra, currentEra, endEra } = poolJoinPerformanceTask; + const totalEras = startEra.minus(endEra); + const erasPassed = startEra.minus(currentEra); + const percentPassed = erasPassed.isEqualTo(0) + ? new BigNumber(0) + : erasPassed.dividedBy(totalEras).multipliedBy(100); + + return ( + + {percentPassed.decimalPlaces(0).toFormat()}% + + ); +}; diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx index 042cb55368..480efffe1c 100644 --- a/src/pages/Pools/Home/Status/NewMember.tsx +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -3,28 +3,38 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CallToActionWrapper } from '../../../../library/CallToAction'; -import { faChevronRight, faUserGroup } from '@fortawesome/free-solid-svg-icons'; +import { + faChevronRight, + faUserGroup, + faUserPlus, +} from '@fortawesome/free-solid-svg-icons'; import { useSetup } from 'contexts/Setup'; -import { usePoolsTabs } from '../context'; import { useStatusButtons } from './useStatusButtons'; import { useTranslation } from 'react-i18next'; import { useOverlay } from 'kits/Overlay/Provider'; import type { NewMemberProps } from './types'; import { CallToActionLoader } from 'library/Loader/CallToAction'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { FindingPoolsPercent } from './FindingPoolPercent'; +import { useJoinPools } from 'contexts/Pools/JoinPools'; export const NewMember = ({ syncing }: NewMemberProps) => { const { t } = useTranslation(); const { setOnPoolSetup } = useSetup(); - const { setActiveTab } = usePoolsTabs(); + const { poolsForJoin } = useJoinPools(); const { openCanvas } = useOverlay().canvas; - const { poolRewardPointsFetched } = usePoolPerformance(); - const { disableJoin, disableCreate } = useStatusButtons(); + const { startJoinPoolFetch } = useJoinPools(); + const { getPoolPerformanceTask } = usePoolPerformance(); + const { getJoinDisabled, getCreateDisabled } = useStatusButtons(); - const joinButtonDisabled = - disableJoin() || poolRewardPointsFetched !== 'synced'; + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); - const createButtonDisabled = disableCreate(); + // Alias for create button disabled state. + const createDisabled = getCreateDisabled(); + + // Disable opening the canvas if data is not ready. + const joinButtonDisabled = getJoinDisabled() || !poolsForJoin.length; return ( @@ -33,39 +43,51 @@ export const NewMember = ({ syncing }: NewMemberProps) => { ) : ( <> -
+
-
-
-
@@ -73,11 +95,11 @@ export const NewMember = ({ syncing }: NewMemberProps) => {