diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..370a32d520 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## [1.3.0](https://github.com/paritytech/polkadot-staking-dashboard/compare/v1.2.1...v1.3.0) (2024-04-08) + + +### Features + +* Create pool canvas ([#2061](https://github.com/paritytech/polkadot-staking-dashboard/issues/2061)) ([de3ad50](https://github.com/paritytech/polkadot-staking-dashboard/commit/de3ad50ed2eda49a0378a26c22fb8a48fdc9e305)) +* Join pool progress bar on performance fetch. ([#2064](https://github.com/paritytech/polkadot-staking-dashboard/issues/2064)) ([e5027ff](https://github.com/paritytech/polkadot-staking-dashboard/commit/e5027fffc3151dbdf0c4b7cce09f37aaeb184971)) +* Open `JoinPool` canvas immediately, preloader, prioritise low member pools. ([#2059](https://github.com/paritytech/polkadot-staking-dashboard/issues/2059)) ([5360eaa](https://github.com/paritytech/polkadot-staking-dashboard/commit/5360eaa17ef08b6b602d21967d9174f2eed9cf83)) +* Pool performance data to batches. Per-page fetching, pool join subset. ([#2057](https://github.com/paritytech/polkadot-staking-dashboard/issues/2057)) ([965d3e1](https://github.com/paritytech/polkadot-staking-dashboard/commit/965d3e182c77e0b6d46c2d1c603e74a30cd7be92)) +* **refactor:** Persist all imported accounts active pools. ([#2066](https://github.com/paritytech/polkadot-staking-dashboard/issues/2066)) ([1a1847d](https://github.com/paritytech/polkadot-staking-dashboard/commit/1a1847deb0d4763b893335293c85dbe8d3f330b1)) +* Simple pool join & call to action UI ([#2050](https://github.com/paritytech/polkadot-staking-dashboard/issues/2050)) ([6d04429](https://github.com/paritytech/polkadot-staking-dashboard/commit/6d0442947b4322ec949bbb88e82b24720dce4143)) +* Start nominating canvas ([#2062](https://github.com/paritytech/polkadot-staking-dashboard/issues/2062)) ([0208d5f](https://github.com/paritytech/polkadot-staking-dashboard/commit/0208d5fc5658bc375eeef3aa853954c05290796f)) +* use `bondedPool.memberCounter`, deprecate `nomination_pool/pool` Subscan call ([#2054](https://github.com/paritytech/polkadot-staking-dashboard/issues/2054)) ([b536faf](https://github.com/paritytech/polkadot-staking-dashboard/commit/b536faf8fc410c8291dea84fa2b96189ab2c8e76)) +* **ux:** `JoinPool`: Inline sync for provided pool ([#2067](https://github.com/paritytech/polkadot-staking-dashboard/issues/2067)) ([e146bfc](https://github.com/paritytech/polkadot-staking-dashboard/commit/e146bfcb15df96cd0a10fe1d268e3eab343ef1d1)) +* **ux:** Disconnect from extension ([#2069](https://github.com/paritytech/polkadot-staking-dashboard/issues/2069)) ([c5c2ecb](https://github.com/paritytech/polkadot-staking-dashboard/commit/c5c2ecb54d31b59cc4db3bdb20b55e48cc01160a)) +* **ux:** Improve Join Pool preloader ([#2063](https://github.com/paritytech/polkadot-staking-dashboard/issues/2063)) ([69d716e](https://github.com/paritytech/polkadot-staking-dashboard/commit/69d716e2e99a6f32e45407362d951352fd6a884f)) +* **ux:** Pool display polishes, pre-release fixes ([#2065](https://github.com/paritytech/polkadot-staking-dashboard/issues/2065)) ([89e5f98](https://github.com/paritytech/polkadot-staking-dashboard/commit/89e5f98dd146d4838b9580a857eddfa73090762f)) +* **ux:** use secondary accent color for status UI ([#2055](https://github.com/paritytech/polkadot-staking-dashboard/issues/2055)) ([bf16d80](https://github.com/paritytech/polkadot-staking-dashboard/commit/bf16d80a661ca1d1cd0cf038bcff4525fbff19c8)) + + +### Bug Fixes + +* Search bar initial value on Pools/Validators page ([#2032](https://github.com/paritytech/polkadot-staking-dashboard/issues/2032)) ([c4749c6](https://github.com/paritytech/polkadot-staking-dashboard/commit/c4749c6e7ca338a9f3fd3299ebb53bbf45c3de07)) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2f06502319..be1c38563d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [1.3.0](https://github.com/paritytech/polkadot-staking-dashboard/compare/v1.2.1...v1.3.0) (2024-04-08) + + +### Features + +* Create pool canvas ([#2061](https://github.com/paritytech/polkadot-staking-dashboard/issues/2061)) ([de3ad50](https://github.com/paritytech/polkadot-staking-dashboard/commit/de3ad50ed2eda49a0378a26c22fb8a48fdc9e305)) +* Join pool progress bar on performance fetch. ([#2064](https://github.com/paritytech/polkadot-staking-dashboard/issues/2064)) ([e5027ff](https://github.com/paritytech/polkadot-staking-dashboard/commit/e5027fffc3151dbdf0c4b7cce09f37aaeb184971)) +* Open `JoinPool` canvas immediately, preloader, prioritise low member pools. ([#2059](https://github.com/paritytech/polkadot-staking-dashboard/issues/2059)) ([5360eaa](https://github.com/paritytech/polkadot-staking-dashboard/commit/5360eaa17ef08b6b602d21967d9174f2eed9cf83)) +* Pool performance data to batches. Per-page fetching, pool join subset. ([#2057](https://github.com/paritytech/polkadot-staking-dashboard/issues/2057)) ([965d3e1](https://github.com/paritytech/polkadot-staking-dashboard/commit/965d3e182c77e0b6d46c2d1c603e74a30cd7be92)) +* **refactor:** Persist all imported accounts active pools. ([#2066](https://github.com/paritytech/polkadot-staking-dashboard/issues/2066)) ([1a1847d](https://github.com/paritytech/polkadot-staking-dashboard/commit/1a1847deb0d4763b893335293c85dbe8d3f330b1)) +* Simple pool join & call to action UI ([#2050](https://github.com/paritytech/polkadot-staking-dashboard/issues/2050)) ([6d04429](https://github.com/paritytech/polkadot-staking-dashboard/commit/6d0442947b4322ec949bbb88e82b24720dce4143)) +* Start nominating canvas ([#2062](https://github.com/paritytech/polkadot-staking-dashboard/issues/2062)) ([0208d5f](https://github.com/paritytech/polkadot-staking-dashboard/commit/0208d5fc5658bc375eeef3aa853954c05290796f)) +* use `bondedPool.memberCounter`, deprecate `nomination_pool/pool` Subscan call ([#2054](https://github.com/paritytech/polkadot-staking-dashboard/issues/2054)) ([b536faf](https://github.com/paritytech/polkadot-staking-dashboard/commit/b536faf8fc410c8291dea84fa2b96189ab2c8e76)) +* **ux:** `JoinPool`: Inline sync for provided pool ([#2067](https://github.com/paritytech/polkadot-staking-dashboard/issues/2067)) ([e146bfc](https://github.com/paritytech/polkadot-staking-dashboard/commit/e146bfcb15df96cd0a10fe1d268e3eab343ef1d1)) +* **ux:** Disconnect from extension ([#2069](https://github.com/paritytech/polkadot-staking-dashboard/issues/2069)) ([c5c2ecb](https://github.com/paritytech/polkadot-staking-dashboard/commit/c5c2ecb54d31b59cc4db3bdb20b55e48cc01160a)) +* **ux:** Improve Join Pool preloader ([#2063](https://github.com/paritytech/polkadot-staking-dashboard/issues/2063)) ([69d716e](https://github.com/paritytech/polkadot-staking-dashboard/commit/69d716e2e99a6f32e45407362d951352fd6a884f)) +* **ux:** Pool display polishes, pre-release fixes ([#2065](https://github.com/paritytech/polkadot-staking-dashboard/issues/2065)) ([89e5f98](https://github.com/paritytech/polkadot-staking-dashboard/commit/89e5f98dd146d4838b9580a857eddfa73090762f)) +* **ux:** use secondary accent color for status UI ([#2055](https://github.com/paritytech/polkadot-staking-dashboard/issues/2055)) ([bf16d80](https://github.com/paritytech/polkadot-staking-dashboard/commit/bf16d80a661ca1d1cd0cf038bcff4525fbff19c8)) + + +### Bug Fixes + +* Search bar initial value on Pools/Validators page ([#2032](https://github.com/paritytech/polkadot-staking-dashboard/issues/2032)) ([c4749c6](https://github.com/paritytech/polkadot-staking-dashboard/commit/c4749c6e7ca338a9f3fd3299ebb53bbf45c3de07)) +* Fixes an issue where Polkagate Snap enablement would also enable other extensions . + ## [1.2.1](https://github.com/paritytech/polkadot-staking-dashboard/compare/v1.2.0...v1.2.1) (2024-03-14) diff --git a/package.json b/package.json index 97d662595d..fab60551bc 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "visualizer": "vite-bundle-visualizer" }, "dependencies": { - "@dotlottie/player-component": "^2.7.10", + "@dotlottie/player-component": "^2.7.12", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", @@ -34,24 +34,24 @@ "@polkadot/util-crypto": "^12.6.2", "@polkawatch/ddp-client": "^2.0.11", "@substrate/connect": "0.7.35", - "@w3ux/extension-assets": "^0.2.3", + "@w3ux/extension-assets": "0.2.6", "@w3ux/hooks": "^0.0.3", - "@w3ux/react-connect-kit": "^0.1.2", + "@w3ux/react-connect-kit": "0.1.8", "@w3ux/react-odometer": "^0.0.3", "@w3ux/react-polkicon": "^0.0.2", "@w3ux/utils": "^0.0.2", "@w3ux/validator-assets": "^0.0.4", - "@zondax/ledger-substrate": "^0.41.3", + "@zondax/ledger-substrate": "^0.41.4", "bignumber.js": "^9.1.2", "bn.js": "^5.2.1", "buffer": "^6.0.3", "chart.js": "^4.4.2", "chroma-js": "^2.4.2", "date-fns": "^3.3.1", - "framer-motion": "^11.0.18", + "framer-motion": "^11.0.24", "html5-qrcode": "^2.3.8", "i18next": "^23.10.0", - "i18next-browser-languagedetector": "^7.2.0", + "i18next-browser-languagedetector": "^7.2.1", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "qrcode-generator": "1.4.4", @@ -65,20 +65,20 @@ "react-router-dom": "^6.22.3", "react-scroll": "^1.9.0", "styled-components": "^6.1.8", - "usehooks-ts": "^3.0.1" + "usehooks-ts": "^3.0.2" }, "devDependencies": { "@ledgerhq/logs": "^6.12.0", "@types/chroma-js": "^2.4.4", "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.9", - "@types/react": "^18.2.67", - "@types/react-dom": "^18.2.22", + "@types/react": "^18.2.73", + "@types/react-dom": "^18.2.23", "@types/react-helmet": "^6.1.11", "@types/react-scroll": "^1.8.10", "@types/styled-components": "^5.1.34", - "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", + "@typescript-eslint/eslint-plugin": "^7.5.0", + "@typescript-eslint/parser": "^7.4.0", "@vitejs/plugin-react-swc": "^3.6.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", @@ -94,8 +94,8 @@ "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", "sass": "^1.72.0", - "typescript": "^5.3.3", - "vite": "^5.2.2", + "typescript": "^5.4.3", + "vite": "^5.2.7", "vite-bundle-visualizer": "^1.1.0", "vite-plugin-checker": "^0.6.4", "vite-plugin-eslint": "^1.8.1", diff --git a/src/Providers.tsx b/src/Providers.tsx index da2616a71f..bb4a54728f 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -45,6 +45,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 { @@ -59,7 +60,10 @@ export const Providers = () => { [APIProvider, { network }], VaultAccountsProvider, LedgerHardwareProvider, - ExtensionsProvider, + [ + ExtensionsProvider, + { options: { chainSafeSnapEnabled: true, polkagateSnapEnabled: true } }, + ], [ ExtensionAccountsProvider, { @@ -96,6 +100,7 @@ export const Providers = () => { FastUnstakeProvider, PayoutsProvider, PoolPerformanceProvider, + JoinPoolsProvider, SetupProvider, MenuProvider, TooltipProvider, diff --git a/src/pages/Pools/Create/Bond/index.tsx b/src/canvas/CreatePool/Bond/index.tsx similarity index 100% rename from src/pages/Pools/Create/Bond/index.tsx rename to src/canvas/CreatePool/Bond/index.tsx diff --git a/src/pages/Pools/Create/PoolName/Input.tsx b/src/canvas/CreatePool/PoolName/Input.tsx similarity index 100% rename from src/pages/Pools/Create/PoolName/Input.tsx rename to src/canvas/CreatePool/PoolName/Input.tsx diff --git a/src/pages/Pools/Create/PoolName/index.tsx b/src/canvas/CreatePool/PoolName/index.tsx similarity index 100% rename from src/pages/Pools/Create/PoolName/index.tsx rename to src/canvas/CreatePool/PoolName/index.tsx diff --git a/src/pages/Pools/Create/PoolRoles/index.tsx b/src/canvas/CreatePool/PoolRoles/index.tsx similarity index 98% rename from src/pages/Pools/Create/PoolRoles/index.tsx rename to src/canvas/CreatePool/PoolRoles/index.tsx index 60450fc52b..65fbf2a644 100644 --- a/src/pages/Pools/Create/PoolRoles/index.tsx +++ b/src/canvas/CreatePool/PoolRoles/index.tsx @@ -9,7 +9,7 @@ import { Header } from 'library/SetupSteps/Header'; import { MotionContainer } from 'library/SetupSteps/MotionContainer'; import type { SetupStepProps } from 'library/SetupSteps/types'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { Roles } from '../../Roles'; +import { Roles } from 'pages/Pools/Roles'; import type { PoolProgress } from 'contexts/Setup/types'; import type { PoolRoles as PoolRolesInterface } from 'contexts/Pools/ActivePool/types'; diff --git a/src/pages/Pools/Create/Summary/Wrapper.ts b/src/canvas/CreatePool/Summary/Wrapper.ts similarity index 100% rename from src/pages/Pools/Create/Summary/Wrapper.ts rename to src/canvas/CreatePool/Summary/Wrapper.ts diff --git a/src/pages/Pools/Create/Summary/index.tsx b/src/canvas/CreatePool/Summary/index.tsx similarity index 94% rename from src/pages/Pools/Create/Summary/index.tsx rename to src/canvas/CreatePool/Summary/index.tsx index 8e03d57c5b..c61f19db7e 100644 --- a/src/pages/Pools/Create/Summary/index.tsx +++ b/src/canvas/CreatePool/Summary/index.tsx @@ -21,6 +21,7 @@ import { useApi } from 'contexts/Api'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { SummaryWrapper } from './Wrapper'; +import { useOverlay } from 'kits/Overlay/Provider'; export const Summary = ({ section }: SetupStepProps) => { const { t } = useTranslation('pages'); @@ -33,11 +34,12 @@ export const Summary = ({ section }: SetupStepProps) => { networkData: { units, unit }, } = useNetwork(); const { newBatchCall } = useBatchCall(); + const { closeCanvas } = useOverlay().canvas; const { accountHasSigner } = useImportedAccounts(); const { getPoolSetup, removeSetupProgress } = useSetup(); + const { activeAccount, activeProxy } = useActiveAccounts(); const { queryPoolMember, addToPoolMembers } = usePoolMembers(); const { queryBondedPool, addToBondedPools } = useBondedPools(); - const { activeAccount, activeProxy } = useActiveAccounts(); const poolId = lastPoolId.plus(1); const setup = getPoolSetup(activeAccount); @@ -75,17 +77,20 @@ export const Summary = ({ section }: SetupStepProps) => { from: activeAccount, shouldSubmit: true, callbackInBlock: async () => { - // query and add created pool to bondedPools list + // Close canvas. + closeCanvas(); + + // Query and add created pool to bondedPools list. const pool = await queryBondedPool(poolId.toNumber()); addToBondedPools(pool); - // query and add account to poolMembers list + // Query and add account to poolMembers list. const member = await queryPoolMember(activeAccount); if (member) { addToPoolMembers(member); } - // reset localStorage setup progress + // Reset setup progress. removeSetupProgress('pool', activeAccount); }, }); diff --git a/src/canvas/CreatePool/index.tsx b/src/canvas/CreatePool/index.tsx new file mode 100644 index 0000000000..048dd6db2f --- /dev/null +++ b/src/canvas/CreatePool/index.tsx @@ -0,0 +1,70 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { Element } from 'react-scroll'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { Nominate } from 'library/SetupSteps/Nominate'; +import { Summary } from 'canvas/CreatePool/Summary'; +import { Bond } from 'canvas/CreatePool/Bond'; +import { PoolName } from 'canvas/CreatePool/PoolName'; +import { PoolRoles } from 'canvas/CreatePool/PoolRoles'; +import { CanvasFullScreenWrapper, CanvasTitleWrapper } from 'canvas/Wrappers'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { useOverlay } from 'kits/Overlay/Provider'; + +export const CreatePool = () => { + const { t } = useTranslation(); + const { closeCanvas } = useOverlay().canvas; + + return ( + +
+ closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ + +
+
+
+
+

{t('pools.createAPool', { ns: 'pages' })}

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/canvas/JoinPool/Header.tsx b/src/canvas/JoinPool/Header.tsx new file mode 100644 index 0000000000..2af9044820 --- /dev/null +++ b/src/canvas/JoinPool/Header.tsx @@ -0,0 +1,123 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faArrowsRotate, + faHashtag, + faTimes, +} from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { ButtonPrimaryInvert } from 'kits/Buttons/ButtonPrimaryInvert'; +import { Polkicon } from '@w3ux/react-polkicon'; +import { determinePoolDisplay, remToUnit } from '@w3ux/utils'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { PageTitleTabs } from 'kits/Structure/PageTitleTabs'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from 'kits/Overlay/Provider'; +import type { JoinPoolHeaderProps } from './types'; +import { CanvasTitleWrapper } from 'canvas/Wrappers'; + +export const Header = ({ + activeTab, + bondedPool, + filteredBondedPools, + metadata, + autoSelected, + setActiveTab, + setSelectedPoolId, + providedPoolId, +}: JoinPoolHeaderProps) => { + const { t } = useTranslation(); + const { closeCanvas } = useOverlay().canvas; + + // Randomly select a new pool to display. + const handleChooseNewPool = () => { + // 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() * filteredPools.length - 1); + setSelectedPoolId(filteredPools[index].id); + }; + + return ( + <> +
+ {providedPoolId === null && ( + handleChooseNewPool()} + lg + /> + )} + closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ +
+
+ +
+
+
+

+ {determinePoolDisplay( + bondedPool?.addresses.stash || '', + metadata + )} +

+
+
+

+ {t('pool', { ns: 'library' })}{' '} + + {bondedPool.id} + {['Blocked', 'Destroying'].includes(bondedPool.state) && ( + + {t(bondedPool.state.toLowerCase(), { ns: 'library' })} + + )} +

+ + {autoSelected && ( +

+ {t('autoSelected', { ns: 'library' })} +

+ )} +
+
+
+ + setActiveTab(0), + }, + { + title: t('nominate.nominations', { ns: 'pages' }), + active: activeTab === 1, + onClick: () => setActiveTab(1), + }, + ]} + tabClassName="canvas" + inline={true} + /> +
+ + ); +}; diff --git a/src/canvas/JoinPool/Nominations/index.tsx b/src/canvas/JoinPool/Nominations/index.tsx new file mode 100644 index 0000000000..ea72e17269 --- /dev/null +++ b/src/canvas/JoinPool/Nominations/index.tsx @@ -0,0 +1,47 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ValidatorList } from 'library/ValidatorList'; +import { useTranslation } from 'react-i18next'; +import { HeadingWrapper, NominationsWrapper } from '../Wrappers'; +import type { NominationsProps } from '../types'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; + +export const Nominations = ({ stash, poolId }: NominationsProps) => { + const { t } = useTranslation(); + const { validators } = useValidators(); + const { poolsNominations } = useBondedPools(); + + // Extract validator entries from pool targets. + const targets = poolsNominations[poolId]?.targets || []; + const filteredTargets = validators.filter(({ address }) => + targets.includes(address) + ); + + return ( + + +

+ {!targets.length + ? t('nominate.noNominationsSet', { ns: 'pages' }) + : `${targets.length} ${t('nominations', { ns: 'library', count: targets.length })}`} +

+
+ + {targets.length > 0 && ( + + )} +
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/AddressSection.tsx b/src/canvas/JoinPool/Overview/AddressSection.tsx new file mode 100644 index 0000000000..ac569d23b1 --- /dev/null +++ b/src/canvas/JoinPool/Overview/AddressSection.tsx @@ -0,0 +1,45 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useHelp } from 'contexts/Help'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { HeadingWrapper } from '../Wrappers'; +import { Polkicon } from '@w3ux/react-polkicon'; +import { CopyAddress } from 'library/ListItem/Labels/CopyAddress'; +import { ellipsisFn, remToUnit } from '@w3ux/utils'; +import type { AddressSectionProps } from '../types'; + +export const AddressSection = ({ + address, + label, + helpKey, +}: AddressSectionProps) => { + const { openHelp } = useHelp(); + + return ( +
+ +

+ {label} + {!!helpKey && ( + openHelp(helpKey)} /> + )} +

+
+ +
+ + + +

+ {ellipsisFn(address, 6)} + +

+
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Addresses.tsx b/src/canvas/JoinPool/Overview/Addresses.tsx new file mode 100644 index 0000000000..8a8cc93a27 --- /dev/null +++ b/src/canvas/JoinPool/Overview/Addresses.tsx @@ -0,0 +1,27 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CardWrapper } from 'library/Card/Wrappers'; +import { AddressesWrapper, HeadingWrapper } from '../Wrappers'; +import { AddressSection } from './AddressSection'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Addresses = ({ + bondedPool: { addresses }, +}: OverviewSectionProps) => { + const { t } = useTranslation('library'); + + return ( + + +

{t('addresses')}

+
+ + + + + +
+ ); +}; diff --git a/src/modals/JoinPool/index.tsx b/src/canvas/JoinPool/Overview/JoinForm.tsx similarity index 52% rename from src/modals/JoinPool/index.tsx rename to src/canvas/JoinPool/Overview/JoinForm.tsx index c53268a453..82fdb79f85 100644 --- a/src/modals/JoinPool/index.tsx +++ b/src/canvas/JoinPool/Overview/JoinForm.tsx @@ -2,99 +2,88 @@ // SPDX-License-Identifier: GPL-3.0-only import { planckToUnit, unitToPlanck } from '@w3ux/utils'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useApi } from 'contexts/Api'; -import { usePoolMembers } from 'contexts/Pools/PoolMembers'; -import { useSetup } from 'contexts/Setup'; -import { defaultPoolProgress } from 'contexts/Setup/defaults'; +import type BigNumber from 'bignumber.js'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNetwork } from 'contexts/Network'; +import type { ClaimPermission } from 'contexts/Pools/types'; import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxMeta } from 'contexts/TxMeta'; -import { BondFeedback } from 'library/Form/Bond/BondFeedback'; +import { useState } from 'react'; +import { JoinFormWrapper } from '../Wrappers'; import { ClaimPermissionInput } from 'library/Form/ClaimPermissionInput'; -import { useBatchCall } from 'hooks/useBatchCall'; +import { BondFeedback } from 'library/Form/Bond/BondFeedback'; import { useBondGreatestFee } from 'hooks/useBondGreatestFee'; -import { useSignerWarnings } from 'hooks/useSignerWarnings'; +import { useApi } from 'contexts/Api'; +import { useBatchCall } from 'hooks/useBatchCall'; import { useSubmitExtrinsic } from 'hooks/useSubmitExtrinsic'; -import { Close } from 'library/Modal/Close'; -import { SubmitTx } from 'library/SubmitTx'; import { useOverlay } from 'kits/Overlay/Provider'; -import { useNetwork } from 'contexts/Network'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import type { ClaimPermission } from 'contexts/Pools/types'; -import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; +import { useSetup } from 'contexts/Setup'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { defaultPoolProgress } from 'contexts/Setup/defaults'; +import { useSignerWarnings } from 'hooks/useSignerWarnings'; +import { SubmitTx } from 'library/SubmitTx'; +import type { OverviewSectionProps } from '../types'; +import { defaultClaimPermission } from 'controllers/ActivePoolsController/defaults'; +import { useTranslation } from 'react-i18next'; -export const JoinPool = () => { - const { t } = useTranslation('modals'); +export const JoinForm = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation(); const { api } = useApi(); const { - networkData: { units }, + networkData: { units, unit }, } = useNetwork(); - const { activeAccount } = useActiveAccounts(); + const { + closeCanvas, + config: { options }, + } = useOverlay().canvas; const { newBatchCall } = useBatchCall(); const { setActiveAccountSetup } = useSetup(); - const { txFees, notEnoughFunds } = useTxMeta(); + const { activeAccount } = useActiveAccounts(); const { getSignerWarnings } = useSignerWarnings(); const { getTransferOptions } = useTransferOptions(); + const largestTxFee = useBondGreatestFee({ bondFor: 'pool' }); const { queryPoolMember, addToPoolMembers } = usePoolMembers(); - const { - setModalStatus, - config: { options }, - setModalResize, - } = useOverlay().modal; - - const { id: poolId, setActiveTab } = options; const { pool: { totalPossibleBond }, - transferrableBalance, } = getTransferOptions(activeAccount); - const largestTxFee = useBondGreatestFee({ bondFor: 'pool' }); - - // if we are bonding, subtract tx fees from bond amount - const freeBondAmount = BigNumber.max(transferrableBalance.minus(txFees), 0); + // Pool claim permission value. + const [claimPermission, setClaimPermission] = useState( + defaultClaimPermission + ); - // local bond value + // Bond amount to join pool with. const [bond, setBond] = useState<{ bond: string }>({ bond: planckToUnit(totalPossibleBond, units).toString(), }); - // handler to set bond as a string - const handleSetBond = (newBond: { bond: BigNumber }) => { - setBond({ bond: newBond.bond.toString() }); - }; - - // Updated claim permission value - const [claimPermission, setClaimPermission] = useState< - ClaimPermission | undefined - >('Permissioned'); - - // bond valid + // Whether the bond amount is valid. const [bondValid, setBondValid] = useState(false); // feedback errors to trigger modal resize const [feedbackErrors, setFeedbackErrors] = useState([]); - // modal resize on form update - useEffect( - () => setModalResize(), - [bond, notEnoughFunds, feedbackErrors.length] - ); + // Handler to set bond on input change. + const handleSetBond = (value: { bond: BigNumber }) => { + setBond({ bond: value.bond.toString() }); + }; - // tx to submit + // Whether the form is ready to submit. + const formValid = bondValid && feedbackErrors.length === 0; + + // Get transaction for submission. const getTx = () => { const tx = null; - if (!api) { + if (!api || !claimPermission || !formValid) { return tx; } const bondToSubmit = unitToPlanck(!bondValid ? '0' : bond.bond, units); const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); - const txs = [api.tx.nominationPools.join(bondAsString, poolId)]; + const txs = [api.tx.nominationPools.join(bondAsString, bondedPool.id)]; - if (![undefined, 'Permissioned'].includes(claimPermission)) { + // If claim permission is not the default, add it to tx. + if (claimPermission !== defaultClaimPermission) { txs.push(api.tx.nominationPools.setClaimPermission(claimPermission)); } @@ -110,17 +99,23 @@ export const JoinPool = () => { from: activeAccount, shouldSubmit: bondValid, callbackSubmit: () => { - setModalStatus('closing'); - setActiveTab(0); + closeCanvas(); + + // Optional callback function on join success. + const onJoinCallback = options?.onJoinCallback; + + if (typeof onJoinCallback === 'function') { + onJoinCallback(); + } }, callbackInBlock: async () => { - // query and add account to poolMembers list + // Query and add account to poolMembers list const member = await queryPoolMember(activeAccount); if (member) { addToPoolMembers(member); } - // reset localStorage setup progress + // Reset local storage setup progress setActiveAccountSetup('pool', defaultPoolProgress); }, }); @@ -132,33 +127,49 @@ export const JoinPool = () => { ); return ( - <> - - -

{t('joinPool')}

- { - setBondValid(valid); - setFeedbackErrors(errors); - }} - defaultBond={null} - setters={[handleSetBond]} - parentErrors={warnings} - txFees={largestTxFee} - /> - { - setClaimPermission(val); - }} - disabled={freeBondAmount.isZero()} + +

{t('pools.joinPool', { ns: 'pages' })}

+

+ {t('bond', { ns: 'library' })} {unit} +

+ +
+
+ { + setBondValid(valid); + setFeedbackErrors(errors); + }} + defaultBond={null} + setters={[handleSetBond]} + parentErrors={warnings} + txFees={largestTxFee} + /> +
+
+ +

{t('claimSetting', { ns: 'library' })}

+ + { + setClaimPermission(val); + }} + /> + +
+ - - - +
+
); }; diff --git a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx new file mode 100644 index 0000000000..7d8c1126b2 --- /dev/null +++ b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx @@ -0,0 +1,193 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import { useNetwork } from 'contexts/Network'; +import { GraphWrapper, HeadingWrapper } from '../Wrappers'; +import { Line } from 'react-chartjs-2'; +import BigNumber from 'bignumber.js'; +import type { AnyJson } from 'types'; +import { graphColors } from 'theme/graphs'; +import { useTheme } from 'contexts/Themes'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { useHelp } from 'contexts/Help'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { useRef } from 'react'; +import { formatSize } from 'library/Graphs/Utils'; +import { useSize } from 'hooks/useSize'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + Legend +); + +export const PerformanceGraph = ({ + bondedPool, + performanceKey, + graphSyncing, +}: OverviewSectionProps) => { + const { t } = useTranslation(); + const { mode } = useTheme(); + const { openHelp } = useHelp(); + const { colors } = useNetwork().networkData; + const { getPoolRewardPoints } = usePoolPerformance(); + + const poolRewardPoints = getPoolRewardPoints(performanceKey); + const rawEraRewardPoints = poolRewardPoints[bondedPool.addresses.stash] || {}; + + // Ref to the graph container. + const graphInnerRef = useRef(null); + + // Get the size of the graph container. + const size = useSize(graphInnerRef?.current || undefined); + const { width, height } = formatSize(size, 150); + + // Format reward points as an array of strings, or an empty array if syncing. + const dataset = graphSyncing + ? [] + : Object.values( + Object.fromEntries( + Object.entries(rawEraRewardPoints).map(([k, v]: AnyJson) => [ + k, + new BigNumber(v).toString(), + ]) + ) + ); + + // Format labels, only displaying the first and last era. + const labels = Object.keys(rawEraRewardPoints).map(() => ''); + + const firstEra = Object.keys(rawEraRewardPoints)[0]; + labels[0] = firstEra + ? `${t('era', { ns: 'library' })} ${Object.keys(rawEraRewardPoints)[0]}` + : ''; + + const lastEra = Object.keys(rawEraRewardPoints)[labels.length - 1]; + labels[labels.length - 1] = lastEra + ? `${t('era', { ns: 'library' })} ${Object.keys(rawEraRewardPoints)[labels.length - 1]}` + : ''; + + // Use primary color for bars. + const color = colors.primary[mode]; + + const options = { + responsive: true, + maintainAspectRatio: false, + barPercentage: 0.3, + maxBarThickness: 13, + scales: { + x: { + stacked: true, + grid: { + display: false, + }, + ticks: { + font: { + size: 10, + }, + autoSkip: true, + }, + }, + y: { + stacked: true, + beginAtZero: true, + ticks: { + font: { + size: 10, + }, + }, + border: { + display: false, + }, + grid: { + color: graphColors.grid[mode], + }, + }, + }, + plugins: { + legend: { + display: false, + }, + title: { + display: false, + }, + tooltip: { + displayColors: false, + backgroundColor: graphColors.tooltip[mode], + titleColor: graphColors.label[mode], + bodyColor: graphColors.label[mode], + bodyFont: { + weight: 600, + }, + callbacks: { + title: () => [], + label: (context: AnyJson) => + `${new BigNumber(context.parsed.y).decimalPlaces(0).toFormat()} ${t('eraPoints', { ns: 'library' })}`, + }, + intersect: false, + interaction: { + mode: 'nearest', + }, + }, + }, + }; + + const data = { + labels, + datasets: [ + { + label: t('era', { ns: 'library' }), + data: dataset, + borderColor: color, + backgroundColor: color, + pointRadius: 0, + borderRadius: 3, + }, + ], + }; + + return ( +
+ +

+ {t('recentPerformance', { ns: 'library' })} + openHelp('Era Points')} + /> +

+
+ + +
+ +
+
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Roles.tsx b/src/canvas/JoinPool/Overview/Roles.tsx new file mode 100644 index 0000000000..cc3f2a89a6 --- /dev/null +++ b/src/canvas/JoinPool/Overview/Roles.tsx @@ -0,0 +1,55 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CardWrapper } from 'library/Card/Wrappers'; +import { AddressesWrapper, HeadingWrapper } from '../Wrappers'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { useHelp } from 'contexts/Help'; +import { AddressSection } from './AddressSection'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Roles = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation('pages'); + const { openHelp } = useHelp(); + + return ( +
+ + +

+ {t('pools.roles')} + openHelp('Pool Roles')} /> +

+
+ + + {bondedPool.roles.root && ( + + )} + {bondedPool.roles.nominator && ( + + )} + {bondedPool.roles.bouncer && ( + + )} + {bondedPool.roles.depositor && ( + + )} + +
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Stats.tsx b/src/canvas/JoinPool/Overview/Stats.tsx new file mode 100644 index 0000000000..61e9befdd9 --- /dev/null +++ b/src/canvas/JoinPool/Overview/Stats.tsx @@ -0,0 +1,99 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useNetwork } from 'contexts/Network'; +import { HeadingWrapper } from '../Wrappers'; +import { planckToUnit, rmCommas } from '@w3ux/utils'; +import { useApi } from 'contexts/Api'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { MaxEraRewardPointsEras } from 'consts'; +import { StyledLoader } from 'library/PoolSync/Loader'; +import type { CSSProperties } from 'styled-components'; +import { PoolSync } from 'library/PoolSync'; + +export const Stats = ({ + bondedPool, + performanceKey, + graphSyncing, +}: OverviewSectionProps) => { + const { t } = useTranslation('library'); + const { + networkData: { + units, + unit, + brand: { token: Token }, + }, + } = useNetwork(); + const { isReady, api } = useApi(); + const { getPoolRewardPoints } = usePoolPerformance(); + const poolRewardPoints = getPoolRewardPoints(performanceKey); + const rawEraRewardPoints = Object.values( + poolRewardPoints[bondedPool.addresses.stash] || {} + ); + + // Store the pool balance. + const [poolBalance, setPoolBalance] = useState(null); + + // Fetches the balance of the bonded pool. + const getPoolBalance = async () => { + if (!api) { + return; + } + + const balance = ( + await api.call.nominationPoolsApi.pointsToBalance( + bondedPool.id, + rmCommas(bondedPool.points) + ) + ).toString(); + + if (balance) { + setPoolBalance(new BigNumber(balance)); + } + }; + + // Fetch the balance when pool or points change. + useEffect(() => { + if (isReady) { + getPoolBalance(); + } + }, [bondedPool.id, bondedPool.points, isReady]); + + const vars = { + '--loader-color': 'var(--text-color-secondary)', + } as CSSProperties; + + return ( + +

+ {graphSyncing ? ( + + {t('syncing')} + + + + ) : ( + <> + {rawEraRewardPoints.length === MaxEraRewardPointsEras && ( + {t('activelyNominating')} + )} + + + + {!poolBalance + ? `...` + : planckToUnit(poolBalance, units) + .decimalPlaces(3) + .toFormat()}{' '} + {unit} {t('bonded')} + + + )} +

+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/index.tsx b/src/canvas/JoinPool/Overview/index.tsx new file mode 100644 index 0000000000..57bef1f426 --- /dev/null +++ b/src/canvas/JoinPool/Overview/index.tsx @@ -0,0 +1,47 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { JoinForm } from './JoinForm'; + +import { PerformanceGraph } from './PerformanceGraph'; +import { Stats } from './Stats'; +import { Addresses } from './Addresses'; +import { Roles } from './Roles'; +import { GraphLayoutWrapper } from '../Wrappers'; +import type { OverviewSectionProps } from '../types'; +import { useBalances } from 'contexts/Balances'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const Overview = (props: OverviewSectionProps) => { + const { getPoolMembership } = useBalances(); + const { activeAccount } = useActiveAccounts(); + + const { + bondedPool: { state }, + } = props; + + const showJoinForm = + activeAccount !== null && + state === 'Open' && + getPoolMembership(activeAccount) === null; + + return ( + <> +
+ + + + + + +
+ {showJoinForm && ( +
+
+ +
+
+ )} + + ); +}; diff --git a/src/canvas/JoinPool/Preloader.tsx b/src/canvas/JoinPool/Preloader.tsx new file mode 100644 index 0000000000..3326a3841a --- /dev/null +++ b/src/canvas/JoinPool/Preloader.tsx @@ -0,0 +1,88 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { useOverlay } from 'kits/Overlay/Provider'; +import { useTranslation } from 'react-i18next'; +import { JoinPoolInterfaceWrapper } from './Wrappers'; +import { CanvasTitleWrapper } from 'canvas/Wrappers'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import BigNumber from 'bignumber.js'; +import type { BondedPool } from 'contexts/Pools/BondedPools/types'; +import { capitalizeFirstLetter, planckToUnit, rmCommas } from '@w3ux/utils'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from 'contexts/Api'; +import { PoolSyncBar } from 'library/PoolSync/Bar'; +import type { PoolRewardPointsKey } from 'contexts/Pools/PoolPerformance/types'; + +export const Preloader = ({ + performanceKey, +}: { + performanceKey: PoolRewardPointsKey; +}) => { + const { t } = useTranslation('pages'); + const { + network, + networkData: { units, unit }, + } = useNetwork(); + const { bondedPools } = useBondedPools(); + const { + poolsConfig: { counterForPoolMembers }, + } = useApi(); + const { closeCanvas } = useOverlay().canvas; + + let totalPoolPoints = new BigNumber(0); + bondedPools.forEach((b: BondedPool) => { + totalPoolPoints = totalPoolPoints.plus(rmCommas(b.points)); + }); + const totalPoolPointsUnit = planckToUnit(totalPoolPoints, units) + .decimalPlaces(0) + .toFormat(); + + return ( + <> +
+ closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ +
+
+
+
+

{t('pools.pools')}

+
+
+

+ {t('pools.joinPoolHeading', { + totalMembers: new BigNumber(counterForPoolMembers).toFormat(), + totalPoolPoints: totalPoolPointsUnit, + unit, + network: capitalizeFirstLetter(network), + })} +

+
+
+
+
+ + +
+

+ {t('analyzingPoolPerformance', { ns: 'library' })}... +

+ +

+ +

+
+
+ + ); +}; diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts new file mode 100644 index 0000000000..cc831bc612 --- /dev/null +++ b/src/canvas/JoinPool/Wrappers.ts @@ -0,0 +1,352 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const JoinPoolInterfaceWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + > .header { + display: flex; + margin-bottom: 2rem; + } + + > .content { + display: flex; + flex-grow: 1; + + @media (max-width: 1000px) { + flex-flow: row wrap; + } + + > div { + display: flex; + + &.main { + flex-grow: 1; + display: flex; + flex-direction: column; + + @media (max-width: 1000px) { + flex-basis: 100%; + } + } + + &.side { + min-width: 460px; + padding-left: 2.5rem; + + @media (max-width: 1000px) { + flex-grow: 1; + flex-basis: 100%; + margin-top: 0.5rem; + padding-left: 0; + } + + > div { + width: 100%; + } + } + } + + > .tip { + color: var(--accent-color-primary); + margin-bottom: 1.25rem; + font-family: Inter, sans-serif; + display: flex; + align-items: center; + justify-content: flex-start; + + > .loader { + background-color: var(--background-canvas-card-secondary); + color: var(--accent-color-primary); + width: 100%; + height: 0.5rem; + border-radius: 1rem; + position: relative; + + > div { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border-radius: 1rem; + + > .progress { + background-color: var(--accent-color-primary); + position: absolute; + top: 0; + left: 0; + width: 0; + height: 100%; + border-radius: 2rem; + transition: width 1s cubic-bezier(0.1, 1, 0.1, 1); + } + } + } + } + } +`; + +export const PreloaderWrapper = styled.div` + background-color: var(--background-floating-card); + width: 100%; + height: 2rem; + border-radius: 2rem; + opacity: 0.4; +`; + +export const JoinFormWrapper = styled.div` + background: var(--background-canvas-card); + border: 0.75px solid var(--border-primary-color); + box-shadow: var(--card-shadow); + border-radius: 1.5rem; + padding: 1.5rem; + width: 100%; + + @media (max-width: 1000px) { + margin-top: 1rem; + } + + &.preload { + padding: 0; + opacity: 0.5; + } + + h4 { + display: flex; + align-items: center; + &.note { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + } + } + + > h2 { + color: var(--text-color-secondary); + margin: 0.25rem 0; + } + + > h4 { + margin: 1.5rem 0 0.5rem 0; + color: var(--text-color-tertiary); + + &.underline { + border-bottom: 1px solid var(--border-primary-color); + padding-bottom: 0.5rem; + margin: 2rem 0 1rem 0; + } + } + + > .input { + border-bottom: 1px solid var(--border-primary-color); + padding: 0 0.25rem; + display: flex; + align-items: flex-end; + padding-bottom: 1.25rem; + + > div { + flex-grow: 1; + display: flex; + flex-direction: column; + + > div { + margin: 0; + } + } + } + + > .available { + margin-top: 0.5rem; + margin-bottom: 1.5rem; + display: flex; + } + + > .submit { + margin-top: 2.5rem; + } +`; + +export const HeadingWrapper = styled.div` + margin: 0.5rem 0.5rem 0.5rem 0rem; + + @media (max-width: 600px) { + margin-right: 0; + } + + h3, + p { + padding: 0 0.5rem; + } + + h4 { + font-size: 1.15rem; + } + + p { + color: var(--text-color-tertiary); + margin: 0.35rem 0 0 0; + } + + > h3, + h4 { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + margin: 0; + display: flex; + align-items: center; + + @media (max-width: 600px) { + flex-wrap: wrap; + } + + > span { + background-color: var(--background-canvas-card-secondary); + color: var(--text-color-secondary); + font-family: InterBold, sans-serif; + border-radius: 1.5rem; + padding: 0rem 1.25rem; + margin-right: 1rem; + height: 2.6rem; + display: flex; + align-items: center; + + @media (max-width: 600px) { + flex-grow: 1; + justify-content: center; + margin-bottom: 1rem; + margin-right: 0; + height: 2.9rem; + width: 100%; + + &:last-child { + margin-bottom: 0; + } + } + + &.balance { + padding-left: 0.5rem; + } + + > .icon { + width: 2.1rem; + height: 2.1rem; + margin-right: 0.3rem; + } + &.inactive { + color: var(--text-color-tertiary); + border: 1px solid var(--border-secondary-color); + } + } + } +`; + +export const AddressesWrapper = styled.div` + flex: 1; + display: flex; + padding: 0rem 0.25rem; + flex-wrap: wrap; + + > section { + display: flex; + flex-direction: column; + flex-basis: 50%; + margin: 0.9rem 0 0.7rem 0; + + @media (max-width: 600px) { + flex-basis: 100%; + } + + > div { + display: flex; + flex-direction: row; + align-items: center; + + > span { + margin-right: 0.75rem; + } + + > h4 { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + display: flex; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin: 0; + flex: 1; + + &.heading { + font-family: InterBold, sans-serif; + } + + > .label { + margin-left: 0.75rem; + + > button { + color: var(--text-color-tertiary); + } + } + } + } + } +`; + +// Wrapper that houses the chart, allowing it to be responsive. +export const GraphWrapper = styled.div` + flex: 1; + position: relative; + padding: 0 4rem 0 1rem; + margin-top: 2rem; + + @media (max-width: 1000px) { + padding: 0 0 0 1rem; + } + + > .inner { + position: absolute; + width: 100%; + height: 100%; + padding-left: 1rem; + padding-right: 4rem; + + @media (max-width: 1000px) { + padding-right: 1.5rem; + } + } +`; + +// Element used to wrap graph and pool stats, allowing flex ordering on smaller screens. +export const GraphLayoutWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; + + @media (min-width: 1001px) { + > div:last-child { + margin-top: 1.25rem; + } + } + + @media (max-width: 1000px) { + > div { + &:first-child { + order: 2; + margin-top: 1.5rem; + margin-bottom: 0; + } + &:last-child { + order: 1; + } + } + } +`; + +export const NominationsWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; +`; diff --git a/src/canvas/JoinPool/index.tsx b/src/canvas/JoinPool/index.tsx new file mode 100644 index 0000000000..9d63aba25f --- /dev/null +++ b/src/canvas/JoinPool/index.tsx @@ -0,0 +1,147 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CanvasFullScreenWrapper } from 'canvas/Wrappers'; +import { useOverlay } from 'kits/Overlay/Provider'; +import { JoinPoolInterfaceWrapper } from './Wrappers'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +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'; +import { Preloader } from './Preloader'; + +export const JoinPool = () => { + const { + config: { options }, + } = useOverlay().canvas; + const { eraStakers } = useStaking(); + const { poolsForJoin } = useJoinPools(); + const { poolsMetaData, bondedPools } = useBondedPools(); + const { getPoolRewardPoints, getPoolPerformanceTask } = usePoolPerformance(); + + // Get the provided pool id and performance batch key from options, if available. + const providedPool = options?.providedPool; + const providedPoolId = providedPool?.id || null; + const performanceKey = + providedPoolId && providedPool?.performanceBatchKey + ? providedPool?.performanceBatchKey + : 'pool_join'; + + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask(performanceKey); + + const performanceDataReady = poolJoinPerformanceTask.status === 'synced'; + + // Get performance data: Assumed to be fetched now. + const poolRewardPoints = getPoolRewardPoints(performanceKey); + + // The active canvas tab. + const [activeTab, setActiveTab] = 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( + () => + poolsForJoin + .filter((pool) => { + // Fetch reward point data for the pool. + const rawEraRewardPoints = + poolRewardPoints[pool.addresses.stash] || {}; + const rewardPoints = Object.values(rawEraRewardPoints); + + // Ensure pool has been active for every era in performance data. + const activeDaily = + rewardPoints.every((points) => Number(points) > 0) && + rewardPoints.length === MaxEraRewardPointsEras; + + return activeDaily; + }) + // Ensure the pool is currently in the active set of backers. + .filter((pool) => + eraStakers.stakers.find((staker) => + staker.others.find(({ who }) => who !== pool.addresses.stash) + ) + ), + [poolsForJoin, poolRewardPoints, performanceDataReady] + ); + + const initialSelectedPoolId = useMemo( + () => + providedPoolId || + filteredBondedPools[(filteredBondedPools.length * Math.random()) << 0] + ?.id || + 0, + [] + ); + + // The selected bonded pool id. Assigns a random id if one is not provided. + const [selectedPoolId, setSelectedPoolId] = useState( + 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] + ); + + // If syncing completes within the canvas, assign a selected pool. + useEffect(() => { + if (performanceDataReady && selectedPoolId === 0) { + setSelectedPoolId( + filteredBondedPools[(filteredBondedPools.length * Math.random()) << 0] + ?.id || 0 + ); + } + }, [performanceDataReady]); + + return ( + + {(!providedPoolId && poolJoinPerformanceTask.status !== 'synced') || + !bondedPool ? ( + + ) : ( + <> +
+ + +
+ {activeTab === 0 && ( + + )} + {activeTab === 1 && ( + + )} +
+
+ + )} + + ); +}; diff --git a/src/canvas/JoinPool/types.ts b/src/canvas/JoinPool/types.ts new file mode 100644 index 0000000000..7f890837d9 --- /dev/null +++ b/src/canvas/JoinPool/types.ts @@ -0,0 +1,34 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { BondedPool } from 'contexts/Pools/BondedPools/types'; +import type { PoolRewardPointsKey } from 'contexts/Pools/PoolPerformance/types'; +import type { Dispatch, SetStateAction } from 'react'; + +export interface JoinPoolHeaderProps { + activeTab: number; + bondedPool: BondedPool; + filteredBondedPools: BondedPool[]; + metadata: string; + autoSelected: boolean; + setActiveTab: (tab: number) => void; + setSelectedPoolId: Dispatch>; + providedPoolId: number; +} + +export interface NominationsProps { + stash: string; + poolId: number; +} + +export interface AddressSectionProps { + address: string; + label: string; + helpKey?: string; +} + +export interface OverviewSectionProps { + bondedPool: BondedPool; + performanceKey: PoolRewardPointsKey; + graphSyncing: boolean; +} diff --git a/src/pages/Nominate/Setup/Bond/index.tsx b/src/canvas/NominatorSetup/Bond/index.tsx similarity index 100% rename from src/pages/Nominate/Setup/Bond/index.tsx rename to src/canvas/NominatorSetup/Bond/index.tsx diff --git a/src/pages/Nominate/Setup/Payee/index.tsx b/src/canvas/NominatorSetup/Payee/index.tsx similarity index 100% rename from src/pages/Nominate/Setup/Payee/index.tsx rename to src/canvas/NominatorSetup/Payee/index.tsx diff --git a/src/pages/Nominate/Setup/Summary/Wrapper.ts b/src/canvas/NominatorSetup/Summary/Wrapper.ts similarity index 100% rename from src/pages/Nominate/Setup/Summary/Wrapper.ts rename to src/canvas/NominatorSetup/Summary/Wrapper.ts diff --git a/src/pages/Nominate/Setup/Summary/index.tsx b/src/canvas/NominatorSetup/Summary/index.tsx similarity index 95% rename from src/pages/Nominate/Setup/Summary/index.tsx rename to src/canvas/NominatorSetup/Summary/index.tsx index 3b4bf22263..d468193a83 100644 --- a/src/pages/Nominate/Setup/Summary/index.tsx +++ b/src/canvas/NominatorSetup/Summary/index.tsx @@ -20,6 +20,7 @@ import { useApi } from 'contexts/Api'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { SummaryWrapper } from './Wrapper'; +import { useOverlay } from 'kits/Overlay/Provider'; export const Summary = ({ section }: SetupStepProps) => { const { t } = useTranslation('pages'); @@ -30,6 +31,7 @@ export const Summary = ({ section }: SetupStepProps) => { } = useNetwork(); const { newBatchCall } = useBatchCall(); const { getPayeeItems } = usePayeeConfig(); + const { closeCanvas } = useOverlay().canvas; const { accountHasSigner } = useImportedAccounts(); const { activeAccount, activeProxy } = useActiveAccounts(); const { getNominatorSetup, removeSetupProgress } = useSetup(); @@ -71,6 +73,10 @@ export const Summary = ({ section }: SetupStepProps) => { from: activeAccount, shouldSubmit: true, callbackInBlock: () => { + // Close the canvas after the extrinsic is included in a block. + closeCanvas(); + + // Reset setup progress. removeSetupProgress('nominator', activeAccount); }, }); diff --git a/src/canvas/NominatorSetup/index.tsx b/src/canvas/NominatorSetup/index.tsx new file mode 100644 index 0000000000..cecec5bac0 --- /dev/null +++ b/src/canvas/NominatorSetup/index.tsx @@ -0,0 +1,64 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { Element } from 'react-scroll'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { Nominate } from 'library/SetupSteps/Nominate'; +import { Payee } from 'canvas/NominatorSetup/Payee'; +import { Bond } from 'canvas/NominatorSetup/Bond'; +import { Summary } from 'canvas/NominatorSetup/Summary'; +import { CanvasFullScreenWrapper, CanvasTitleWrapper } from 'canvas/Wrappers'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { useOverlay } from 'kits/Overlay/Provider'; + +export const NominatorSetup = () => { + const { t } = useTranslation('pages'); + const { closeCanvas } = useOverlay().canvas; + + return ( + +
+ closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ + +
+
+
+
+

{t('nominate.startNominating')}

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + ); +}; 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 22018ef3a0..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(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 4afd4dcd56..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; } @@ -15,5 +14,5 @@ export type DefaultMembersListProps = MembersListProps & { }; export type FetchpageMembersListProps = MembersListProps & { - memberCount: number; + memberCount: string; }; diff --git a/src/canvas/PoolMembers/Members.tsx b/src/canvas/PoolMembers/Members.tsx index 4e6bf6e8ee..de96654816 100644 --- a/src/canvas/PoolMembers/Members.tsx +++ b/src/canvas/PoolMembers/Members.tsx @@ -18,8 +18,7 @@ export const Members = () => { const { mode } = useTheme(); const { pluginEnabled } = usePlugins(); const { getMembersOfPoolFromNode } = usePoolMembers(); - const { activePool, isOwner, isBouncer, activePoolMemberCount } = - useActivePool(); + const { activePool, isOwner, isBouncer } = useActivePool(); const { colors } = useNetwork().networkData; const annuncementBorderColor = colors.secondary[mode]; @@ -27,6 +26,8 @@ export const Members = () => { const showBlockedPrompt = activePool?.bondedPool?.state === 'Blocked' && (isOwner() || isBouncer()); + const memberCount = activePool?.bondedPool?.memberCounter ?? '0'; + const membersListProps = { batchKey: 'active_pool_members', pagination: true, @@ -80,7 +81,7 @@ export const Members = () => { {pluginEnabled('subscan') ? ( ) : ( .head { + display: flex; + align-items: center; + justify-content: flex-end; + } + + > h1 { + margin-top: 1.5rem; + margin-bottom: 1.25rem; + } +`; + +export const CanvasTitleWrapper = styled.div` + border-bottom: 1px solid var(--border-secondary-color); + flex: 1; + display: flex; + flex-direction: column; + margin: 2rem 0 1.55rem 0; + padding-bottom: 0.1rem; + + &.padding { + padding-bottom: 0.75rem; + } + + > .inner { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + flex: 1; + + &.standalone { + padding-bottom: 0.5rem; + } + + > div { + display: flex; + flex: 1; + + &:nth-child(1) { + max-width: 4rem; + + &.empty { + max-width: 0px; + } + } + + &:nth-child(2) { + padding-left: 1rem; + flex-direction: column; + + &.standalone { + padding-left: 0; + } + + > .title { + position: relative; + padding-top: 2rem; + flex: 1; + + h1 { + position: absolute; + top: 0; + left: 0; + margin: 0; + line-height: 2.2rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + } + } + + > .labels { + display: flex; + margin-top: 1.1rem; + + > h3 { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + margin: 0; + + > svg { + margin: 0 0 0 0.2rem; + } + + > span { + border: 1px solid var(--border-secondary-color); + border-radius: 0.5rem; + padding: 0.4rem 0.6rem; + margin-left: 1rem; + font-size: 1.1rem; + + &.blocked { + color: var(--accent-color-secondary); + border-color: var(--accent-color-secondary); + } + + &.destroying { + color: var(--status-danger-color); + border-color: var(--status-danger-color); + } + } + } + } + } + } + } +`; + +export const CanvasSubmitTxFooter = styled.div` + border-radius: 1rem; + overflow: hidden; + margin-bottom: 2rem; + width: 100%; +`; diff --git a/src/canvas/Wrappers.tsx b/src/canvas/Wrappers.tsx deleted file mode 100644 index 9b1d29a4ec..0000000000 --- a/src/canvas/Wrappers.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import styled from 'styled-components'; - -export const CanvasFullScreenWrapper = styled.div` - padding-top: 3rem; - min-height: calc(100vh - 12rem); - padding-bottom: 2rem; - width: 100%; - - > .head { - display: flex; - align-items: center; - justify-content: flex-end; - } - - > h1 { - margin-top: 1.5rem; - margin-bottom: 1.25rem; - } -`; - -export const CanvasSubmitTxFooter = styled.div` - border-radius: 1rem; - overflow: hidden; - margin-bottom: 2rem; - width: 100%; -`; 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/Api/index.tsx b/src/contexts/Api/index.tsx index 46b1bea262..031bd3149e 100644 --- a/src/contexts/Api/index.tsx +++ b/src/contexts/Api/index.tsx @@ -331,6 +331,11 @@ export const APIProvider = ({ children, network }: APIProviderProps) => { const reInitialiseApi = async (type: ConnectionType) => { setApiStatus('disconnected'); + + // Dispatch all default syncIds as syncing. + SyncController.dispatchAllDefault(); + + // Instanaite new API instance. await ApiController.instantiate(network, type, rpcEndpoint); }; diff --git a/src/contexts/Balances/index.tsx b/src/contexts/Balances/index.tsx index ca7c16dc7f..e5bbc3f7dc 100644 --- a/src/contexts/Balances/index.tsx +++ b/src/contexts/Balances/index.tsx @@ -14,6 +14,9 @@ import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useActiveBalances } from 'hooks/useActiveBalances'; import { useBonded } from 'contexts/Bonded'; import { SyncController } from 'controllers/SyncController'; +import { useApi } from 'contexts/Api'; +import { ActivePoolsController } from 'controllers/ActivePoolsController'; +import { useCreatePoolAccounts } from 'hooks/useCreatePoolAccounts'; export const BalancesContext = createContext( defaults.defaultBalancesContext @@ -22,8 +25,10 @@ export const BalancesContext = createContext( export const useBalances = () => useContext(BalancesContext); export const BalancesProvider = ({ children }: { children: ReactNode }) => { + const { api } = useApi(); const { getBondedAccount } = useBonded(); const { accounts } = useImportedAccounts(); + const createPoolAccounts = useCreatePoolAccounts(); const { activeAccount, activeProxy } = useActiveAccounts(); const controller = getBondedAccount(activeAccount); @@ -46,9 +51,24 @@ export const BalancesProvider = ({ children }: { children: ReactNode }) => { isCustomEvent(e) && BalancesController.isValidNewAccountBalanceEvent(e) ) { - // Update whether all account balances have been synced. Uses greater than to account for - // possible errors on the API side. + // Update whether all account balances have been synced. checkBalancesSynced(); + + const { address, ...newBalances } = e.detail; + const { poolMembership } = newBalances; + + // If a pool membership exists, let `ActivePools` know of pool membership to re-sync pool + // details and nominations. + if (api && poolMembership) { + const { poolId } = poolMembership; + const newPools = ActivePoolsController.getformattedPoolItems( + address + ).concat({ + id: String(poolId), + addresses: { ...createPoolAccounts(Number(poolId)) }, + }); + ActivePoolsController.syncPools(api, address, newPools); + } } }; 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/ActivePool/defaults.ts b/src/contexts/Pools/ActivePool/defaults.ts index 3bcd9f5fda..d909c9b15f 100644 --- a/src/contexts/Pools/ActivePool/defaults.ts +++ b/src/contexts/Pools/ActivePool/defaults.ts @@ -31,6 +31,5 @@ export const defaultActivePoolContext: ActivePoolContextState = { setActivePoolId: (p) => {}, activePool: null, activePoolNominations: null, - activePoolMemberCount: 0, pendingPoolRewards: new BigNumber(0), }; diff --git a/src/contexts/Pools/ActivePool/index.tsx b/src/contexts/Pools/ActivePool/index.tsx index b329d86608..ed30bd33b0 100644 --- a/src/contexts/Pools/ActivePool/index.tsx +++ b/src/contexts/Pools/ActivePool/index.tsx @@ -11,16 +11,12 @@ import { useRef, useState, } from 'react'; -import type { Sync } from 'types'; import { useEffectIgnoreInitial } from '@w3ux/hooks'; -import { usePlugins } from 'contexts/Plugins'; import { useNetwork } from 'contexts/Network'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useApi } from '../../Api'; import { useBondedPools } from '../BondedPools'; -import { usePoolMembers } from '../PoolMembers'; import type { ActivePoolContextState } from './types'; -import { SubscanController } from 'controllers/SubscanController'; import { useCreatePoolAccounts } from 'hooks/useCreatePoolAccounts'; import { useBalances } from 'contexts/Balances'; import { ActivePoolsController } from 'controllers/ActivePoolsController'; @@ -38,12 +34,11 @@ export const useActivePool = () => useContext(ActivePoolContext); export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { const { network } = useNetwork(); const { isReady, api } = useApi(); - const { pluginEnabled } = usePlugins(); const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); const createPoolAccounts = useCreatePoolAccounts(); - const { getMembersOfPoolFromNode } = usePoolMembers(); const { getAccountPoolRoles, bondedPools } = useBondedPools(); + const membership = getPoolMembership(activeAccount); // Determine active pools to subscribe to. Dependencies of `activeAccount`, and `membership` mean @@ -73,40 +68,34 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { setStateWithRef(id, setActivePoolIdState, activePoolIdRef); }; - // Only listen to the currently selected active pool, otherwise return an empty array. - const poolIds = activePoolIdRef.current ? [activePoolIdRef.current] : []; - - // Listen for active pools. - const { activePools, poolNominations } = useActivePools({ - poolIds, - onCallback: async () => { - // Sync: active pools synced once all account pools have been reported. - if (accountPoolIds.length <= ActivePoolsController.pools.length) { - SyncController.dispatch('active-pools', 'complete'); - } - }, - }); + // Only listen to the active account's active pools, otherwise return an empty array. NOTE: + // `activePoolsRef` is needed to check if the pool has changed after the async call of fetching + // pending rewards. + const { getActivePools, activePoolsRef, getPoolNominations } = useActivePools( + { + who: activeAccount, + onCallback: async () => { + // Sync: active pools synced once all account pools have been reported. + if ( + accountPoolIds.length <= + ActivePoolsController.getPools(activeAccount).length + ) { + SyncController.dispatch('active-pools', 'complete'); + } + }, + } + ); // Store the currently active pool's pending rewards for the active account. const [pendingPoolRewards, setPendingPoolRewards] = useState( new BigNumber(0) ); - const activePool = - activePoolId && activePools[activePoolId] - ? activePools[activePoolId] - : null; + const activePool = activePoolId ? getActivePools(activePoolId) : null; - const activePoolNominations = - activePoolId && poolNominations[activePoolId] - ? poolNominations[activePoolId] - : null; - - // Store the member count of the selected pool. - const [activePoolMemberCount, setactivePoolMemberCount] = useState(0); - - // Keep track of whether the pool member count is being fetched. - const fetchingMemberCount = useRef('unsynced'); + const activePoolNominations = activePoolId + ? getPoolNominations(activePoolId) + : null; // Sync active pool subscriptions. const syncActivePoolSubscriptions = async () => { @@ -115,7 +104,12 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { id: pool, addresses: { ...createPoolAccounts(Number(pool)) }, })); - ActivePoolsController.syncPools(api, newActivePools); + + SyncController.dispatch('active-pools', 'syncing'); + ActivePoolsController.syncPools(api, activeAccount, newActivePools); + } else { + // No active pools to sync. Mark as complete. + SyncController.dispatch('active-pools', 'complete'); } }; @@ -197,9 +191,20 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { if ( activePool && membership?.poolId && + membership?.address && String(activePool.id) === String(membership.poolId) ) { - setPendingPoolRewards(await fetchPendingRewards(membership?.address)); + const pendingRewards = await fetchPendingRewards(membership.address); + + // Check if active pool has changed in the time the pending rewards were being fetched. If it + // has, do not update. + if ( + activePoolId && + activePoolsRef.current[activePoolId]?.id === + Number(membership.poolId || -1) + ) { + setPendingPoolRewards(pendingRewards); + } } else { setPendingPoolRewards(new BigNumber(0)); } @@ -215,41 +220,6 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { return new BigNumber(0); }; - // Gets the member count of the currently selected pool. If Subscan is enabled, it is used instead of the connected node. - const getMemberCount = async () => { - if (!activePool?.id) { - setactivePoolMemberCount(0); - return; - } - // If `Subscan` plugin is enabled, fetch member count directly from the API. - if ( - pluginEnabled('subscan') && - fetchingMemberCount.current === 'unsynced' - ) { - fetchingMemberCount.current = 'syncing'; - const poolDetails = await SubscanController.handleFetchPoolDetails( - activePool.id - ); - fetchingMemberCount.current = 'synced'; - setactivePoolMemberCount(poolDetails?.member_count || 0); - return; - } - // If no plugin available, fetch all pool members from RPC and filter them to determine current - // pool member count. NOTE: Expensive operation. - setactivePoolMemberCount( - getMembersOfPoolFromNode(activePool?.id || 0)?.length || 0 - ); - }; - - // Fetch pool member count. We use `membership` as a dependency as the member count could change - // in the UI when active account's membership changes. NOTE: Do not have `poolMembersNode` as a - // dependency - could trigger many re-renders if value is constantly changing - more suited as a - // custom event. - useEffect(() => { - fetchingMemberCount.current = 'unsynced'; - getMemberCount(); - }, [activeAccount, activePool, membership?.poolId]); - // Re-calculate pending rewards when membership changes. useEffectIgnoreInitial(() => { if (isReady) { @@ -293,7 +263,6 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { getPoolRoles, setActivePoolId, activePool, - activePoolMemberCount, activePoolNominations, pendingPoolRewards, }} diff --git a/src/contexts/Pools/ActivePool/types.ts b/src/contexts/Pools/ActivePool/types.ts index bf30385589..1ef0b32509 100644 --- a/src/contexts/Pools/ActivePool/types.ts +++ b/src/contexts/Pools/ActivePool/types.ts @@ -19,7 +19,6 @@ export interface ActivePoolContextState { setActivePoolId: (p: string) => void; activePool: ActivePool | null; activePoolNominations: Nominations | null; - activePoolMemberCount: number; pendingPoolRewards: BigNumber; } 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 d3d22677f8..e110524485 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'; @@ -20,6 +21,7 @@ import { useNetwork } from 'contexts/Network'; import { useApi } from '../../Api'; import { defaultBondedPoolsContext } from './defaults'; import { useCreatePoolAccounts } from 'hooks/useCreatePoolAccounts'; +import { SyncController } from 'controllers/SyncController'; export const BondedPoolsContext = createContext( defaultBondedPoolsContext @@ -55,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') { @@ -85,6 +90,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { ); bondedPoolsSynced.current = 'synced'; + SyncController.dispatch('bonded-pools', 'complete'); }; // Fetches pool nominations and updates state. @@ -197,7 +203,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { }); const getBondedPool = (poolId: MaybePool) => - bondedPools.find((p) => p.id === poolId) ?? null; + bondedPools.find((p) => String(p.id) === String(poolId)) ?? null; /* * poolSearchFilter Iterates through the supplied list and refers to the meta batch of the list to @@ -286,7 +292,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { }; // Gets all pools that the account has a role in. Returns an object with each pool role as keys, - // and and array of pool ids as their values. + // and array of pool ids as their values. const accumulateAccountPoolRoles = (who: MaybeAddress): AccountPoolRoles => { if (!who) { return { @@ -386,6 +392,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { // Clear existing state for network refresh. useEffectIgnoreInitial(() => { bondedPoolsSynced.current = 'unsynced'; + SyncController.dispatch('bonded-pools', 'syncing'); setStateWithRef([], setBondedPools, bondedPoolsRef); setPoolsMetadata({}); setPoolsNominations({}); @@ -422,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..73371e7580 --- /dev/null +++ b/src/contexts/Pools/JoinPools/index.tsx @@ -0,0 +1,100 @@ +// 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 { rmCommas, shuffle } from '@w3ux/utils'; +import BigNumber from 'bignumber.js'; + +export const JoinPoolsContext = createContext( + defaultJoinPoolsContext +); + +export const useJoinPools = () => useContext(JoinPoolsContext); + +export const JoinPoolsProvider = ({ children }: { children: ReactNode }) => { + const { + api, + activeEra, + networkMetrics: { minimumActiveStake }, + } = 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. Start by only considering active pools. + const activeBondedPools = bondedPools.filter( + ({ state }) => state === 'Open' + ); + + // Filter pools that do not have at least double the minimum stake to earn rewards, in points. + // NOTE: assumes that points are a 1:1 ratio between balance and points. + const rewardBondedPools = activeBondedPools.filter(({ points }) => { + const pointsBn = new BigNumber(rmCommas(points)); + const threshold = minimumActiveStake.multipliedBy(2); + return pointsBn.isGreaterThanOrEqualTo(threshold); + }); + + // Order active bonded pools by member count. + const sortedBondedPools = rewardBondedPools.sort( + (a, b) => + Number(rmCommas(a.memberCounter)) - Number(rmCommas(b.memberCounter)) + ); + + // Take lower third of sorted bonded pools to join. + const lowerThirdBondedPools = sortedBondedPools.slice( + 0, + Math.floor(sortedBondedPools.length / 3) + ); + + // Shuffle the lower third of bonded pools to join, and select a random subset of them. + const poolJoinSelection = shuffle(lowerThirdBondedPools).slice( + 0, + MaxPoolsForJoin + ); + + // Commit final pool selection to state. + 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..6f0a735f1c 100644 --- a/src/contexts/Pools/PoolPerformance/types.ts +++ b/src/contexts/Pools/PoolPerformance/types.ts @@ -1,9 +1,63 @@ // 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 = Record< + PoolRewardPointsKey, + PoolPerformanceTaskStatus +>; + +// 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 = string; + +// Pool reward batches, keyed by batch key. +export type PoolRewardPointsMap = Record; + +// 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/contexts/Setup/defaults.ts b/src/contexts/Setup/defaults.ts index 1309ba94ef..58eb99dd21 100644 --- a/src/contexts/Setup/defaults.ts +++ b/src/contexts/Setup/defaults.ts @@ -30,10 +30,6 @@ export const defaultSetupContext: SetupContextInterface = { getPoolSetupPercent: (a) => 0, setActiveAccountSetup: (t, p) => {}, setActiveAccountSetupSection: (t, s) => {}, - setOnNominatorSetup: (v) => {}, - setOnPoolSetup: (v) => {}, - onNominatorSetup: false, - onPoolSetup: false, getNominatorSetup: (address) => ({ section: 1, progress: defaultNominatorProgress, diff --git a/src/contexts/Setup/index.tsx b/src/contexts/Setup/index.tsx index 2da627cb08..8aff6e9986 100644 --- a/src/contexts/Setup/index.tsx +++ b/src/contexts/Setup/index.tsx @@ -13,7 +13,6 @@ import { useEffectIgnoreInitial } from '@w3ux/hooks'; import { useNetwork } from 'contexts/Network'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -import { useStaking } from '../Staking'; import { defaultNominatorProgress, defaultPoolProgress, @@ -28,7 +27,6 @@ import type { PoolSetups, SetupContextInterface, } from './types'; -import { useBalances } from 'contexts/Balances'; export const SetupContext = createContext(defaultSetupContext); @@ -36,23 +34,13 @@ export const SetupContext = export const useSetup = () => useContext(SetupContext); export const SetupProvider = ({ children }: { children: ReactNode }) => { - const { inSetup } = useStaking(); const { network, networkData: { units }, } = useNetwork(); const { accounts } = useImportedAccounts(); - const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); - const poolMembership = getPoolMembership(activeAccount); - - // is the user actively on the setup page - const [onNominatorSetup, setOnNominatorSetup] = useState(false); - - // is the user actively on the pool creation page - const [onPoolSetup, setOnPoolSetup] = useState(false); - // Store all imported accounts nominator setups. const [nominatorSetups, setNominatorSetups] = useState({}); @@ -271,16 +259,6 @@ export const SetupProvider = ({ children }: { children: ReactNode }) => { } }; - // Move away from setup pages on completion / network change. - useEffectIgnoreInitial(() => { - if (!inSetup()) { - setOnNominatorSetup(false); - } - if (poolMembership) { - setOnPoolSetup(false); - } - }, [inSetup(), network, poolMembership]); - // Update setup state when activeAccount changes useEffectIgnoreInitial(() => { if (accounts.length) { @@ -296,10 +274,6 @@ export const SetupProvider = ({ children }: { children: ReactNode }) => { getPoolSetupPercent, setActiveAccountSetup, setActiveAccountSetupSection, - setOnNominatorSetup, - setOnPoolSetup, - onNominatorSetup, - onPoolSetup, getNominatorSetup, getPoolSetup, }} diff --git a/src/contexts/Setup/types.ts b/src/contexts/Setup/types.ts index aba1b0ca80..1ab6dcaebd 100644 --- a/src/contexts/Setup/types.ts +++ b/src/contexts/Setup/types.ts @@ -53,10 +53,6 @@ export interface SetupContextInterface { p: NominatorProgress | PoolProgress ) => void; setActiveAccountSetupSection: (t: BondFor, s: number) => void; - setOnNominatorSetup: (v: boolean) => void; - setOnPoolSetup: (v: boolean) => void; - onNominatorSetup: boolean; - onPoolSetup: boolean; getNominatorSetup: (address: MaybeAddress) => NominatorSetup; getPoolSetup: (address: MaybeAddress) => PoolSetup; } diff --git a/src/contexts/Staking/index.tsx b/src/contexts/Staking/index.tsx index 25885e6bf8..cb061c5adc 100644 --- a/src/contexts/Staking/index.tsx +++ b/src/contexts/Staking/index.tsx @@ -313,7 +313,7 @@ export const StakingProvider = ({ children }: { children: ReactNode }) => { } }, [apiStatus]); - // handle syncing with eraStakers + // handle syncing with eraStakers. useEffectIgnoreInitial(() => { if (isReady) { fetchActiveEraStakers(); diff --git a/src/controllers/ActivePoolsController/defaults.ts b/src/controllers/ActivePoolsController/defaults.ts new file mode 100644 index 0000000000..5280165f72 --- /dev/null +++ b/src/controllers/ActivePoolsController/defaults.ts @@ -0,0 +1,4 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export const defaultClaimPermission = 'PermissionlessWithdraw'; diff --git a/src/controllers/ActivePoolsController/index.ts b/src/controllers/ActivePoolsController/index.ts index cb1a5d023c..edba26acdc 100644 --- a/src/controllers/ActivePoolsController/index.ts +++ b/src/controllers/ActivePoolsController/index.ts @@ -5,8 +5,14 @@ import type { VoidFn } from '@polkadot/api/types'; import { defaultPoolNominations } from 'contexts/Pools/ActivePool/defaults'; import type { ActivePool, PoolRoles } from 'contexts/Pools/ActivePool/types'; import { IdentitiesController } from 'controllers/IdentitiesController'; -import type { AnyApi } from 'types'; -import type { ActivePoolItem, DetailActivePool } from './types'; +import type { AnyApi, MaybeAddress } from 'types'; +import type { + AccountActivePools, + AccountPoolNominations, + AccountUnsubs, + ActivePoolItem, + DetailActivePool, +} from './types'; import { SyncController } from 'controllers/SyncController'; import type { Nominations } from 'contexts/Balances/types'; import type { ApiPromise } from '@polkadot/api'; @@ -16,17 +22,18 @@ export class ActivePoolsController { // Class members. // ------------------------------------------------------ - // Pool ids that are being subscribed to. - static pools: ActivePoolItem[] = []; + // Pool ids that are being subscribed to. Keyed by address. + static pools: Record = {}; - // Active pools that are being returned from subscriptions, keyed by pool id. - static activePools: Record = {}; + // Active pools that are being returned from subscriptions, keyed by account address, then pool + // id. + static activePools: Record = {}; - // Active pool nominations, keyed by pool id. - static poolNominations: Record = {}; + // Active pool nominations, keyed by account address, then pool id. + static poolNominations: Record = {}; - // Unsubscribe objects. - static #unsubs: Record = {}; + // Unsubscribe objects, keyed by account address, then pool id. + static #unsubs: Record = {}; // ------------------------------------------------------ // Pool membership syncing. @@ -35,23 +42,27 @@ export class ActivePoolsController { // Subscribes to pools and unsubscribes from removed pools. static syncPools = async ( api: ApiPromise, + address: MaybeAddress, newPools: ActivePoolItem[] ): Promise => { - // Sync: Checking active pools. - SyncController.dispatch('active-pools', 'syncing'); + if (!address) { + return; + } // Handle pools that have been removed. - this.handleRemovedPools(newPools); + this.handleRemovedPools(address, newPools); + + const currentPools = this.getPools(address); // Determine new pools that need to be subscribed to. const poolsAdded = newPools.filter( - (newPool) => !this.pools.find((pool) => pool.id === newPool.id) + (newPool) => !currentPools.find(({ id }) => id === newPool.id) ); if (poolsAdded.length) { // Subscribe to and add new pool data. poolsAdded.forEach(async (pool) => { - this.pools.push(pool); + this.pools[address] = currentPools.concat(pool); const unsub = await api.queryMulti( [ @@ -69,26 +80,31 @@ export class ActivePoolsController { // NOTE: async: fetches identity data for roles. await this.handleActivePoolCallback( api, + address, pool, bondedPool, rewardPool, accountData ); - this.handleNominatorsCallback(pool, nominators); + this.handleNominatorsCallback(address, pool, nominators); - if (this.activePools[pool.id] && this.poolNominations[pool.id]) { + if ( + this.activePools?.[address]?.[pool.id] && + this.poolNominations?.[address]?.[pool.id] + ) { document.dispatchEvent( new CustomEvent('new-active-pool', { detail: { - pool: this.activePools[pool.id], - nominations: this.poolNominations[pool.id], + address, + pool: this.activePools[address][pool.id], + nominations: this.poolNominations[address][pool.id], }, }) ); } } ); - this.#unsubs[pool.id] = unsub; + this.setUnsub(address, pool.id, unsub); }); } else { // Status: Pools Synced Completed. @@ -99,6 +115,7 @@ export class ActivePoolsController { // Handle active pool callback. static handleActivePoolCallback = async ( api: ApiPromise, + address: string, pool: ActivePoolItem, bondedPoolResult: AnyApi, rewardPoolResult: AnyApi, @@ -126,15 +143,16 @@ export class ActivePoolsController { rewardAccountBalance, }; - this.activePools[pool.id] = newPool; + this.setActivePool(address, pool.id, newPool); } else { // Invalid pools were returned. To signal pool was synced, set active pool to `null`. - this.activePools[pool.id] = null; + this.setActivePool(address, pool.id, null); } }; // Handle nominators callback. static handleNominatorsCallback = ( + address: string, pool: ActivePoolItem, nominatorsResult: AnyApi ): void => { @@ -148,28 +166,50 @@ export class ActivePoolsController { submittedIn: maybeNewNominations.submittedIn.toHuman(), }; - this.poolNominations[pool.id] = newNominations; + this.setPoolNominations(address, pool.id, newNominations); }; // Remove pools that no longer exist. - static handleRemovedPools = (newPools: ActivePoolItem[]): void => { + static handleRemovedPools = ( + address: string, + newPools: ActivePoolItem[] + ): void => { + const currentPools = this.getPools(address); + // Determine removed pools - current ones that no longer exist in `newPools`. - const poolsRemoved = this.pools.filter( + const poolsRemoved = currentPools.filter( (pool) => !newPools.find((newPool) => newPool.id === pool.id) ); // Unsubscribe from removed pool subscriptions. poolsRemoved.forEach((pool) => { - if (this.#unsubs[pool.id]) { - this.#unsubs[pool.id](); + if (this.#unsubs?.[address]?.[pool.id]) { + this.#unsubs[address][pool.id](); } - delete this.#unsubs[pool.id]; - delete this.activePools[pool.id]; - delete this.poolNominations[pool.id]; + delete this.activePools[address][pool.id]; + delete this.poolNominations[address][pool.id]; }); // Remove removed pools from class. - this.pools = this.pools.filter((pool) => !poolsRemoved.includes(pool)); + this.pools[address] = currentPools.filter( + (pool) => !poolsRemoved.includes(pool) + ); + + // Tidy up empty class state. + if (!this.pools[address].length) { + delete this.pools[address]; + } + + if (!this.activePools[address]) { + delete this.activePools[address]; + } + + if (!this.poolNominations[address]) { + delete this.poolNominations[address]; + } + if (!this.#unsubs[address]) { + delete this.#unsubs[address]; + } }; // ------------------------------------------------------ @@ -178,22 +218,51 @@ export class ActivePoolsController { // Unsubscribe from all subscriptions and reset class members. static unsubscribe = (): void => { - Object.values(this.#unsubs).forEach((unsub) => { - unsub(); + Object.values(this.#unsubs).forEach((accountUnsubs) => { + Object.values(accountUnsubs).forEach((unsub) => { + unsub(); + }); }); + this.#unsubs = {}; }; static resetState = (): void => { - this.pools = []; + this.pools = {}; this.activePools = {}; this.poolNominations = {}; }; // ------------------------------------------------------ - // Class helpers. + // Getters. // ------------------------------------------------------ + // Gets pools for a provided address. + static getPools = (address: MaybeAddress): ActivePoolItem[] => { + if (!address) { + return []; + } + return this.pools?.[address] || []; + }; + + // Gets active pools for a provided address. + static getActivePools = (address: MaybeAddress): AccountActivePools => { + if (!address) { + return {}; + } + return this.activePools?.[address] || {}; + }; + + // Gets active pool nominations for a provided address. + static getPoolNominations = ( + address: MaybeAddress + ): AccountPoolNominations => { + if (!address) { + return {}; + } + return this.poolNominations?.[address] || {}; + }; + // Gets unique role addresses from a bonded pool's `roles` record. static getUniqueRoleAddresses = (roles: PoolRoles): string[] => { const roleAddresses: string[] = [ @@ -202,9 +271,65 @@ export class ActivePoolsController { return roleAddresses; }; + // ------------------------------------------------------ + // Setters. + // ------------------------------------------------------ + + // Set an active pool for an address. + static setActivePool = ( + address: string, + poolId: string, + activePool: ActivePool | null + ): void => { + if (!this.activePools[address]) { + this.activePools[address] = {}; + } + this.activePools[address][poolId] = activePool; + }; + + // Set pool nominations for an address. + static setPoolNominations = ( + address: string, + poolId: string, + nominations: Nominations + ): void => { + if (!this.poolNominations[address]) { + this.poolNominations[address] = {}; + } + this.poolNominations[address][poolId] = nominations; + }; + + // Set unsub for an address and pool id. + static setUnsub = (address: string, poolId: string, unsub: VoidFn): void => { + if (!this.#unsubs[address]) { + this.#unsubs[address] = {}; + } + this.#unsubs[address][poolId] = unsub; + }; + + // ------------------------------------------------------ + // Class helpers. + // ------------------------------------------------------ + + // Format pools into active pool items (id and addresses only). + static getformattedPoolItems = (address: MaybeAddress): ActivePoolItem[] => { + if (!address) { + return []; + } + return ( + this.pools?.[address]?.map(({ id, addresses }) => ({ + id: id.toString(), + addresses, + })) || [] + ); + }; + // Checks if event detailis a valid `new-active-pool` event. static isValidNewActivePool = ( event: CustomEvent ): event is CustomEvent => - event.detail && event.detail.pool && event.detail.nominations; + event.detail && + event.detail.address && + event.detail.pool && + event.detail.nominations; } diff --git a/src/controllers/ActivePoolsController/types.ts b/src/controllers/ActivePoolsController/types.ts index 4ffe7b691f..2108f15062 100644 --- a/src/controllers/ActivePoolsController/types.ts +++ b/src/controllers/ActivePoolsController/types.ts @@ -1,10 +1,12 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import type { VoidFn } from '@polkadot/api/types'; import type { Nominations } from 'contexts/Balances/types'; import type { ActivePool } from 'contexts/Pools/ActivePool/types'; export interface DetailActivePool { + address: string; pool: ActivePool; nominations: Nominations; } @@ -16,3 +18,9 @@ export interface ActivePoolItem { reward: string; }; } + +export type AccountActivePools = Record; + +export type AccountPoolNominations = Record; + +export type AccountUnsubs = Record; diff --git a/src/controllers/SubscanController/index.ts b/src/controllers/SubscanController/index.ts index a83d14c99a..b19a08431d 100644 --- a/src/controllers/SubscanController/index.ts +++ b/src/controllers/SubscanController/index.ts @@ -5,7 +5,6 @@ import type { SubscanPoolClaim, SubscanData, SubscanPayout, - SubscanPoolDetails, SubscanPoolMember, SubscanRequestBody, SubscanEraPoints, @@ -13,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 { // ------------------------------------------------------ @@ -26,7 +25,6 @@ export class SubscanController { // List of endpoints to be used for Subscan API calls. static ENDPOINTS = { eraStat: '/api/scan/staking/era_stat', - poolDetails: '/api/scan/nomination_pool/pool', poolMembers: '/api/scan/nomination_pool/pool/members', poolRewards: '/api/scan/nomination_pool/rewards', rewardSlash: '/api/v2/scan/account/reward_slash', @@ -45,7 +43,7 @@ export class SubscanController { static payoutData: Record = {}; // Subscan pool data, keyed by `---...`. - static poolData: Record = {}; + static poolData: Record = {}; // Subscan era points data, keyed by `-
-`. static eraPointsData: Record = {}; @@ -162,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) { @@ -178,19 +176,6 @@ export class SubscanController { .splice(0, result.list.length - 1); }; - // Fetch a pool's details from Subscan. - static fetchPoolDetails = async ( - poolId: number - ): Promise => { - const result = await this.makeRequest(this.ENDPOINTS.poolDetails, { - pool_id: poolId, - }); - if (!result) { - return { member_count: 0 }; - } - return { member_count: result.member_count }; - }; - // Fetch a pool's era points from Subscan. static fetchEraPoints = async ( address: string, @@ -235,20 +220,6 @@ export class SubscanController { } }; - // Handle fetching pool details. - static handleFetchPoolDetails = async (poolId: number) => { - const dataKey = `${this.network}-${poolId}-details}`; - const currentValue = this.poolData[dataKey]; - - if (currentValue) { - return currentValue as SubscanPoolDetails; - } else { - const result = await this.fetchPoolDetails(poolId); - this.poolData[dataKey] = result; - return result; - } - }; - // Handle fetching era point history. static handleFetchEraPoints = async (address: string, era: number) => { const dataKey = `${this.network}-${address}-${era}}`; diff --git a/src/controllers/SubscanController/types.ts b/src/controllers/SubscanController/types.ts index a436a3a65a..02d11191b0 100644 --- a/src/controllers/SubscanController/types.ts +++ b/src/controllers/SubscanController/types.ts @@ -39,8 +39,7 @@ export interface SubscanRequestPagination { export type SubscanResult = | SubscanPayout[] | SubscanPoolClaim[] - | SubscanPoolMember[] - | SubscanPoolDetails; + | SubscanPoolMember[]; export interface SubscanPoolClaim { account_display: { @@ -89,10 +88,6 @@ export interface SubscanPoolMember { claimable: string; } -export interface SubscanPoolDetails { - member_count: number; -} - export interface SubscanEraPoints { era: number; reward_point: number; diff --git a/src/controllers/SyncController/defaults.ts b/src/controllers/SyncController/defaults.ts new file mode 100644 index 0000000000..e04d5254b5 --- /dev/null +++ b/src/controllers/SyncController/defaults.ts @@ -0,0 +1,12 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { SyncID } from './types'; + +export const defaultSyncIds: SyncID[] = [ + 'initialization', + 'balances', + 'era-stakers', + 'bonded-pools', + 'active-pools', +]; diff --git a/src/controllers/SyncController/index.ts b/src/controllers/SyncController/index.ts index 67baa82887..7ad062c693 100644 --- a/src/controllers/SyncController/index.ts +++ b/src/controllers/SyncController/index.ts @@ -1,18 +1,27 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import { defaultSyncIds } from './defaults'; import type { SyncEvent, SyncID, SyncIDConfig, SyncStatus } from './types'; export class SyncController { // ------------------------------------------------------ // Class members // ------------------------------------------------------ - static syncIds: SyncID[] = []; + + // List of all syncIds currently syncing. NOTE: `initialization` is added by default as the + // network always initializes from initial state. + static syncIds: SyncID[] = defaultSyncIds; // ------------------------------------------------------ // Dispatch sync events // ------------------------------------------------------ + // Dispatch all default syncId events as syncing. + static dispatchAllDefault = () => { + this.syncIds.forEach((id) => this.dispatch(id, 'syncing')); + }; + // Dispatches a new sync event to the document. static dispatch = (id: SyncID, status: SyncStatus) => { const detail: SyncEvent = { @@ -20,20 +29,31 @@ export class SyncController { status, }; + // Whether to dispatch the event. + let dispatch = true; + // Keep class syncIds up to date. - if (status === 'syncing' && !this.syncIds.includes(id)) { - this.syncIds.push(id); + if (status === 'syncing') { + if (this.syncIds.includes(id)) { + // Cancel event if already syncing. + dispatch = false; + } else { + this.syncIds.push(id); + } } - if (status === 'complete' && this.syncIds.includes(id)) { + + if (status === 'complete') { this.syncIds = this.syncIds.filter((syncId) => syncId !== id); } // Dispatch event to UI. - document.dispatchEvent( - new CustomEvent('new-sync-status', { - detail, - }) - ); + if (dispatch) { + document.dispatchEvent( + new CustomEvent('new-sync-status', { + detail, + }) + ); + } }; // Checks if event detailis a valid `new-sync-status` event. diff --git a/src/controllers/SyncController/types.ts b/src/controllers/SyncController/types.ts index ced7533cba..75c3f97b81 100644 --- a/src/controllers/SyncController/types.ts +++ b/src/controllers/SyncController/types.ts @@ -5,6 +5,7 @@ export type SyncID = | 'initialization' | 'balances' | 'era-stakers' + | 'bonded-pools' | 'active-pools'; export interface SyncEvent { diff --git a/src/hooks/useActivePools/index.tsx b/src/hooks/useActivePools/index.tsx index f658c9da59..4ba1508847 100644 --- a/src/hooks/useActivePools/index.tsx +++ b/src/hooks/useActivePools/index.tsx @@ -11,38 +11,32 @@ import type { ActivePoolsProps, ActivePoolsState, } from './types'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useNetwork } from 'contexts/Network'; -export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { +export const useActivePools = ({ onCallback, who }: ActivePoolsProps) => { const { network } = useNetwork(); - const { activeAccount } = useActiveAccounts(); // Stores active pools. - const [activePools, setActivePools] = useState({}); + const [activePools, setActivePools] = useState( + ActivePoolsController.getActivePools(who) + ); const activePoolsRef = useRef(activePools); // Store nominations of active pools. const [poolNominations, setPoolNominations] = - useState({}); + useState( + ActivePoolsController.getPoolNominations(who) + ); const poolNominationsRef = useRef(poolNominations); // Handle report of new active pool data. const newActivePoolCallback = async (e: Event) => { if (isCustomEvent(e) && ActivePoolsController.isValidNewActivePool(e)) { - const { pool, nominations } = e.detail; + const { address, pool, nominations } = e.detail; const { id } = pool; - // Call custom `onCallback` function if provided. - if (typeof onCallback === 'function') { - await onCallback(e.detail); - } - - // Persist to active pools state if this pool is specified in `poolIds`. - if ( - poolIds === '*' || - (Array.isArray(poolIds) && poolIds.includes(String(id))) - ) { + // Persist to active pools state for the specified account. + if (address === who) { const newActivePools = { ...activePoolsRef.current }; newActivePools[id] = pool; setStateWithRef(newActivePools, setActivePools, activePoolsRef); @@ -55,46 +49,49 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { poolNominationsRef ); } + + // Call custom `onCallback` function if provided. + if (typeof onCallback === 'function') { + await onCallback(e.detail); + } } }; - const documentRef = useRef(document); + // Get an active pool. + const getActivePools = (poolId: string) => activePools?.[poolId] || null; - // Bootstrap state on initial render. + // Get an active pool's nominations. + const getPoolNominations = (poolId: string) => + poolNominations?.[poolId] || null; + + // Reset state on network change. useEffect(() => { - const initialActivePools = - poolIds === '*' - ? ActivePoolsController.activePools - : Object.fromEntries( - Object.entries(ActivePoolsController.activePools).filter(([key]) => - poolIds.includes(key) - ) - ); - setStateWithRef(initialActivePools || {}, setActivePools, activePoolsRef); + setStateWithRef({}, setActivePools, activePoolsRef); + setStateWithRef({}, setPoolNominations, poolNominationsRef); + }, [network]); - const initialPoolNominations = - poolIds === '*' - ? ActivePoolsController.poolNominations - : Object.fromEntries( - Object.entries(ActivePoolsController.poolNominations).filter( - ([key]) => poolIds.includes(key) - ) - ); + // Update state on account change. + useEffect(() => { + setStateWithRef( + ActivePoolsController.getActivePools(who), + setActivePools, + activePoolsRef + ); setStateWithRef( - initialPoolNominations, + ActivePoolsController.getPoolNominations(who), setPoolNominations, poolNominationsRef ); - }, [JSON.stringify(poolIds)]); - - // Reset state on active account or network change. - useEffect(() => { - setStateWithRef({}, setActivePools, activePoolsRef); - setStateWithRef({}, setPoolNominations, poolNominationsRef); - }, [network, activeAccount]); + }, [who]); // Listen for new active pool events. + const documentRef = useRef(document); useEventListener('new-active-pool', newActivePoolCallback, documentRef); - return { activePools, poolNominations }; + return { + activePools, + activePoolsRef, + getActivePools, + getPoolNominations, + }; }; diff --git a/src/hooks/useActivePools/types.ts b/src/hooks/useActivePools/types.ts index 84d5d82e8d..c0eaa31af2 100644 --- a/src/hooks/useActivePools/types.ts +++ b/src/hooks/useActivePools/types.ts @@ -4,9 +4,10 @@ import type { Nominations } from 'contexts/Balances/types'; import type { ActivePool } from 'contexts/Pools/ActivePool/types'; import type { DetailActivePool } from 'controllers/ActivePoolsController/types'; +import type { MaybeAddress } from 'types'; export interface ActivePoolsProps { - poolIds: string[] | '*'; + who: MaybeAddress; onCallback?: (detail: DetailActivePool) => Promise; } diff --git a/src/hooks/useSyncing/index.tsx b/src/hooks/useSyncing/index.tsx index b1a6c843a2..f1a19b1fae 100644 --- a/src/hooks/useSyncing/index.tsx +++ b/src/hooks/useSyncing/index.tsx @@ -8,12 +8,12 @@ import type { SyncID, SyncIDConfig } from 'controllers/SyncController/types'; import { isCustomEvent } from 'controllers/utils'; import { useEventListener } from 'usehooks-ts'; -export const useSyncing = (config: SyncIDConfig) => { +export const useSyncing = (config: SyncIDConfig = '*') => { // Retrieve the ids from the config provided. const ids = SyncController.getIdsFromSyncConfig(config); // Keep a record of active sync statuses. - const [syncIds, setSyncIds] = useState([]); + const [syncIds, setSyncIds] = useState(SyncController.syncIds); const syncIdsRef = useRef(syncIds); // Handle new syncing status events. @@ -40,7 +40,16 @@ export const useSyncing = (config: SyncIDConfig) => { } }; - const documentRef = useRef(document); + // Helper to determine if pool membership is syncing. + const poolMembersipSyncing = (): boolean => { + const POOL_SYNC_IDS: SyncID[] = [ + 'initialization', + 'balances', + 'bonded-pools', + 'active-pools', + ]; + return syncIds.some(() => POOL_SYNC_IDS.find((id) => syncIds.includes(id))); + }; // Bootstrap existing sync statuses of interest when hook is mounted. useEffect(() => { @@ -54,7 +63,8 @@ export const useSyncing = (config: SyncIDConfig) => { }, []); // Listen for new sync events. + const documentRef = useRef(document); useEventListener('new-sync-status', newSyncStatusCallback, documentRef); - return { syncing: syncIds.length > 0 }; + return { syncing: syncIds.length > 0, poolMembersipSyncing }; }; diff --git a/src/kits/Buttons/ButtonTab.tsx b/src/kits/Buttons/ButtonTab.tsx index c8c5f82e5e..7cad7dea23 100644 --- a/src/kits/Buttons/ButtonTab.tsx +++ b/src/kits/Buttons/ButtonTab.tsx @@ -14,6 +14,8 @@ export type ButtonTabProps = ComponentBaseWithClassName & title: string; // a badge value can represent the main content of the tab page badge?: string | number; + // whether this tab is acting as a preloader. + asPreloader?: boolean; }; /** @@ -31,17 +33,20 @@ export const ButtonTab = ({ onMouseOver, onMouseMove, onMouseOut, + asPreloader, }: ButtonTabProps) => ( ); diff --git a/src/kits/Buttons/index.scss b/src/kits/Buttons/index.scss index 2ecc25d1e1..721f2ed2a2 100644 --- a/src/kits/Buttons/index.scss +++ b/src/kits/Buttons/index.scss @@ -397,7 +397,6 @@ .btn-tab { @include btn-core; @include btn-layout; - @include btn-disabled; color: var(--text-color-primary); transition: @@ -418,28 +417,61 @@ opacity: 0.9; } - > .badge { - border: 1px solid var(--border-primary-color); + &.canvas { color: var(--text-color-tertiary); - font-size: var(--button-font-size-small); - margin-left: var(--button-spacing-large); + + &.active { + color: var(--text-color-primary); + background: var(--button-tab-canvas-background); + } + } + + &:disabled { + cursor: default; + } + + > span { display: flex; - padding: 0.3rem 0.6rem; align-items: center; - justify-content: center; - border-radius: 0.4rem; - overflow: hidden; - width: fit-content; - max-width: 3rem; + + &.preload { + opacity: 0; + } + + > .badge { + border: 1px solid var(--border-primary-color); + color: var(--text-color-tertiary); + font-size: var(--button-font-size-small); + margin-left: var(--button-spacing-large); + display: flex; + padding: 0.3rem 0.6rem; + align-items: center; + justify-content: center; + border-radius: 0.4rem; + overflow: hidden; + width: fit-content; + max-width: 3rem; + } } &.active { background: var(--button-tab-background); - > .badge { + > span > .badge { border: 1px solid var(--border-secondary-color); } } + + &.preload { + background: var(--shimmer-foreground); + } + + &.canvas { + &.preload { + background: var(--shimmer-foreground); + opacity: 1; + } + } } .btn-tertiary { diff --git a/src/kits/Structure/PageTitle/types.ts b/src/kits/Structure/PageTitle/types.ts index bedc24c1d9..c71624a590 100644 --- a/src/kits/Structure/PageTitle/types.ts +++ b/src/kits/Structure/PageTitle/types.ts @@ -4,6 +4,10 @@ import type { PageTitleTabsProps } from '../PageTitleTabs/types'; export type PageTitleProps = PageTitleTabsProps & { + // tab button class. + tabClassName?: string; + // whether tabs are inline. + inline?: boolean; // title of the page. title?: string; // a button right next to the page title. diff --git a/src/kits/Structure/PageTitleTabs/Wrapper.ts b/src/kits/Structure/PageTitleTabs/Wrapper.ts index d9dcdf0bc1..7137b57580 100644 --- a/src/kits/Structure/PageTitleTabs/Wrapper.ts +++ b/src/kits/Structure/PageTitleTabs/Wrapper.ts @@ -11,6 +11,10 @@ export const Wrapper = styled.section` margin-top: 0.9rem; max-width: 100%; + &.inline { + border-bottom: none; + } + @media (max-width: ${PageWidthMediumThreshold}px) { margin-top: 0.5rem; } diff --git a/src/kits/Structure/PageTitleTabs/index.tsx b/src/kits/Structure/PageTitleTabs/index.tsx index 180de7132a..6d0a9b51bf 100644 --- a/src/kits/Structure/PageTitleTabs/index.tsx +++ b/src/kits/Structure/PageTitleTabs/index.tsx @@ -11,18 +11,38 @@ import { Wrapper } from './Wrapper'; * @name PageTitleTabs * @summary The element in a page title. Inculding the ButtonTab. */ -export const PageTitleTabs = ({ sticky, tabs = [] }: PageTitleProps) => ( - +export const PageTitleTabs = ({ + sticky, + tabs = [], + inline = false, + tabClassName, +}: PageTitleProps) => ( +
{tabs.map( - ({ active, onClick, title, badge }: PageTitleTabProps, i: number) => ( + ( + { + active, + onClick, + title, + badge, + disabled, + asPreloader, + }: PageTitleTabProps, + i: number + ) => ( onClick()} title={title} badge={badge} + disabled={disabled === undefined ? false : disabled} + asPreloader={asPreloader == undefined ? false : asPreloader} /> ) )} diff --git a/src/kits/Structure/PageTitleTabs/types.ts b/src/kits/Structure/PageTitleTabs/types.ts index 431f403527..83022a7732 100644 --- a/src/kits/Structure/PageTitleTabs/types.ts +++ b/src/kits/Structure/PageTitleTabs/types.ts @@ -19,4 +19,8 @@ export interface PageTitleTabProps { onClick: () => void; // a badge that can have a glance at before visting the tab page. badge?: string | number; + // whether the tab button is disabled. + disabled?: boolean; + // whether the tab is acting as a preloader. + asPreloader?: boolean; } diff --git a/src/kits/Structure/Tx/Signer.tsx b/src/kits/Structure/Tx/Signer.tsx new file mode 100644 index 0000000000..4a35574c79 --- /dev/null +++ b/src/kits/Structure/Tx/Signer.tsx @@ -0,0 +1,33 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faPenToSquare, faWarning } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import type { SignerProps } from './types'; +import { SignerWrapper } from './Wrapper'; + +export const Signer = ({ + dangerMessage, + notEnoughFunds, + name, + label, +}: SignerProps) => ( + + + + {label} + + {name} + {notEnoughFunds && ( + + /   + + {dangerMessage} + + )} + +); diff --git a/src/kits/Structure/Tx/Wrapper.ts b/src/kits/Structure/Tx/Wrapper.ts index 88573abb21..927c5010ce 100644 --- a/src/kits/Structure/Tx/Wrapper.ts +++ b/src/kits/Structure/Tx/Wrapper.ts @@ -23,6 +23,10 @@ export const Wrapper = styled.div` background: var(--background-canvas-card); } + &.card { + border-radius: 0.5rem; + } + > section { width: 100%; @@ -31,6 +35,26 @@ export const Wrapper = styled.div` flex-direction: row; align-items: center; + &.col { + flex-direction: column; + margin-top: 0.5rem; + + > div { + width: 100%; + margin-bottom: 0.4rem; + + > div, + > p { + width: 100%; + margin-bottom: 0.4rem; + } + + > div:last-child { + margin-bottom: 0; + } + } + } + > div { display: flex; @@ -81,30 +105,30 @@ export const Wrapper = styled.div` } } } +`; - .sign { - display: flex; - align-items: center; - font-size: 0.9rem; - padding-bottom: 0.5rem; - margin: 0; - - .badge { - border: 1px solid var(--border-secondary-color); - border-radius: 0.45rem; - padding: 0.2rem 0.5rem; - margin-right: 0.75rem; - - > svg { - margin-right: 0.5rem; - } +export const SignerWrapper = styled.p` + display: flex; + align-items: center; + font-size: 0.9rem; + padding-bottom: 0.5rem; + margin: 0; + + .badge { + border: 1px solid var(--border-secondary-color); + border-radius: 0.45rem; + padding: 0.2rem 0.5rem; + margin-right: 0.75rem; + + > svg { + margin-right: 0.5rem; } + } - .not-enough { - margin-left: 0.5rem; - } + .not-enough { + margin-left: 0.5rem; - .danger { + > .danger { color: var(--status-danger-color); } diff --git a/src/kits/Structure/Tx/index.tsx b/src/kits/Structure/Tx/index.tsx index 8c93b9f4b2..5e312d441a 100644 --- a/src/kits/Structure/Tx/index.tsx +++ b/src/kits/Structure/Tx/index.tsx @@ -1,29 +1,10 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faPenToSquare, faWarning } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import type { ReactElement } from 'react'; -import type { DisplayFor } from 'types'; import { Wrapper } from './Wrapper'; import { appendOrEmpty } from '@w3ux/utils'; - -export interface TxProps { - // whether there is margin on top. - margin?: boolean; - // account type for the transaction signing. - label: string; - // account id - name: string; - // whether there is enough funds for the transaction. - notEnoughFunds: boolean; - // warning messgae. - dangerMessage: string; - // signing component. - SignerComponent: ReactElement; - // display for. - displayFor?: DisplayFor; -} +import type { TxProps } from './types'; +import { Signer } from './Signer'; /** * @name Tx @@ -39,25 +20,15 @@ export const Tx = ({ displayFor = 'default', }: TxProps) => ( -
-

- - - {label} - - {name} - {notEnoughFunds && ( - - /   - {' '} - {dangerMessage} - - )} -

+
+
{SignerComponent}
diff --git a/src/kits/Structure/Tx/types.ts b/src/kits/Structure/Tx/types.ts new file mode 100644 index 0000000000..01e5981dac --- /dev/null +++ b/src/kits/Structure/Tx/types.ts @@ -0,0 +1,18 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactElement } from 'react'; +import type { DisplayFor } from 'types'; + +export interface SignerProps { + label: string; + name: string; + notEnoughFunds: boolean; + dangerMessage: string; +} + +export interface TxProps extends SignerProps { + margin?: boolean; + SignerComponent: ReactElement; + displayFor?: DisplayFor; +} diff --git a/src/library/CallToAction/index.tsx b/src/library/CallToAction/index.tsx new file mode 100644 index 0000000000..5a74ed0c7a --- /dev/null +++ b/src/library/CallToAction/index.tsx @@ -0,0 +1,210 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const CallToActionWrapper = styled.div` + --button-border-radius: 2rem; + --button-vertical-space: 1.1rem; + + height: inherit; + width: 100%; + + > .inner { + flex: 1; + display: flex; + flex-direction: row; + margin-top: 0.38rem; + + @media (max-width: 650px) { + flex-wrap: wrap; + } + + > section { + display: flex; + flex-direction: row; + height: inherit; + + @media (max-width: 650px) { + margin-top: var(--button-vertical-space); + flex-grow: 1; + flex-basis: 100%; + + &:nth-child(1) { + margin-top: 0; + } + } + + &: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%; + } + } + } + + &:nth-child(2) { + flex: 1; + + @media (min-width: 651px) { + padding-left: 1rem; + } + } + + &.standalone { + border: none; + padding: 0; + } + + h3 { + line-height: 1.4rem; + } + + .buttons { + border: 0.75px solid var(--border-primary-color); + border-radius: var(--button-border-radius); + display: flex; + flex-wrap: nowrap; + width: 100%; + + @media (max-width: 650px) { + flex-wrap: wrap; + } + + > .button { + height: 3.75rem; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + white-space: nowrap; + overflow: hidden; + transition: filter 0.15s; + + &.primary { + background-color: var(--accent-color-primary); + border-top-left-radius: var(--button-border-radius); + border-bottom-left-radius: var(--button-border-radius); + color: white; + flex-grow: 1; + + &:hover { + filter: brightness(90%); + } + + &.disabled { + background-color: var(--accent-color-pending); + + &:hover { + filter: none; + } + } + + &.pulse { + box-shadow: 0 0 30px 0 var(--accent-color-pending); + transform: scale(1); + animation: pulse 4s infinite; + + @keyframes pulse { + 0% { + transform: scale(0.98); + box-shadow: 0 0 0 0 var(--accent-color-pending); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgb(0 0 0 / 0%); + } + + 100% { + transform: scale(0.98); + box-shadow: 0 0 0 0 rgb(0 0 0 / 0%); + } + } + } + } + + &.secondary { + background-color: var(--button-primary-background); + border-top-right-radius: var(--button-border-radius); + border-bottom-right-radius: var(--button-border-radius); + color: var(--text-color-primary); + + &:hover { + filter: brightness(95%); + } + + &.disabled { + opacity: 0.5; + + &:hover { + filter: none; + } + } + } + + &.standalone { + border-radius: var(--button-border-radius); + flex-grow: 1; + } + + @media (max-width: 650px) { + border-radius: var(--button-border-radius); + margin-top: var(--button-vertical-space); + flex-grow: 1; + flex-basis: 100%; + + &:nth-child(1) { + margin-top: 0; + } + } + + > button { + color: inherit; + height: inherit; + transition: transform 0.25s; + padding: 0 2rem; + display: flex; + align-items: center; + 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; + } + + &:disabled { + cursor: default; + } + + > svg { + margin: 0 0.75rem; + } + } + + &.inactive { + > button { + cursor: default; + } + } + } + } + } + } +`; diff --git a/src/library/Card/Wrappers.ts b/src/library/Card/Wrappers.ts index 87e04050d1..c06f80a5f3 100644 --- a/src/library/Card/Wrappers.ts +++ b/src/library/Card/Wrappers.ts @@ -72,6 +72,7 @@ export const CardHeaderWrapper = styled.div` * Used to separate the main modules throughout the app. */ export const CardWrapper = styled.div` + border: 1px solid transparent; box-shadow: var(--card-shadow); background: var(--background-primary); border-radius: 1.1rem; @@ -82,10 +83,23 @@ export const CardWrapper = styled.div` overflow: hidden; margin-top: 1.4rem; padding: 1.5rem; + transition: border 0.2s; &.canvas { background: var(--background-canvas-card); padding: 1.25rem; + + &.secondary { + padding: 1rem; + + @media (max-width: 1000px) { + background: var(--background-canvas-card); + } + + @media (min-width: 1001px) { + background: var(--background-canvas-card-secondary); + } + } } &.transparent { @@ -98,7 +112,11 @@ export const CardWrapper = styled.div` } &.warning { - border: 1px solid var(--status-warning-color); + border: 1px solid var(--accent-color-secondary); + } + + &.prompt { + border: 1px solid var(--accent-color-pending); } @media (max-width: ${PageWidthMediumThreshold}px) { 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/Form/Bond/BondFeedback.tsx b/src/library/Form/Bond/BondFeedback.tsx index 06ccbfb027..944095dd51 100644 --- a/src/library/Form/Bond/BondFeedback.tsx +++ b/src/library/Form/Bond/BondFeedback.tsx @@ -27,6 +27,7 @@ export const BondFeedback = ({ txFees, maxWidth, syncing = false, + displayFirstWarningOnly = true, }: BondFeedbackProps) => { const { t } = useTranslation('library'); const { @@ -145,6 +146,10 @@ export const BondFeedback = ({ setErrors(newErrors); }; + // If `displayFirstWarningOnly` is set, filter errors to only the first one. + const filteredErrors = + displayFirstWarningOnly && errors.length > 1 ? [errors[0]] : errors; + // update bond on account change useEffect(() => { setBond({ @@ -168,7 +173,7 @@ export const BondFeedback = ({ return ( <> - {errors.map((err, i) => ( + {filteredErrors.map((err, i) => ( ))} diff --git a/src/library/Form/ClaimPermissionInput/index.tsx b/src/library/Form/ClaimPermissionInput/index.tsx index 9915509d6a..ac87b2c324 100644 --- a/src/library/Form/ClaimPermissionInput/index.tsx +++ b/src/library/Form/ClaimPermissionInput/index.tsx @@ -6,48 +6,35 @@ import { useTranslation } from 'react-i18next'; import { TabWrapper, TabsWrapper } from 'library/Filter/Wrappers'; import type { ClaimPermission } from 'contexts/Pools/types'; import type { ClaimPermissionConfig } from '../types'; -import { ActionItem } from 'library/ActionItem'; - -export interface ClaimPermissionInputProps { - current: ClaimPermission | undefined; - permissioned: boolean; - onChange: (value: ClaimPermission | undefined) => void; - disabled?: boolean; -} +import type { ClaimPermissionInputProps } from './types'; export const ClaimPermissionInput = ({ current, - permissioned, onChange, disabled = false, }: ClaimPermissionInputProps) => { const { t } = useTranslation('library'); const claimPermissionConfig: ClaimPermissionConfig[] = [ - { - label: t('allowCompound'), - value: 'PermissionlessCompound', - description: t('allowAnyoneCompound'), - }, { label: t('allowWithdraw'), value: 'PermissionlessWithdraw', description: t('allowAnyoneWithdraw'), }, { - label: t('allowAll'), - value: 'PermissionlessAll', - description: t('allowAnyoneCompoundWithdraw'), + label: t('allowCompound'), + value: 'PermissionlessCompound', + description: t('allowAnyoneCompound'), + }, + { + label: t('permissioned'), + value: 'Permissioned', + description: t('permissionedSubtitle'), }, ]; - // Updated claim permission value - const [selected, setSelected] = useState( - current - ); - - // Permissionless claim enabled. - const [enabled, setEnabled] = useState(permissioned); + // Updated claim permission value. + const [selected, setSelected] = useState(current); const activeTab = claimPermissionConfig.find( ({ value }) => value === selected @@ -60,43 +47,22 @@ export const ClaimPermissionInput = ({ return ( <> - { - // toggle enable claim permission. - setEnabled(val); - - const newClaimPermission = val - ? claimPermissionConfig[0].value - : current === undefined - ? undefined - : 'Permissioned'; - - setSelected(newClaimPermission); - onChange(newClaimPermission); - }} - disabled={disabled} - inactive={disabled} - /> {claimPermissionConfig.map(({ label, value }, i) => ( { setSelected(value); onChange(value); }} + style={{ flexGrow: 1 }} > {label} @@ -104,13 +70,17 @@ export const ClaimPermissionInput = ({
{activeTab ? ( -

{activeTab.description}

+

+ {activeTab.description} +

) : ( -

{t('permissionlessClaimingTurnedOff')}

+

+ {t('permissionlessClaimingTurnedOff')} +

)}
diff --git a/src/library/Form/ClaimPermissionInput/types.ts b/src/library/Form/ClaimPermissionInput/types.ts new file mode 100644 index 0000000000..b4d2de3367 --- /dev/null +++ b/src/library/Form/ClaimPermissionInput/types.ts @@ -0,0 +1,10 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ClaimPermission } from 'contexts/Pools/types'; + +export interface ClaimPermissionInputProps { + current: ClaimPermission; + onChange: (value: ClaimPermission) => void; + disabled?: boolean; +} diff --git a/src/library/Form/Unbond/UnbondFeedback.tsx b/src/library/Form/Unbond/UnbondFeedback.tsx index 6f82845d57..9521af7982 100644 --- a/src/library/Form/Unbond/UnbondFeedback.tsx +++ b/src/library/Form/Unbond/UnbondFeedback.tsx @@ -24,6 +24,7 @@ export const UnbondFeedback = ({ setLocalResize, parentErrors = [], txFees, + displayFirstWarningOnly = true, }: UnbondFeedbackProps) => { const { t } = useTranslation('library'); const { @@ -129,6 +130,10 @@ export const UnbondFeedback = ({ setErrors(newErrors); }; + // If `displayFirstWarningOnly` is set, filter errors to only the first one. + const filteredErrors = + displayFirstWarningOnly && errors.length > 1 ? [errors[0]] : errors; + // update bond on account change useEffect(() => { setBond({ bond: defaultValue }); @@ -148,7 +153,7 @@ export const UnbondFeedback = ({ return ( <> - {errors.map((err, i) => ( + {filteredErrors.map((err, i) => ( ))} diff --git a/src/library/Form/Warning/Wrapper.ts b/src/library/Form/Warning/Wrapper.ts index 43da4488f8..973a61ab0f 100644 --- a/src/library/Form/Warning/Wrapper.ts +++ b/src/library/Form/Warning/Wrapper.ts @@ -4,10 +4,10 @@ import styled from 'styled-components'; export const Wrapper = styled.div` - background: var(--background-warning); - border: 1px solid var(--status-warning-color-transparent); + background: var(--button-primary-background); + border: 1px solid var(--accent-color-secondary); margin: 0.5rem 0; - padding: 0.75rem 0.75rem; + padding: 0.6rem 0.9rem; border-radius: 0.75rem; display: flex; flex-flow: row wrap; @@ -15,11 +15,12 @@ export const Wrapper = styled.div` width: 100%; > h4 { - color: var(--status-warning-color); + color: var(--accent-color-secondary); + font-family: Inter, sans-serif; .icon { - color: var(--status-warning-color); - margin-right: 0.6rem; + color: var(--accent-color-secondary); + margin-right: 0.5rem; } } `; diff --git a/src/library/Form/Warning/index.tsx b/src/library/Form/Warning/index.tsx index 3575164094..c9afb6fd4f 100644 --- a/src/library/Form/Warning/index.tsx +++ b/src/library/Form/Warning/index.tsx @@ -1,16 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import type { WarningProps } from '../types'; import { Wrapper } from './Wrapper'; export const Warning = ({ text }: WarningProps) => ( -

- - {text} -

+

{text}

); diff --git a/src/library/Form/types.ts b/src/library/Form/types.ts index 3222f067f7..186f78672b 100644 --- a/src/library/Form/types.ts +++ b/src/library/Form/types.ts @@ -49,6 +49,7 @@ export interface BondFeedbackProps { setLocalResize?: () => void; txFees: BigNumber; maxWidth?: boolean; + displayFirstWarningOnly?: boolean; } export interface BondInputProps { @@ -70,6 +71,7 @@ export interface UnbondFeedbackProps { parentErrors?: string[]; setLocalResize?: () => void; txFees: BigNumber; + displayFirstWarningOnly?: boolean; } export interface UnbondInputProps { diff --git a/src/library/Graphs/GeoDonut.tsx b/src/library/Graphs/GeoDonut.tsx index 3dd8904e45..4ce00d3d13 100644 --- a/src/library/Graphs/GeoDonut.tsx +++ b/src/library/Graphs/GeoDonut.tsx @@ -76,8 +76,8 @@ export const GeoDonut = ({ { label: title, data, - // We make a gradient of N+2 colors from active to inactive, and we discard both ends - // N is the number of datapoints to plot + // We make a gradient of N+2 colors from active to inactive, and we discard both ends N is + // the number of datapoints to plot. backgroundColor: chroma .scale([backgroundColor, graphColors.inactive[mode]]) .colors(data.length + 1), diff --git a/src/library/Graphs/PayoutBar.tsx b/src/library/Graphs/PayoutBar.tsx index e6d17e6230..9d4fdbcd78 100644 --- a/src/library/Graphs/PayoutBar.tsx +++ b/src/library/Graphs/PayoutBar.tsx @@ -94,6 +94,7 @@ export const PayoutBar = ({ }); return `${dateObj}`; }), + datasets: [ { order: 1, diff --git a/src/library/Headers/Sync.tsx b/src/library/Headers/Sync.tsx index a6f4f63c1c..6c4188a047 100644 --- a/src/library/Headers/Sync.tsx +++ b/src/library/Headers/Sync.tsx @@ -13,8 +13,8 @@ import { useTxMeta } from 'contexts/TxMeta'; import { useSyncing } from 'hooks/useSyncing'; export const Sync = () => { + const { syncing } = useSyncing(); const { pathname } = useLocation(); - const { syncing } = useSyncing('*'); const { pendingNonces } = useTxMeta(); const { payoutsSynced } = usePayouts(); const { pluginEnabled } = usePlugins(); diff --git a/src/library/List/SearchInput.tsx b/src/library/List/SearchInput.tsx index d6a2c414f1..b66f0f8a02 100644 --- a/src/library/List/SearchInput.tsx +++ b/src/library/List/SearchInput.tsx @@ -6,12 +6,14 @@ import { SearchInputWrapper } from '.'; import type { SearchInputProps } from './types'; export const SearchInput = ({ + value, handleChange, placeholder, }: SearchInputProps) => ( ) => handleChange(e)} diff --git a/src/library/List/defaults.ts b/src/library/List/defaults.ts index 0aa87f0e92..9941325af3 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 = 30; -// 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/List/types.ts b/src/library/List/types.ts index a4823568e7..5daa5200c2 100644 --- a/src/library/List/types.ts +++ b/src/library/List/types.ts @@ -22,6 +22,7 @@ export interface PaginationProps { } export interface SearchInputProps { + value: string; handleChange: (e: FormEvent) => void; placeholder: string; } diff --git a/src/library/ListItem/Labels/EraStatus.tsx b/src/library/ListItem/Labels/EraStatus.tsx index b3256cf61d..d407860c9c 100644 --- a/src/library/ListItem/Labels/EraStatus.tsx +++ b/src/library/ListItem/Labels/EraStatus.tsx @@ -10,7 +10,7 @@ import { useSyncing } from 'hooks/useSyncing'; export const EraStatus = ({ noMargin, status, totalStake }: EraStatusProps) => { const { t } = useTranslation('library'); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { unit, units } = useNetwork().networkData; // Fallback to `waiting` status if still syncing. diff --git a/src/library/ListItem/Labels/JoinPool.tsx b/src/library/ListItem/Labels/JoinPool.tsx deleted file mode 100644 index 0bb9e9e6ff..0000000000 --- a/src/library/ListItem/Labels/JoinPool.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import { faCaretRight } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useTranslation } from 'react-i18next'; -import { useOverlay } from 'kits/Overlay/Provider'; - -export const JoinPool = ({ - id, - setActiveTab, -}: { - id: number; - setActiveTab: (t: number) => void; -}) => { - const { t } = useTranslation('library'); - const { openModal } = useOverlay().modal; - - return ( -
- -
- ); -}; diff --git a/src/library/ListItem/Labels/More.tsx b/src/library/ListItem/Labels/More.tsx new file mode 100644 index 0000000000..b0e7682be5 --- /dev/null +++ b/src/library/ListItem/Labels/More.tsx @@ -0,0 +1,54 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCaretRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from 'kits/Overlay/Provider'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import type { BondedPool } from 'contexts/Pools/BondedPools/types'; + +export const More = ({ + pool, + setActiveTab, + disabled, +}: { + pool: BondedPool; + setActiveTab: (t: number) => void; + disabled: boolean; +}) => { + const { t } = useTranslation('tips'); + const { openCanvas } = useOverlay().canvas; + const { startPoolRewardPointsFetch } = usePoolPerformance(); + + const { id, addresses } = pool; + + // Define a unique pool performance data key + const performanceKey = `pool_page_standalone_${id}`; + + return ( +
+ +
+ ); +}; diff --git a/src/library/ListItem/Labels/PoolBonded.tsx b/src/library/ListItem/Labels/PoolBonded.tsx index 71a05e59de..65a20797e8 100644 --- a/src/library/ListItem/Labels/PoolBonded.tsx +++ b/src/library/ListItem/Labels/PoolBonded.tsx @@ -1,78 +1,45 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { capitalizeFirstLetter, planckToUnit, rmCommas } from '@w3ux/utils'; +import { planckToUnit, rmCommas } from '@w3ux/utils'; import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useStaking } from 'contexts/Staking'; -import { ValidatorStatusWrapper } from 'library/ListItem/Wrappers'; -import type { Pool } from 'library/Pool/types'; import { useNetwork } from 'contexts/Network'; +import type { Pool } from 'library/Pool/types'; +import { TooltipTrigger } from '../Wrappers'; +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; export const PoolBonded = ({ pool }: { pool: Pool }) => { const { t } = useTranslation('library'); const { - networkData: { units, unit }, + networkData: { + units, + brand: { token }, + }, } = useNetwork(); - const { getPoolNominationStatusCode, poolsNominations } = useBondedPools(); - const { eraStakers, getNominationsStatusFromTargets } = useStaking(); - const { addresses, points } = pool; - - // get pool targets from nominations meta batch - const nominations = poolsNominations[pool.id]; - const targets = nominations?.targets || []; + const { setTooltipTextAndOpen } = useTooltip(); - // store nomination status in state - const [nominationsStatus, setNominationsStatus] = - useState>(); + const tooltipText = t('bonded'); - // update pool nomination status as nominations metadata becomes available. - // we cannot add effect dependencies here as this needs to trigger - // as soon as the component displays. (upon tab change). - const handleNominationsStatus = () => { - setNominationsStatus( - getNominationsStatusFromTargets(addresses.stash, targets) - ); - }; + const { points } = pool; + const TokenIcon = token; - // recalculate nominations status as app syncs - useEffect(() => { - if ( - targets.length && - nominationsStatus === null && - eraStakers.stakers.length - ) { - handleNominationsStatus(); - } - }); - - // metadata has changed, which means pool items may have been added. - // recalculate nominations status - useEffect(() => { - handleNominationsStatus(); - }, [pool, eraStakers.stakers.length, Object.keys(poolsNominations).length]); - - // calculate total bonded pool amount - const poolBonded = planckToUnit(new BigNumber(rmCommas(points)), units); - - // determine nominations status and display - const nominationStatus = getPoolNominationStatusCode( - nominationsStatus || null - ); + // Format total bonded pool amount. + const bonded = planckToUnit(new BigNumber(rmCommas(points)), units); return ( - -
- {nominationStatus === null || !eraStakers.stakers.length - ? `${t('syncing')}...` - : targets.length - ? capitalizeFirstLetter(t(`${nominationStatus}`) ?? '') - : t('notNominating')} - {' / '} - {t('bonded')}: {poolBonded.decimalPlaces(3).toFormat()} {unit} -
-
+
+ setTooltipTextAndOpen(tooltipText)} + /> + + + {bonded.decimalPlaces(0).toFormat()} +
); }; diff --git a/src/library/ListItem/Labels/PoolNominateStatus.tsx b/src/library/ListItem/Labels/PoolNominateStatus.tsx new file mode 100644 index 0000000000..919f98e5b1 --- /dev/null +++ b/src/library/ListItem/Labels/PoolNominateStatus.tsx @@ -0,0 +1,70 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { capitalizeFirstLetter } from '@w3ux/utils'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useStaking } from 'contexts/Staking'; +import { PoolStatusWrapper } from 'library/ListItem/Wrappers'; +import type { Pool } from 'library/Pool/types'; + +export const PoolNominateStatus = ({ pool }: { pool: Pool }) => { + const { t } = useTranslation('library'); + const { getPoolNominationStatusCode, poolsNominations } = useBondedPools(); + const { eraStakers, getNominationsStatusFromTargets } = useStaking(); + const { addresses } = pool; + + // get pool targets from nominations meta batch + const nominations = poolsNominations[pool.id]; + const targets = nominations?.targets || []; + + // store nomination status in state + const [nominationsStatus, setNominationsStatus] = + useState>(); + + // update pool nomination status as nominations metadata becomes available. + // we cannot add effect dependencies here as this needs to trigger + // as soon as the component displays. (upon tab change). + const handleNominationsStatus = () => { + setNominationsStatus( + getNominationsStatusFromTargets(addresses.stash, targets) + ); + }; + + // recalculate nominations status as app syncs + useEffect(() => { + if ( + targets.length && + nominationsStatus === null && + eraStakers.stakers.length + ) { + handleNominationsStatus(); + } + }); + + // metadata has changed, which means pool items may have been added. + // recalculate nominations status + useEffect(() => { + handleNominationsStatus(); + }, [pool, eraStakers.stakers.length, Object.keys(poolsNominations).length]); + + // determine nominations status and display + const nominationStatus = getPoolNominationStatusCode( + nominationsStatus || null + ); + + return ( + +

+ + {nominationStatus === null || !eraStakers.stakers.length + ? `${t('syncing')}...` + : targets.length + ? capitalizeFirstLetter(t(`${nominationStatus}`) ?? '') + : t('notNominating')} + +

+
+ ); +}; diff --git a/src/library/ListItem/Wrappers.ts b/src/library/ListItem/Wrappers.ts index e5ae334a3b..72b8032ed9 100644 --- a/src/library/ListItem/Wrappers.ts +++ b/src/library/ListItem/Wrappers.ts @@ -12,8 +12,8 @@ export const Wrapper = styled.div` &.member { --height-bottom-row: 2.75rem; } - &.pool-join { - --height-bottom-row: 7.5rem; + &.pool-more { + --height-bottom-row: 5.75rem; } --height-total: calc(var(--height-top-row) + var(--height-bottom-row)); @@ -63,9 +63,14 @@ export const Wrapper = styled.div` &.bottom { height: var(--height-bottom-row); + &.pools { + align-items: flex-start; + } + &.lg { display: flex; align-items: center; + > div { &:first-child { flex-grow: 1; @@ -94,6 +99,10 @@ export const Labels = styled.div` padding: 0 0 0 0.25rem; height: inherit; + &.yMargin { + margin-bottom: 0.9rem; + } + button { background: var(--shimmer-foreground); padding: 0 0.1rem; @@ -129,16 +138,13 @@ export const Labels = styled.div` align-items: center; justify-content: center; font-size: inherit; + margin: 0 0.4em; - @media (min-width: ${SmallFontSizeMaxWidth}px) { - margin: 0 0.35rem; - &.pool { - margin: 0 0.45rem; - } + > .token { + margin-right: 0.25rem; } - &.button-with-text { - margin-right: 0; + margin: 0.25rem 0 0 0; button { color: var(--accent-color-secondary); @@ -155,6 +161,12 @@ export const Labels = styled.div` &:hover { opacity: 1; } + + &:disabled { + &:hover { + opacity: var(--opacity-disabled); + } + } > svg { margin-left: 0.3rem; } @@ -247,6 +259,43 @@ export const ValidatorStatusWrapper = styled.div<{ } `; +export const PoolStatusWrapper = styled.div<{ + $status: string; +}>` + h4, + h5 { + display: flex; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + h4 { + color: var(--text-color-tertiary); + font-size: 1rem; + + padding-top: ${(props) => + props.$status === 'active' ? '0.15rem' : '0.25rem'}; + + > span { + color: ${(props) => + props.$status === 'active' + ? 'var(--status-success-color)' + : 'var(--text-color-tertiary)'}; + + border: 0.75px solid + ${(props) => + props.$status === 'active' + ? 'var(--status-success-color)' + : 'transparent'}; + + padding: ${(props) => (props.$status === 'active' ? '0 0.5rem' : '0')}; + border-radius: 0.3rem; + opacity: ${(props) => (props.$status === 'active' ? 1 : 0.6)}; + } + } +`; + export const SelectWrapper = styled.button` background: var(--background-input); margin: 0 0.75rem 0 0.25rem; @@ -305,13 +354,13 @@ export const TooltipTrigger = styled.div` export const ValidatorPulseWrapper = styled.div` border: 1px solid var(--grid-color-primary); border-radius: 0.25rem; - height: 3.2rem; display: flex; align-items: center; - width: 100%; - max-width: 13.5rem; position: relative; padding: 0.15rem 0; + height: 3.2rem; + width: 100%; + max-width: 13.5rem; &.canvas { border: 1px solid var(--grid-color-secondary); diff --git a/src/library/Loader/Announcement.tsx b/src/library/Loader/Announcement.tsx index 3337eebac8..f520e4dabf 100644 --- a/src/library/Loader/Announcement.tsx +++ b/src/library/Loader/Announcement.tsx @@ -1,7 +1,7 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { LoaderWrapper } from './Wrapper'; +import { LoaderWrapper } from './Wrappers'; export const Announcement = () => ( diff --git a/src/library/Loader/CallToAction.tsx b/src/library/Loader/CallToAction.tsx new file mode 100644 index 0000000000..5e1bbfbaeb --- /dev/null +++ b/src/library/Loader/CallToAction.tsx @@ -0,0 +1,15 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { LoaderWrapper } from './Wrappers'; + +export const CallToActionLoader = () => ( + +); diff --git a/src/library/Loader/Wrapper.ts b/src/library/Loader/Wrappers.ts similarity index 85% rename from src/library/Loader/Wrapper.ts rename to src/library/Loader/Wrappers.ts index d350113cb1..983dc0d62f 100644 --- a/src/library/Loader/Wrapper.ts +++ b/src/library/Loader/Wrappers.ts @@ -13,11 +13,11 @@ export const LoaderWrapper = styled.div` var(--shimmer-foreground) 100% ); background-repeat: no-repeat; - background-size: 600px 104px; + background-size: 60% 100%; animation-duration: 1.5s; animation-fill-mode: forwards; animation-iteration-count: infinite; - animation-name: shimmer; + animation-name: shimmer-loader; animation-timing-function: linear; opacity: 0.1; @@ -25,12 +25,12 @@ export const LoaderWrapper = styled.div` display: inline-block; position: relative; - @keyframes shimmer { + @keyframes shimmer-loader { 0% { background-position: 0px 0; } 100% { - background-position: 150% 0; + background-position: 200% 0; } } `; diff --git a/src/library/Nominations/index.tsx b/src/library/Nominations/index.tsx index 98d0398fb2..07ed808a14 100644 --- a/src/library/Nominations/index.tsx +++ b/src/library/Nominations/index.tsx @@ -41,7 +41,7 @@ export const Nominations = ({ modal: { openModal }, canvas: { openCanvas }, } = useOverlay(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(['balances', 'era-stakers']); const { getNominations } = useBalances(); const { isFastUnstaking } = useUnstaking(); const { formatWithPrefs } = useValidators(); @@ -77,7 +77,7 @@ export const Nominations = ({ // Determine whether buttons are disabled. const btnsDisabled = (!isPool && inSetup()) || - syncing || + (!isPool && syncing) || isReadOnlyAccount(activeAccount) || poolDestroying || isFastUnstaking; @@ -131,7 +131,7 @@ export const Nominations = ({ )}
- {syncing ? ( + {!isPool && syncing ? ( {`${t('nominate.syncing')}...`} ) : !nominator ? ( {t('nominate.notNominating')}. @@ -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 f461ac0b71..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 ( @@ -129,7 +132,7 @@ export const RewardsGraph = ({ points = [], syncing }: RewardsGraphProps) => { key={`line_coord_${index}`} strokeWidth={5} opacity={1} - stroke="var(--accent-color-tertiary)" + stroke="var(--accent-color-transparent)" x1={x1} y1={y1} x2={x2} @@ -146,7 +149,7 @@ export const RewardsGraph = ({ points = [], syncing }: RewardsGraphProps) => { stroke={ zero ? 'var(--text-color-tertiary)' - : 'var(--accent-color-secondary)' + : 'var(--accent-color-primary)' } x1={x1} y1={y1} diff --git a/src/library/Pool/index.tsx b/src/library/Pool/index.tsx index facef6ae8e..5efc8c7f38 100644 --- a/src/library/Pool/index.tsx +++ b/src/library/Pool/index.tsx @@ -1,168 +1,62 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faCopy } from '@fortawesome/free-regular-svg-icons'; -import { faBars, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import type { MouseEvent as ReactMouseEvent } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useMenu } from 'contexts/Menu'; -import type { NotificationText } from 'controllers/NotificationsController/types'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useValidators } from 'contexts/Validators/ValidatorEntries'; import { usePoolCommission } from 'hooks/usePoolCommission'; import { FavoritePool } from 'library/ListItem/Labels/FavoritePool'; -import { PoolBonded } from 'library/ListItem/Labels/PoolBonded'; +import { PoolNominateStatus } from 'library/ListItem/Labels/PoolNominateStatus'; import { PoolCommission } from 'library/ListItem/Labels/PoolCommission'; import { PoolIdentity } from 'library/ListItem/Labels/PoolIdentity'; import { Labels, Separator, Wrapper } from 'library/ListItem/Wrappers'; import { usePoolsTabs } from 'pages/Pools/Home/context'; -import { useOverlay } from 'kits/Overlay/Provider'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -import { JoinPool } from '../ListItem/Labels/JoinPool'; +import { More } from '../ListItem/Labels/More'; import { Members } from '../ListItem/Labels/Members'; import { PoolId } from '../ListItem/Labels/PoolId'; import type { PoolProps } from './types'; -import { Rewards } from './Rewards'; -import { NotificationsController } from 'controllers/NotificationsController'; -import type { MenuItem } from 'contexts/Menu/types'; -import { useBalances } from 'contexts/Balances'; import { useSyncing } from 'hooks/useSyncing'; -import { MenuList } from 'library/Menu/List'; +import { PoolBonded } from 'library/ListItem/Labels/PoolBonded'; export const Pool = ({ pool }: PoolProps) => { - const { t } = useTranslation('library'); - const { memberCounter, addresses, id, state } = pool; - const { openMenu, open } = useMenu(); - const { validators } = useValidators(); + const { memberCounter, addresses, id } = pool; const { setActiveTab } = usePoolsTabs(); - const { openModal } = useOverlay().modal; - const { getPoolMembership } = useBalances(); - const { poolsNominations } = useBondedPools(); - const { activeAccount } = useActiveAccounts(); const { syncing } = useSyncing(['active-pools']); - const { isReadOnlyAccount } = useImportedAccounts(); const { getCurrentCommission } = usePoolCommission(); - const membership = getPoolMembership(activeAccount); const currentCommission = getCurrentCommission(id); - // get metadata from pools metabatch - const nominations = poolsNominations[pool.id]; - - // get pool targets from nominations metadata - const targets = nominations?.targets || []; - - // extract validator entries from pool targets - const targetValidators = validators.filter(({ address }) => - targets.includes(address) - ); - - // copy address notification - const notificationCopyAddress = ( - key: 'stash' | 'reward' - ): NotificationText | null => - addresses[key] == null - ? null - : { - title: t('addressCopiedToClipboard'), - subtitle: addresses[key], - }; - - // Consruct pool menu items. - const menuItems: MenuItem[] = []; - - // Add view pool nominations button to menu - menuItems.push({ - icon: , - title: `${t('viewPoolNominations')}`, - cb: () => { - openModal({ - key: 'PoolNominations', - options: { - nominator: addresses.stash, - targets: targetValidators, - }, - }); - }, - }); - - // add copy pool stash address button to menu - menuItems.push({ - icon: , - title: t('copyPoolAddress', { type: 'Stash' }), - cb: () => { - const notification = notificationCopyAddress('stash'); - if (notification) { - navigator.clipboard.writeText(addresses.stash); - NotificationsController.emit(notification); - } - }, - }); - - // add copy pool reward address button to menu - menuItems.push({ - icon: , - title: t('copyPoolAddress', { type: 'Reward' }), - cb: () => { - const notification = notificationCopyAddress('reward'); - if (notification) { - navigator.clipboard.writeText(addresses.reward); - NotificationsController.emit(notification); - } - }, - }); - - // Handler for opening menu. - const toggleMenu = (ev: ReactMouseEvent) => { - if (!open) { - openMenu(ev, ); - } - }; - - const displayJoin = - !syncing && - state === 'Open' && - !membership && - !isReadOnlyAccount(activeAccount) && - activeAccount; - return ( - +
-
- -
-
+
- +   +
- + {currentCommission > 0 && ( )} + + + + + - - {displayJoin && ( - - - - )}
diff --git a/src/library/PoolList/Default.tsx b/src/library/PoolList/index.tsx similarity index 69% rename from src/library/PoolList/Default.tsx rename to src/library/PoolList/index.tsx index 19d01cc3e6..ebff45c5a4 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,99 @@ import { usePoolList } from './context'; import type { PoolListProps } from './types'; import type { BondedPool } from 'contexts/Pools/BondedPools/types'; import { useSyncing } from 'hooks/useSyncing'; +import { useApi } from 'contexts/Api'; +import { useEffectIgnoreInitial } from '@w3ux/hooks'; 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 { syncing } = useSyncing('*'); + const { mode } = useTheme(); + const { activeEra } = useApi(); + const { syncing } = useSyncing(); const { applyFilter } = usePoolFilters(); const { listFormat, setListFormat } = usePoolList(); - const { getFilters, setMultiFilters, getSearchTerm, setSearchTerm } = - useFilters(); const { poolSearchFilter, poolsNominations } = useBondedPools(); + const { getFilters, getSearchTerm, setSearchTerm } = useFilters(); 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 || []); - - // 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); - }; + // Manipulated pool list items after filtering. + const [listPools, setListPools] = useState(filterPoolList()); - // pagination - const totalPages = Math.ceil(listPools.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); + // Whether this the initial render. + const [synced, setSynced] = useState(false); - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + // Handle Pagination. + const totalPages = Math.ceil(listPools.length / poolsPerPage); + const pageEnd = page * poolsPerPage - 1; + const pageStart = pageEnd - (poolsPerPage - 1); - // get throttled subset or entire list - const poolsToDisplay = disableThrottle - ? listPools - : listPools.slice(pageStart).slice(0, listItemsPerPage); + // Get paged subset of list items. + const poolsToDisplay = listPools.slice(pageStart).slice(0, poolsPerPage); - // 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); - } + // Handle filter / order update + 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); + if (JSON.stringify(pools) !== JSON.stringify(poolsDefault) && synced) { + resetPoolList(); } - }, [pools]); - - // Configure pool list when network is ready to fetch. - useEffect(() => { - if (isReady && isNotZero(activeEra.index) && !fetched) { - setupPoolList(); - } - }, [isReady, fetched, activeEra.index]); - - // Render throttling. Only render a batch of pools at a time. - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); + }, [JSON.stringify(pools)]); // List ui changes / validator changes trigger re-render of list. useEffect(() => { @@ -168,21 +135,17 @@ 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 ( {allowSearch && poolsDefault.length > 0 && ( @@ -212,7 +175,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/PoolSync/Bar.tsx b/src/library/PoolSync/Bar.tsx new file mode 100644 index 0000000000..bd9788f948 --- /dev/null +++ b/src/library/PoolSync/Bar.tsx @@ -0,0 +1,36 @@ +// 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'; +import type { PoolRewardPointsKey } from 'contexts/Pools/PoolPerformance/types'; + +export const PoolSyncBar = ({ + performanceKey, +}: { + performanceKey: PoolRewardPointsKey; +}) => { + const { getPoolPerformanceTask } = usePoolPerformance(); + + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask(performanceKey); + + // 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 ( +
+
+ +
+
+ ); +}; diff --git a/src/library/PoolSync/Loader.ts b/src/library/PoolSync/Loader.ts new file mode 100644 index 0000000000..80e7bb6e31 --- /dev/null +++ b/src/library/PoolSync/Loader.ts @@ -0,0 +1,60 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const StyledLoader = styled.div` + height: 0.8rem; + margin-left: 1.6rem; + aspect-ratio: 5; + --_g: no-repeat + radial-gradient(farthest-side, var(--loader-color, 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); + } + } +`; diff --git a/src/library/PoolSync/index.tsx b/src/library/PoolSync/index.tsx new file mode 100644 index 0000000000..e7fd7c0b23 --- /dev/null +++ b/src/library/PoolSync/index.tsx @@ -0,0 +1,37 @@ +// 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'; +import type { PoolRewardPointsKey } from 'contexts/Pools/PoolPerformance/types'; + +export const PoolSync = ({ + label, + performanceKey, +}: { + label?: string; + performanceKey: PoolRewardPointsKey; +}) => { + const { getPoolPerformanceTask } = usePoolPerformance(); + + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask(performanceKey); + + 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()}%{label && ` ${label}`} + + ); +}; diff --git a/src/library/Prompt/Wrappers.ts b/src/library/Prompt/Wrappers.ts index 3a16c2c50a..ab23592d5a 100644 --- a/src/library/Prompt/Wrappers.ts +++ b/src/library/Prompt/Wrappers.ts @@ -89,7 +89,7 @@ export const TitleWrapper = styled.div` align-items: center; padding: 0 0.5rem; - button { + > button { padding: 0; } diff --git a/src/library/SideMenu/Main.tsx b/src/library/SideMenu/Main.tsx index 46d7471cd6..896ec6e485 100644 --- a/src/library/SideMenu/Main.tsx +++ b/src/library/SideMenu/Main.tsx @@ -24,8 +24,8 @@ import { useSyncing } from 'hooks/useSyncing'; export const Main = () => { const { t, i18n } = useTranslation('base'); + const { syncing } = useSyncing(); const { pathname } = useLocation(); - const { syncing } = useSyncing('*'); const { networkData } = useNetwork(); const { getBondedAccount } = useBonded(); const { accounts } = useImportedAccounts(); @@ -33,8 +33,6 @@ export const Main = () => { const { activeAccount } = useActiveAccounts(); const { inSetup: inNominatorSetup, addressDifferentToStash } = useStaking(); const { - onNominatorSetup, - onPoolSetup, getPoolSetupPercent, getNominatorSetupPercent, }: SetupContextInterface = useSetup(); @@ -75,7 +73,6 @@ export const Main = () => { // configure Stake action const staking = !inNominatorSetup(); const warning = !syncing && controllerDifferentToStash; - const setupPercent = getNominatorSetupPercent(activeAccount); if (staking) { pages[i].action = { @@ -90,19 +87,11 @@ export const Main = () => { status: 'warning', }; } - if (!staking && (onNominatorSetup || setupPercent > 0)) { - pages[i].action = { - type: 'text', - status: 'warning', - text: `${setupPercent}%`, - }; - } } if (uri === `${import.meta.env.BASE_URL}pools`) { // configure Pools action const inPool = membership; - const setupPercent = getPoolSetupPercent(activeAccount); if (inPool) { pages[i].action = { @@ -111,13 +100,6 @@ export const Main = () => { text: t('active'), }; } - if (!inPool && (setupPercent > 0 || onPoolSetup)) { - pages[i].action = { - type: 'text', - status: 'warning', - text: `${setupPercent}%`, - }; - } } i++; } @@ -137,8 +119,6 @@ export const Main = () => { getNominatorSetupPercent(activeAccount), getPoolSetupPercent(activeAccount), i18n.resolvedLanguage, - onNominatorSetup, - onPoolSetup, ]); // remove pages that network does not support diff --git a/src/library/SideMenu/Primary/Wrappers.ts b/src/library/SideMenu/Primary/Wrappers.ts index 083e558b53..7fc18bdb2d 100644 --- a/src/library/SideMenu/Primary/Wrappers.ts +++ b/src/library/SideMenu/Primary/Wrappers.ts @@ -27,7 +27,7 @@ export const Wrapper = styled(motion.div)` border: 1px solid var(--accent-color-primary); } &.warning { - border: 1px solid var(--status-warning-color); + border: 1px solid var(--accent-color-secondary); } } @@ -68,8 +68,8 @@ export const Wrapper = styled(motion.div)` border: 1px solid var(--accent-color-primary); } &.warning { - color: var(--status-warning-color); - border: 1px solid var(--status-warning-color-transparent); + color: var(--accent-color-secondary); + border: 1px solid var(--accent-color-secondary); } border-radius: 0.5rem; padding: 0.15rem 0.5rem; @@ -82,7 +82,7 @@ export const Wrapper = styled(motion.div)` } &.warning { svg { - color: var(--status-warning-color); + color: var(--accent-color-secondary); } } &.minimised { diff --git a/src/library/Stat/index.tsx b/src/library/Stat/index.tsx index 4591a12463..3e5bc7efce 100644 --- a/src/library/Stat/index.tsx +++ b/src/library/Stat/index.tsx @@ -24,6 +24,7 @@ export const Stat = ({ helpKey, icon, copy, + dimmed = false, type = 'string', buttonType = 'primary', }: StatProps) => { @@ -81,7 +82,10 @@ export const Stat = ({ } return ( - +

{label} {helpKey !== undefined ? ( diff --git a/src/library/Stat/types.ts b/src/library/Stat/types.ts index 4ab545dbbb..2cb0dbd3ea 100644 --- a/src/library/Stat/types.ts +++ b/src/library/Stat/types.ts @@ -9,6 +9,7 @@ export interface StatProps { stat: AnyJson; type?: string; buttons?: AnyJson[]; + dimmed?: boolean; helpKey: string; icon?: IconProp; buttonType?: string; diff --git a/src/library/StatusLabel/index.tsx b/src/library/StatusLabel/index.tsx index 7c1280f741..25b31e88f0 100644 --- a/src/library/StatusLabel/index.tsx +++ b/src/library/StatusLabel/index.tsx @@ -22,9 +22,9 @@ export const StatusLabel = ({ status = 'sync_or_setup', }: StatusLabelProps) => { const { openHelp } = useHelp(); + const { syncing } = useSyncing(); const { plugins } = usePlugins(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); diff --git a/src/library/SubmitTx/ButtonSubmitLarge.tsx b/src/library/SubmitTx/ButtonSubmitLarge.tsx new file mode 100644 index 0000000000..2fd645bf97 --- /dev/null +++ b/src/library/SubmitTx/ButtonSubmitLarge.tsx @@ -0,0 +1,38 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CallToActionWrapper } from 'library/CallToAction'; +import type { ButtonSubmitLargeProps } from './types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { appendOrEmpty } from '@w3ux/utils'; + +export const ButtonSubmitLarge = ({ + disabled, + onSubmit, + submitText, + icon, + iconTransform, + pulse, +}: ButtonSubmitLargeProps) => ( + +
+
+
+
+ +
+
+
+
+
+); diff --git a/src/library/SubmitTx/Default.tsx b/src/library/SubmitTx/Default.tsx index 3c9632d28e..f101ceaa91 100644 --- a/src/library/SubmitTx/Default.tsx +++ b/src/library/SubmitTx/Default.tsx @@ -8,6 +8,8 @@ import { EstimatedTxFee } from 'library/EstimatedTxFee'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import type { SubmitProps } from './types'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from './ButtonSubmitLarge'; +import { appendOrEmpty } from '@w3ux/utils'; export const Default = ({ onSubmit, @@ -26,22 +28,35 @@ export const Default = ({ submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; return ( -
-
- + <> +
+
+ +
+
+ {buttons} + {displayFor !== 'card' && ( + onSubmit(customEvent)} + disabled={disabled} + pulse={!disabled} + /> + )} +
-
- {buttons} - onSubmit(customEvent)} + {displayFor === 'card' && ( + onSubmit(customEvent)} + submitText={submitText || ''} + icon={faArrowAltCircleUp} pulse={!disabled} /> -
-
+ )} + ); }; diff --git a/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx b/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx index 04a9fb1500..4f95c80416 100644 --- a/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx +++ b/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx @@ -11,6 +11,7 @@ import { getLedgerApp } from 'contexts/Hardware/Utils'; import { useNetwork } from 'contexts/Network'; import { useTxMeta } from 'contexts/TxMeta'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from 'library/SubmitTx/ButtonSubmitLarge'; import type { LedgerSubmitProps } from 'library/SubmitTx/types'; import { useTranslation } from 'react-i18next'; @@ -73,7 +74,7 @@ export const Submit = ({ // Button icon. const icon = !integrityChecked ? faUsb : faSquarePen; - return ( + return displayFor !== 'card' ? ( + ) : ( + ); }; diff --git a/src/library/SubmitTx/ManualSign/Ledger/index.tsx b/src/library/SubmitTx/ManualSign/Ledger/index.tsx index 3011f06e19..1b2701cb0f 100644 --- a/src/library/SubmitTx/ManualSign/Ledger/index.tsx +++ b/src/library/SubmitTx/ManualSign/Ledger/index.tsx @@ -19,6 +19,7 @@ import { getLedgerApp } from 'contexts/Hardware/Utils'; import type { SubmitProps } from '../../types'; import { Submit } from './Submit'; import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { appendOrEmpty } from '@w3ux/utils'; export const Ledger = ({ uid, @@ -133,7 +134,9 @@ export const Ledger = ({
)} -
+
{valid ? (

diff --git a/src/library/SubmitTx/ManualSign/Vault/index.tsx b/src/library/SubmitTx/ManualSign/Vault/index.tsx index 5160640ba2..94a5a1d4be 100644 --- a/src/library/SubmitTx/ManualSign/Vault/index.tsx +++ b/src/library/SubmitTx/ManualSign/Vault/index.tsx @@ -11,6 +11,8 @@ import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import type { SubmitProps } from '../../types'; import { SignPrompt } from './SignPrompt'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from 'library/SubmitTx/ButtonSubmitLarge'; +import { appendOrEmpty } from '@w3ux/utils'; export const Vault = ({ onSubmit, @@ -30,38 +32,51 @@ export const Vault = ({ const disabled = submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; + // Format submit button based on whether signature currently exists or submission is ongoing. + let buttonText: string; + let buttonOnClick: () => void; + let buttonDisabled: boolean; + let buttonPulse: boolean; + + if (getTxSignature() !== null || submitting) { + buttonText = submitText || ''; + buttonOnClick = onSubmit; + buttonDisabled = disabled; + buttonPulse = !(!valid || promptStatus !== 0); + } else { + buttonText = promptStatus === 0 ? t('sign') : t('signing'); + buttonOnClick = async () => { + openPromptWith(, 'small'); + }; + buttonDisabled = disabled || promptStatus !== 0; + buttonPulse = !disabled || promptStatus === 0; + } + return ( -

+
{valid ?

{t('submitTransaction')}

:

...

}
{buttons} - {getTxSignature() !== null || submitting ? ( + {displayFor !== 'card' ? ( onSubmit()} - disabled={disabled} - pulse={!(!valid || promptStatus !== 0)} + onClick={() => buttonOnClick()} + pulse={buttonPulse} /> ) : ( - { - openPromptWith( - , - 'small' - ); - }} - disabled={disabled || promptStatus !== 0} - pulse={!disabled || promptStatus === 0} + )}
diff --git a/src/library/SubmitTx/types.ts b/src/library/SubmitTx/types.ts index c8325981c0..420de34af0 100644 --- a/src/library/SubmitTx/types.ts +++ b/src/library/SubmitTx/types.ts @@ -1,6 +1,7 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; import type { ReactNode } from 'react'; import type { DisplayFor, MaybeAddress } from 'types'; @@ -34,3 +35,12 @@ export interface LedgerSubmitProps { disabled: boolean; submitText?: string; } + +export interface ButtonSubmitLargeProps { + disabled: boolean; + onSubmit: () => void; + submitText: string; + icon?: IconProp; + iconTransform?: string; + pulse: boolean; +} diff --git a/src/library/ValidatorList/ValidatorItem/Nomination.tsx b/src/library/ValidatorList/ValidatorItem/Nomination.tsx index 7ca4be2327..0d0236637e 100644 --- a/src/library/ValidatorList/ValidatorItem/Nomination.tsx +++ b/src/library/ValidatorList/ValidatorItem/Nomination.tsx @@ -43,13 +43,15 @@ export const Nomination = ({ {toggleFavorites && } - + {displayFor !== 'canvas' && ( + + )}
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 9c967dca10..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 { @@ -78,7 +77,7 @@ export const ValidatorListInner = ({ } = useFilters(); const { mode } = useTheme(); const listProvider = useList(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { isReady, activeEra } = useApi(); const { activeAccount } = useActiveAccounts(); const { setModalResize } = useOverlay().modal; @@ -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,13 +298,14 @@ export const ValidatorListInner = ({ // Handle modal resize on list format change. useEffect(() => { maybeHandleModalResize(); - }, [listFormat, renderIteration, validators, page]); + }, [listFormat, validators, page]); return ( {allowSearch && ( 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/library/WithdrawPrompt/index.tsx b/src/library/WithdrawPrompt/index.tsx index 3696f05e1c..ad1021a051 100644 --- a/src/library/WithdrawPrompt/index.tsx +++ b/src/library/WithdrawPrompt/index.tsx @@ -18,17 +18,21 @@ import { useErasToTimeLeft } from 'hooks/useErasToTimeLeft'; import { useApi } from 'contexts/Api'; import { useTranslation } from 'react-i18next'; import type { BondFor } from 'types'; +import { useActivePool } from 'contexts/Pools/ActivePool'; export const WithdrawPrompt = ({ bondFor }: { bondFor: BondFor }) => { const { t } = useTranslation('modals'); const { mode } = useTheme(); const { consts } = useApi(); + const { activePool } = useActivePool(); const { openModal } = useOverlay().modal; const { colors } = useNetwork().networkData; + const { syncing } = useSyncing(['balances']); const { activeAccount } = useActiveAccounts(); const { erasToSeconds } = useErasToTimeLeft(); const { getTransferOptions } = useTransferOptions(); + const { state } = activePool?.bondedPool || {}; const { bondDuration } = consts; const allTransferOptions = getTransferOptions(activeAccount); @@ -49,6 +53,9 @@ export const WithdrawPrompt = ({ bondFor }: { bondFor: BondFor }) => { const displayPrompt = totalUnlockChunks > 0; return ( + /* NOTE: ClosurePrompts is a component that displays a prompt to the user when a pool is being + destroyed. */ + state !== 'Destroying' && displayPrompt && ( diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json index 09b535ee24..77449b5fcc 100644 --- a/src/locale/cn/library.json +++ b/src/locale/cn/library.json @@ -9,10 +9,12 @@ "activePools": "活跃提名池", "activeValidator": "活跃验证人", "activeValidators": "活跃验证人", + "activelyNominating": "活跃提名中", "add": "添加", "addFromFavorites": "从收藏夹添加", "address": "地址", "addressCopiedToClipboard": "复制到剪贴板的地址", + "addresses": "地址", "all": "全部", "allowAll": "允许所有", "allowAnyoneCompound": "允许任何人代表您复利收益", @@ -21,22 +23,28 @@ "allowCompound": "允许复利", "allowWithdraw": "允许取出收益", "alreadyImported": "地址已导入", + "analyzingPoolPerformance": "分析提名池性能", "asAPoolMember": "作为提名池成员", "asThePoolDepositor": "作为提名池存款人", "atLeast": "质押金最低为", + "autoSelected": "己自动选定", "available": "可用", "backToMethods": "返回方案选择", "backToScan": "回到扫描", + "blocked": "己关闭", "blockedNominations": "己冻结提名", "blockingNominations": "冻结提名中", "bond": "质押", "bondAmountDecimals": "质押金额最多只能有 {{units}}个小数位", "bondDecimalsError": "质押金额能最多有 {{units}} 位点数", "bonded": "己质押", + "browseValidators": "浏览验证人", "cancel": "取消", "cancelled": "已取消", + "chooseAnotherPool": "选择另一个池", "chooseValidators": "最多能选择 {{maxNominations}} 个验证人。", "chooseValidators2": "自动生成提名或手动加入提名", + "claimSetting": "申领设置", "clear": "清除", "clearSelection": "清除选择", "clickToReload": "重新加载", @@ -65,6 +73,7 @@ "displayingValidators": "正在显示 {{count}} 个验证人", "done": "完成", "enablePermissionlessClaiming": "启用己许可申领", + "era": "Era", "eraPoints": "Era 点数", "errorUnknown": "抱歉,页面出现点小问题哦", "errorWithTransaction": "交易出错", @@ -122,6 +131,7 @@ "nominate": "提名", "nominateActive": "激活", "nominateInactive": "未激活", + "nominations": "提名", "nominationsReverted": "已恢复原来提名", "nominator": "提名人", "notEnough": "不足", @@ -143,6 +153,8 @@ "payoutAccount": "收益到账账户", "payoutAddress": "收益到账地址", "pending": "待定中", + "permissioned": "已获许可", + "permissionedSubtitle": "仅本人可申领奖励", "permissionlessClaimingTurnedOff": "己许可申领己关闭", "points": "点数", "pool": "提名池", @@ -155,6 +167,8 @@ "proxy": "代理账户", "randomValidator": "随机验证人", "reGenerate": "重新生成", + "readyToJoinPool": "可加入提名池", + "recentPerformance": "最近表现", "remove": "删除", "removeSelected": "移除选定项", "reset": "重设", @@ -171,6 +185,7 @@ "signing": "签署中", "submitTransaction": "准备提交交易", "syncing": "正在同步", + "syncingPoolData": "查找提名池中", "syncingPoolList": "同步提名池列表", "tooSmall": "质押金额太少", "top": "首", diff --git a/src/locale/cn/modals.json b/src/locale/cn/modals.json index 7827a08bdb..e817ffd244 100644 --- a/src/locale/cn/modals.json +++ b/src/locale/cn/modals.json @@ -71,6 +71,7 @@ "developerTools": "开发者工具", "differentNetworkAddress": "不同的网络地址", "disconnect": "断开", + "disconnectFromExtension": "请重新确认断开与此扩展的连接,这将重新加载该应用", "done": "完成", "ensureLedgerIsConnected": "提示:请确保您的Ledger设备已连接", "exitYourStakingPosition": "退出抵押", diff --git a/src/locale/cn/pages.json b/src/locale/cn/pages.json index cca2e74eba..09dae46e41 100644 --- a/src/locale/cn/pages.json +++ b/src/locale/cn/pages.json @@ -140,7 +140,6 @@ "cancel": "取消", "closePool": "可提取己解锁金额并关闭池", "compound": "复利", - "create": "创建", "createAPool": "创建提名池", "createPool": "创建提名池", "depositor": "存款人", @@ -154,7 +153,8 @@ "generateNominations": "生成提名", "inPool": "提名池中", "inactivePoolNotNominating": "非活跃:提名池未提名任何验证人", - "join": "加入", + "joinPool": "加入提名池", + "joinPoolHeading": " {{totalMembers}} 个提名池成员在{{network}}上抵押共{{totalPoolPoints}} 个{{unit}} ", "leave": "离开", "leavingPool": "离开提名池中", "leftThePool": "所有成员已离开", diff --git a/src/locale/en/library.json b/src/locale/en/library.json index 1ceb67213b..7901a8b96e 100644 --- a/src/locale/en/library.json +++ b/src/locale/en/library.json @@ -9,34 +9,42 @@ "activePools": "Active Pools", "activeValidator": "Active Validator", "activeValidators": "Active Validators", + "activelyNominating": "Actively Nominating", "add": "Add", "addFromFavorites": "Add From Favorites", "address": "Address", "addressCopiedToClipboard": "Address Copied to Clipboard", + "addresses": "Addresses", "all": "All", "allowAll": "Allow All", - "allowAnyoneCompound": "Allow anyone to compound rewards on your behalf.", + "allowAnyoneCompound": "Allow anyone to compound your rewards.", "allowAnyoneCompoundWithdraw": "Allow anyone to compound or withdraw rewards on your behalf.", - "allowAnyoneWithdraw": "Allow anyone to withdraw rewards on your behalf.", + "allowAnyoneWithdraw": "Allow anyone to withdraw your rewards to your account.", "allowCompound": "Allow Compound", "allowWithdraw": "Allow Withdraw", "alreadyImported": "Address Already Imported", + "analyzingPoolPerformance": "Analyzing pool performance", "asAPoolMember": "as a pool member.", "asThePoolDepositor": "as the pool depositor.", "atLeast": "Bond amount must be at least", + "autoSelected": "Auto Selected", "available": "Available", "backToMethods": "Back to Methods", "backToScan": "Back to Scan", + "blocked": "Blocked", "blockedNominations": "Blocked Nominations", "blockingNominations": "Blocking Nominations", "bond": "Bond", "bondAmountDecimals": "Bond amount can only have at most {{units}} decimals.", "bondDecimalsError": "Bond amount can have at most {{units}} decimals.", "bonded": "Bonded", + "browseValidators": "Browse Validators", "cancel": "Cancel", "cancelled": "Cancelled", + "chooseAnotherPool": "Choose Another Pool", "chooseValidators": "Choose up to {{maxNominations}} validators to nominate.", "chooseValidators2": "Generate your nominations automatically or manually insert them.", + "claimSetting": "Claim Setting", "clear": "Clear", "clearSelection": "clear selection", "clickToReload": "Click to reload", @@ -66,6 +74,7 @@ "displayingValidators_other": "Displaying {{count}} Validators", "done": "Done", "enablePermissionlessClaiming": "Enable Permissionless Claiming", + "era": "Era", "eraPoints": "Era Points", "errorUnknown": "Oops, Something Went Wrong", "errorWithTransaction": "Error with transaction", @@ -125,6 +134,8 @@ "nominateActive": "Active", "nominateInactive": "Inactive", "nominationsReverted": "Nominations Reverted", + "nominations_one": "Nomination", + "nominations_other": "Nominations", "nominator": "Nominator", "notEnough": "Not Enough", "notEnoughAfter": "Not enough {{unit}} to bond after transaction fees.", @@ -145,6 +156,8 @@ "payoutAccount": "Payout Account", "payoutAddress": "Payout Adddress", "pending": "Pending", + "permissioned": "Permissioned", + "permissionedSubtitle": "Only you can claim rewards.", "permissionlessClaimingTurnedOff": "Permissionless claiming is turned off.", "points": "Points", "pool": "Pool", @@ -157,6 +170,8 @@ "proxy": "Proxy", "randomValidator": "Random Validator", "reGenerate": "Re-Generate", + "readyToJoinPool": "Ready to Join Pool", + "recentPerformance": "Recent Performance", "remove": "Remove", "removeSelected": "Remove Selected", "reset": "Reset", @@ -173,6 +188,7 @@ "signing": "Signing", "submitTransaction": "Ready to submit transaction.", "syncing": "Syncing", + "syncingPoolData": "Finding Pools to Join", "syncingPoolList": "Syncing Pool list", "tooSmall": "Bond amount is too small.", "top": "Top", diff --git a/src/locale/en/modals.json b/src/locale/en/modals.json index 791d225ce8..2c2b989839 100644 --- a/src/locale/en/modals.json +++ b/src/locale/en/modals.json @@ -74,6 +74,7 @@ "developerTools": "Developer Tools", "differentNetworkAddress": "Different Network Address", "disconnect": "Disconnect", + "disconnectFromExtension": "Are you sure you want to disconnect from this extension? This will reload the dashboard.", "done": "Done", "ensureLedgerIsConnected": "Tip: Ensure your Ledger device is connected before continuing.", "exitYourStakingPosition": "Exit your staking position.", diff --git a/src/locale/en/pages.json b/src/locale/en/pages.json index 9c06757d17..211f72d6ca 100644 --- a/src/locale/en/pages.json +++ b/src/locale/en/pages.json @@ -142,7 +142,6 @@ "cancel": "Cancel", "closePool": "You can now withdraw and close the pool.", "compound": "Compound", - "create": "Create", "createAPool": "Create a Pool", "createPool": "Create Pool", "depositor": "Depositor", @@ -156,7 +155,8 @@ "generateNominations": "Generate Nominations", "inPool": "In Pool", "inactivePoolNotNominating": "Inactive: Pool Not Nominating", - "join": "Join", + "joinPool": "Join Pool", + "joinPoolHeading": "Join {{totalMembers}} pool members staking a total of {{totalPoolPoints}} {{unit}} on {{network}}.", "leave": "Leave", "leavingPool": "Leaving Pool", "leftThePool": "All members have now left the pool", diff --git a/src/modals/AccountPoolRoles/index.tsx b/src/modals/AccountPoolRoles/index.tsx index a9a65f77f1..a1f36b1cb8 100644 --- a/src/modals/AccountPoolRoles/index.tsx +++ b/src/modals/AccountPoolRoles/index.tsx @@ -43,7 +43,7 @@ export const AccountPoolRoles = () => { )}

{t('activeRoles', { - count: activePools?.length || 0, + count: Object.keys(activePools)?.length || 0, })}

diff --git a/src/modals/Connect/Extension.tsx b/src/modals/Connect/Extension.tsx index 5dace6bada..c9cab29022 100644 --- a/src/modals/Connect/Extension.tsx +++ b/src/modals/Connect/Extension.tsx @@ -1,7 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { + faExternalLinkAlt, + faMinus, + faPlus, +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +15,7 @@ import type { ExtensionProps } from './types'; import { NotificationsController } from 'controllers/NotificationsController'; import { ModalConnectItem } from 'kits/Overlay/structure/ModalConnectItem'; import { useExtensionAccounts, useExtensions } from '@w3ux/react-connect-kit'; +import { localStorageOrDefault } from '@w3ux/utils'; export const Extension = ({ meta, size, flag }: ExtensionProps) => { const { t } = useTranslation('modals'); @@ -24,18 +29,34 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => { // Force re-render on click. const [increment, setIncrement] = useState(0); - // click to connect to extension + const connected = extensionsStatus[id] === 'connected'; + + // Click to connect to extension. const handleClick = async () => { - if (canConnect) { - const connected = await connectExtensionAccounts(id); - // force re-render to display error messages - setIncrement(increment + 1); + if (!connected) { + if (canConnect) { + const success = await connectExtensionAccounts(id); + // force re-render to display error messages + setIncrement(increment + 1); + + if (success) { + NotificationsController.emit({ + title: t('extensionConnected'), + subtitle: `${t('titleExtensionConnected', { title })}`, + }); + } + } + } else { + if (confirm(t('disconnectFromExtension'))) { + const updatedAtiveExtensions = ( + localStorageOrDefault('active_extensions', [], true) as string[] + ).filter((ext: string) => ext !== id); - if (connected) { - NotificationsController.emit({ - title: t('extensionConnected'), - subtitle: `${t('titleExtensionConnected', { title })}`, - }); + localStorage.setItem( + 'active_extensions', + JSON.stringify(updatedAtiveExtensions) + ); + location.reload(); } } }; @@ -47,15 +68,22 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => { : id; const Icon = ExtensionIcons[iconId]; - // determine message to be displayed based on extension status. + // Determine message to be displayed based on extension status. let statusJsx; switch (extensionsStatus[id]) { case 'connected': - statusJsx =

{t('connected')}

; + statusJsx = ( +

+ + {t('disconnect')} +

+ ); break; + case 'not_authenticated': statusJsx =

{t('notAuthenticated')}

; break; + default: statusJsx = (

@@ -67,8 +95,7 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => { const websiteText = typeof website === 'string' ? website : website.text; const websiteUrl = typeof website === 'string' ? website : website.url; - - const disabled = extensionsStatus[id] === 'connected' || !isInstalled; + const disabled = !isInstalled; return ( @@ -94,6 +121,7 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => {

{title}

+ {connected &&

{t('connected')}

}
diff --git a/src/modals/Connect/Wrappers.ts b/src/modals/Connect/Wrappers.ts index 85df9608d4..25a1e8c3d7 100644 --- a/src/modals/Connect/Wrappers.ts +++ b/src/modals/Connect/Wrappers.ts @@ -26,19 +26,43 @@ export const ExtensionInner = styled.div` h3 { font-family: InterSemiBold, sans-serif; - margin: 1rem 0 0 0; + margin: 0; + > svg { margin-right: 0.5rem; } } + p { color: var(--text-color-secondary); padding: 0; margin: 0; - .plus { + + &.success { + color: var(--status-success-color); + } + + &.danger { + color: var(--status-danger-color); + } + + &.active { + color: var(--accent-color-primary); + } + + &.inline { + color: var(--status-success-color); + border: 0.75px solid var(--status-success-color); + border-radius: 0.4rem; + margin-left: 0.75rem; + padding: 0.1rem 0.4rem; + } + + > .plus { margin-right: 0.4rem; } } + .body { width: 100%; padding: 1.35rem 0.85rem 0.75rem 0.85rem; @@ -60,24 +84,27 @@ export const ExtensionInner = styled.div` .row { width: 100%; display: flex; + align-items: center; } .foot { padding: 0.25rem 1rem 1rem 1rem; } + .status { position: absolute; top: 0.9rem; right: 0.9rem; - .success { - color: var(--status-success-color); - } - .active { - color: var(--accent-color-primary); + display: flex; + + > p { + margin-left: 1rem; } } + .icon { color: var(--text-color-primary); width: 100%; + margin-bottom: 0.75rem; svg { max-width: 2.6rem; diff --git a/src/modals/Connect/index.tsx b/src/modals/Connect/index.tsx index 861d558a23..e60a67e6ab 100644 --- a/src/modals/Connect/index.tsx +++ b/src/modals/Connect/index.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; -import extensions from '@w3ux/extension-assets'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Close } from 'library/Modal/Close'; @@ -26,6 +25,8 @@ import { ModalMotionThreeSection } from 'kits/Overlay/structure/ModalMotionThree import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; import { useExtensions } from '@w3ux/react-connect-kit'; import { useEffectIgnoreInitial } from '@w3ux/hooks'; +import extensions from '@w3ux/extension-assets'; +import type { ExtensionArrayListItem } from '@w3ux/extension-assets/util'; export const Connect = () => { const { t } = useTranslation('modals'); @@ -44,12 +45,13 @@ export const Connect = () => { // Whether the app is running on of mobile wallets. const inMobileWallet = inNova || inSubWallet; - // If in SubWallet Mobile, keep `subwallet-js` only. + // Get supported extensions. const extensionsAsArray = Object.entries(extensions).map(([key, value]) => ({ id: key, ...value, - })); + })) as ExtensionArrayListItem[]; + // If in SubWallet Mobile, keep `subwallet-js` only. const web = inSubWallet ? extensionsAsArray.filter((a) => a.id === 'subwallet-js') : // If in Nova Wallet, fetch nova wallet metadata and replace its id with `polkadot-js`. diff --git a/src/modals/Connect/types.ts b/src/modals/Connect/types.ts index 935e3dc83f..930f35b256 100644 --- a/src/modals/Connect/types.ts +++ b/src/modals/Connect/types.ts @@ -20,6 +20,7 @@ export interface ExtensionMetaProps { url: string; text: string; }; + otherEcosystems?: string[]; } export interface ListWithInputProps { diff --git a/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx b/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx index 919d3b9198..7ce53ec93b 100644 --- a/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx +++ b/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx @@ -19,6 +19,7 @@ import { useBalances } from 'contexts/Balances'; import { ButtonSubmitInvert } from 'kits/Buttons/ButtonSubmitInvert'; import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; import { ModalWarnings } from 'kits/Overlay/structure/ModalWarnings'; +import { defaultClaimPermission } from 'controllers/ActivePoolsController/defaults'; export const SetClaimPermission = ({ setSection, @@ -92,10 +93,7 @@ export const SetClaimPermission = ({ ) : null} { setClaimPermission(val); }} diff --git a/src/modals/PoolNominations/Wrappers.ts b/src/modals/PoolNominations/Wrappers.ts deleted file mode 100644 index 9348154c0a..0000000000 --- a/src/modals/PoolNominations/Wrappers.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import styled from 'styled-components'; - -export const ListWrapper = styled.div` - display: flex; - flex-flow: column wrap; - align-items: center; - position: relative; - width: 100%; - - > div, - h3 { - width: 100%; - } -`; diff --git a/src/modals/PoolNominations/index.tsx b/src/modals/PoolNominations/index.tsx deleted file mode 100644 index b29b23fdfd..0000000000 --- a/src/modals/PoolNominations/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import { useTranslation } from 'react-i18next'; -import { Title } from 'library/Modal/Title'; -import { ValidatorList } from 'library/ValidatorList'; -import { useOverlay } from 'kits/Overlay/Provider'; -import { ListWrapper } from './Wrappers'; -import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; - -export const PoolNominations = () => { - const { - config: { options }, - } = useOverlay().modal; - const { nominator, targets } = options; - const { t } = useTranslation('modals'); - - return ( - <> - - <ModalPadding> - <ListWrapper> - {targets.length > 0 ? ( - <ValidatorList - format="nomination" - bondFor="pool" - validators={targets} - nominator={nominator} - showMenu={false} - displayFor="modal" - allowListFormat={false} - refetchOnListUpdate - /> - ) : ( - <h3>{t('poolIsNotNominating')}</h3> - )} - </ListWrapper> - </ModalPadding> - </> - ); -}; diff --git a/src/model/Api/index.ts b/src/model/Api/index.ts index bfb40e30e8..ec1fa28b5f 100644 --- a/src/model/Api/index.ts +++ b/src/model/Api/index.ts @@ -92,7 +92,8 @@ export class Api { // Class initialization. Sets the `provider` and `api` class members. async initialize(type: ConnectionType, rpcEndpoint: string) { - // Add initial syncing items. + // Add initial syncing items. Even though `initialization` is added by default, it is called + // again here in case a new API is initialized. SyncController.dispatch('initialization', 'syncing'); // Persist the network to local storage. diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx index 7b76f3821f..56960499cd 100644 --- a/src/overlay/index.tsx +++ b/src/overlay/index.tsx @@ -16,11 +16,9 @@ import { Connect } from '../modals/Connect'; import { GoToFeedback } from '../modals/GoToFeedback'; import { ImportLedger } from '../modals/ImportLedger'; import { ImportVault } from '../modals/ImportVault'; -import { JoinPool } from '../modals/JoinPool'; import { ManageFastUnstake } from '../modals/ManageFastUnstake'; import { ManagePool } from '../modals/ManagePool'; import { Networks } from '../modals/Networks'; -import { PoolNominations } from '../modals/PoolNominations'; import { Settings } from '../modals/Settings'; import { Unbond } from '../modals/Unbond'; import { UnlockChunks } from '../modals/UnlockChunks'; @@ -31,6 +29,9 @@ import { ValidatorMetrics } from '../modals/ValidatorMetrics'; import { ValidatorGeo } from '../modals/ValidatorGeo'; import { ManageNominations } from '../canvas/ManageNominations'; import { PoolMembers } from 'canvas/PoolMembers'; +import { JoinPool } from 'canvas/JoinPool'; +import { CreatePool } from 'canvas/CreatePool'; +import { NominatorSetup } from 'canvas/NominatorSetup'; import { Overlay } from 'kits/Overlay'; export const Overlays = () => { @@ -51,13 +52,11 @@ export const Overlays = () => { Connect, Accounts, GoToFeedback, - JoinPool, ImportLedger, ImportVault, ManagePool, ManageFastUnstake, Networks, - PoolNominations, Settings, ValidatorMetrics, ValidatorGeo, @@ -70,6 +69,9 @@ export const Overlays = () => { canvas={{ ManageNominations, PoolMembers, + JoinPool, + CreatePool, + NominatorSetup, }} /> ); diff --git a/src/pages/Nominate/Active/ManageBond.tsx b/src/pages/Nominate/Active/ManageBond.tsx index 76999e7e83..d0967f16cc 100644 --- a/src/pages/Nominate/Active/ManageBond.tsx +++ b/src/pages/Nominate/Active/ManageBond.tsx @@ -31,8 +31,8 @@ export const ManageBond = () => { }, } = useNetwork(); const { openHelp } = useHelp(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { getLedger } = useBalances(); const { openModal } = useOverlay().modal; const { isFastUnstaking } = useUnstaking(); diff --git a/src/pages/Nominate/Active/Status/NewNominator.tsx b/src/pages/Nominate/Active/Status/NewNominator.tsx new file mode 100644 index 0000000000..6f3cd3e59c --- /dev/null +++ b/src/pages/Nominate/Active/Status/NewNominator.tsx @@ -0,0 +1,79 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CallToActionWrapper } from 'library/CallToAction'; +import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNavigate } from 'react-router-dom'; +import { useApi } from 'contexts/Api'; +import type { NewNominatorProps } from '../types'; +import { CallToActionLoader } from 'library/Loader/CallToAction'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useOverlay } from 'kits/Overlay/Provider'; +import { registerSaEvent } from 'Utils'; +import { useNetwork } from 'contexts/Network'; + +export const NewNominator = ({ syncing }: NewNominatorProps) => { + const { t } = useTranslation(); + const { isReady } = useApi(); + const navigate = useNavigate(); + const { network } = useNetwork(); + const { openCanvas } = useOverlay().canvas; + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + const nominateButtonDisabled = + !isReady || !activeAccount || isReadOnlyAccount(activeAccount); + + return ( + <CallToActionWrapper> + <div className="inner"> + {syncing ? ( + <CallToActionLoader /> + ) : ( + <> + <section className="standalone"> + <div className="buttons"> + <div + className={`button primary standalone${nominateButtonDisabled ? ` disabled` : ``}`} + > + <button + onClick={() => { + registerSaEvent( + `${network.toLowerCase()}_nominate_setup_button_pressed` + ); + + openCanvas({ + key: 'NominatorSetup', + options: {}, + size: 'xl', + }); + }} + disabled={nominateButtonDisabled} + > + {t('nominate.startNominating', { ns: 'pages' })} + </button> + </div> + </div> + </section> + <section> + <div className="buttons"> + <div className={`button secondary standalone`}> + <button onClick={() => navigate('/validators')}> + {t('browseValidators', { ns: 'library' })} + <FontAwesomeIcon + icon={faChevronRight} + transform="shrink-4" + /> + </button> + </div> + </div> + </section> + </> + )} + </div> + </CallToActionWrapper> + ); +}; diff --git a/src/pages/Nominate/Active/Status/NominationStatus.tsx b/src/pages/Nominate/Active/Status/NominationStatus.tsx index 74e53dfb9d..5ee363c027 100644 --- a/src/pages/Nominate/Active/Status/NominationStatus.tsx +++ b/src/pages/Nominate/Active/Status/NominationStatus.tsx @@ -1,25 +1,18 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { - faBolt, - faChevronCircleRight, - faSignOutAlt, -} from '@fortawesome/free-solid-svg-icons'; +import { faBolt, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useBonded } from 'contexts/Bonded'; import { useFastUnstake } from 'contexts/FastUnstake'; -import { useSetup } from 'contexts/Setup'; import { useStaking } from 'contexts/Staking'; import { useNominationStatus } from 'hooks/useNominationStatus'; import { useUnstaking } from 'hooks/useUnstaking'; import { Stat } from 'library/Stat'; -import { useTranslation } from 'react-i18next'; -import { registerSaEvent } from 'Utils'; import { useOverlay } from 'kits/Overlay/Provider'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -import { useNetwork } from 'contexts/Network'; import { useSyncing } from 'hooks/useSyncing'; export const NominationStatus = ({ @@ -30,7 +23,6 @@ export const NominationStatus = ({ buttonType?: string; }) => { const { t } = useTranslation('pages'); - const { network } = useNetwork(); const { inSetup } = useStaking(); const { openModal } = useOverlay().modal; const { getBondedAccount } = useBonded(); @@ -44,7 +36,6 @@ export const NominationStatus = ({ const { isReadOnlyAccount } = useImportedAccounts(); const { getNominationStatus } = useNominationStatus(); const { getFastUnstakeText, isUnstaking } = useUnstaking(); - const { setOnNominatorSetup, getNominatorSetupPercent } = useSetup(); const fastUnstakeText = getFastUnstakeText(); const controller = getBondedAccount(activeAccount); @@ -70,15 +61,6 @@ export const NominationStatus = ({ onClick: () => openModal({ key: 'Unstake', size: 'sm' }), }; - // Display progress alongside start title if exists and in setup. - let startTitle = t('nominate.startNominating'); - if (inSetup()) { - const progress = getNominatorSetupPercent(activeAccount); - if (progress > 0) { - startTitle += `: ${progress}%`; - } - } - return ( <Stat label={t('nominate.status')} @@ -91,23 +73,7 @@ export const NominationStatus = ({ ? !isUnstaking ? [unstakeButton] : [] - : [ - { - title: startTitle, - icon: faChevronCircleRight, - transform: 'grow-1', - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - !activeAccount, - onClick: () => { - registerSaEvent( - `${network.toLowerCase()}_nominate_setup_button_pressed` - ); - setOnNominatorSetup(true); - }, - }, - ] + : [] } buttonType={buttonType} /> diff --git a/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx index 39cc95b035..8334686cfc 100644 --- a/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx +++ b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx @@ -16,8 +16,8 @@ import { useSyncing } from 'hooks/useSyncing'; export const PayoutDestinationStatus = () => { const { t } = useTranslation('pages'); const { getPayee } = useBalances(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { openModal } = useOverlay().modal; const { isFastUnstaking } = useUnstaking(); const { getPayeeItems } = usePayeeConfig(); diff --git a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx index cca2d2cc3c..46805ace60 100644 --- a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx +++ b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx @@ -13,7 +13,7 @@ import { useNetwork } from 'contexts/Network'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const UnclaimedPayoutsStatus = () => { +export const UnclaimedPayoutsStatus = ({ dimmed }: { dimmed: boolean }) => { const { t } = useTranslation(); const { networkData: { units }, @@ -43,6 +43,7 @@ export const UnclaimedPayoutsStatus = () => { 2 ), }} + dimmed={dimmed} buttons={ Object.keys(unclaimedPayouts || {}).length > 0 && !totalUnclaimed.isZero() diff --git a/src/pages/Nominate/Active/Status/index.tsx b/src/pages/Nominate/Active/Status/index.tsx index 2ed3ce0c39..f37a61ec12 100644 --- a/src/pages/Nominate/Active/Status/index.tsx +++ b/src/pages/Nominate/Active/Status/index.tsx @@ -6,13 +6,41 @@ import { UnclaimedPayoutsStatus } from './UnclaimedPayoutsStatus'; import { NominationStatus } from './NominationStatus'; import { PayoutDestinationStatus } from './PayoutDestinationStatus'; import { Separator } from 'kits/Structure/Separator'; +import { useSyncing } from 'hooks/useSyncing'; +import { useStaking } from 'contexts/Staking'; +import { NewNominator } from './NewNominator'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const Status = ({ height }: { height: number }) => ( - <CardWrapper height={height}> - <NominationStatus /> - <Separator /> - <UnclaimedPayoutsStatus /> - <Separator /> - <PayoutDestinationStatus /> - </CardWrapper> -); +export const Status = ({ height }: { height: number }) => { + const { syncing } = useSyncing(); + const { inSetup } = useStaking(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + return ( + <CardWrapper + height={height} + className={!syncing && inSetup() ? 'prompt' : undefined} + > + <NominationStatus /> + <Separator /> + <UnclaimedPayoutsStatus dimmed={inSetup()} /> + + {!syncing ? ( + !inSetup() ? ( + <> + <Separator /> + <PayoutDestinationStatus /> + </> + ) : ( + !isReadOnlyAccount(activeAccount) && ( + <NewNominator syncing={syncing} /> + ) + ) + ) : ( + <NewNominator syncing={syncing} /> + )} + </CardWrapper> + ); +}; diff --git a/src/pages/Nominate/Active/UnstakePrompts.tsx b/src/pages/Nominate/Active/UnstakePrompts.tsx index 122eb5ec0a..f9a3ea106a 100644 --- a/src/pages/Nominate/Active/UnstakePrompts.tsx +++ b/src/pages/Nominate/Active/UnstakePrompts.tsx @@ -19,7 +19,7 @@ import { ButtonRow } from 'kits/Structure/ButtonRow'; export const UnstakePrompts = () => { const { t } = useTranslation('pages'); const { mode } = useTheme(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { openModal } = useOverlay().modal; const { activeAccount } = useActiveAccounts(); const { unit, colors } = useNetwork().networkData; diff --git a/src/pages/Nominate/Active/index.tsx b/src/pages/Nominate/Active/index.tsx index 58e7ef1d59..cf05491721 100644 --- a/src/pages/Nominate/Active/index.tsx +++ b/src/pages/Nominate/Active/index.tsx @@ -31,8 +31,8 @@ import { WithdrawPrompt } from 'library/WithdrawPrompt'; export const Active = () => { const { t } = useTranslation(); const { openHelp } = useHelp(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { getNominations } = useBalances(); const { openCanvas } = useOverlay().canvas; const { isFastUnstaking } = useUnstaking(); @@ -55,14 +55,14 @@ export const Active = () => { <UnstakePrompts /> <PageRow> - <RowSection hLast> - <Status height={ROW_HEIGHT} /> - </RowSection> - <RowSection secondary> + <RowSection secondary vLast> <CardWrapper height={ROW_HEIGHT}> <ManageBond /> </CardWrapper> </RowSection> + <RowSection hLast> + <Status height={ROW_HEIGHT} /> + </RowSection> </PageRow> <PageRow> <CardWrapper> diff --git a/src/pages/Nominate/Active/types.ts b/src/pages/Nominate/Active/types.ts index d3c14eeec1..db68ada844 100644 --- a/src/pages/Nominate/Active/types.ts +++ b/src/pages/Nominate/Active/types.ts @@ -10,3 +10,7 @@ export interface BondedChartProps { unlocked: BigNumber; inactive: boolean; } + +export interface NewNominatorProps { + syncing: boolean; +} diff --git a/src/pages/Nominate/Setup/index.tsx b/src/pages/Nominate/Setup/index.tsx deleted file mode 100644 index a67fc654a9..0000000000 --- a/src/pages/Nominate/Setup/index.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import { faChevronLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; -import { PageRow } from 'kits/Structure/PageRow'; -import { extractUrlValue, removeVarFromUrlHash } from '@w3ux/utils'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { Element } from 'react-scroll'; -import { useSetup } from 'contexts/Setup'; -import { CardWrapper } from 'library/Card/Wrappers'; -import { Nominate } from 'library/SetupSteps/Nominate'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { Bond } from './Bond'; -import { Payee } from './Payee'; -import { Summary } from './Summary'; -import { ButtonSecondary } from 'kits/Buttons/ButtonSecondary'; -import { PageTitle } from 'kits/Structure/PageTitle'; -import { PageHeadingWrapper } from 'kits/Structure/PageHeading/Wrapper'; - -export const Setup = () => { - const { t } = useTranslation('pages'); - const navigate = useNavigate(); - const { activeAccount } = useActiveAccounts(); - const { setOnNominatorSetup, removeSetupProgress } = useSetup(); - - return ( - <> - <PageTitle title={t('nominate.startNominating')} /> - <PageRow> - <PageHeadingWrapper> - <span> - <ButtonSecondary - text={t('nominate.back')} - iconLeft={faChevronLeft} - iconTransform="shrink-3" - onClick={() => { - if (extractUrlValue('f') === 'overview') { - navigate('/overview'); - } else { - removeVarFromUrlHash('f'); - setOnNominatorSetup(false); - } - }} - /> - </span> - <span> - <ButtonSecondary - text={t('nominate.cancel')} - iconLeft={faTimes} - onClick={() => { - removeVarFromUrlHash('f'); - setOnNominatorSetup(false); - removeSetupProgress('nominator', activeAccount); - }} - /> - </span> - <div className="right" /> - </PageHeadingWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="payee" style={{ position: 'absolute' }} /> - <Payee section={1} /> - </CardWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="nominate" style={{ position: 'absolute' }} /> - <Nominate bondFor="nominator" section={2} /> - </CardWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="bond" style={{ position: 'absolute' }} /> - <Bond section={3} /> - </CardWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="summary" style={{ position: 'absolute' }} /> - <Summary section={4} /> - </CardWrapper> - </PageRow> - </> - ); -}; diff --git a/src/pages/Nominate/index.tsx b/src/pages/Nominate/index.tsx index 0238d1d752..fbbd63515e 100644 --- a/src/pages/Nominate/index.tsx +++ b/src/pages/Nominate/index.tsx @@ -1,12 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { useSetup } from 'contexts/Setup'; import { Active } from './Active'; -import { Setup } from './Setup'; import { Wrapper } from './Wrappers'; -export const Nominate = () => { - const { onNominatorSetup } = useSetup(); - return <Wrapper>{onNominatorSetup ? <Setup /> : <Active />}</Wrapper>; -}; +export const Nominate = () => ( + <Wrapper> + <Active /> + </Wrapper> +); diff --git a/src/pages/Overview/Payouts.tsx b/src/pages/Overview/Payouts.tsx index b9b9d8b2a3..5c4750d1fd 100644 --- a/src/pages/Overview/Payouts.tsx +++ b/src/pages/Overview/Payouts.tsx @@ -30,7 +30,7 @@ export const Payouts = () => { }, } = useNetwork(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { plugins } = usePlugins(); const { getData, injectBlockTimestamp } = useSubscanData([ 'payouts', 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<number>(1); - // current render iteration - const [renderIteration, _setRenderIteration] = useState<number>(1); - // manipulated list (ordering, filtering) of payouts const [payouts, setPayouts] = useState<AnySubscan>(initialPayouts); // is this the initial fetch const [fetched, setFetched] = useState<boolean>(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/index.tsx b/src/pages/Payouts/index.tsx index eaeb36ea8d..ea1a94a200 100644 --- a/src/pages/Payouts/index.tsx +++ b/src/pages/Payouts/index.tsx @@ -32,7 +32,7 @@ export const Payouts = ({ page: { key } }: PageProps) => { const { openHelp } = useHelp(); const { plugins } = usePlugins(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { getData, injectBlockTimestamp } = useSubscanData([ 'payouts', 'unclaimedPayouts', 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/Create/index.tsx b/src/pages/Pools/Create/index.tsx deleted file mode 100644 index af4d3c5f71..0000000000 --- a/src/pages/Pools/Create/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; -import { PageRow } from 'kits/Structure/PageRow'; -import { useTranslation } from 'react-i18next'; -import { Element } from 'react-scroll'; -import { useSetup } from 'contexts/Setup'; -import { CardWrapper } from 'library/Card/Wrappers'; -import { Nominate } from 'library/SetupSteps/Nominate'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { Bond } from './Bond'; -import { PoolName } from './PoolName'; -import { PoolRoles } from './PoolRoles'; -import { Summary } from './Summary'; -import { ButtonSecondary } from 'kits/Buttons/ButtonSecondary'; -import { PageTitle } from 'kits/Structure/PageTitle'; -import { PageHeadingWrapper } from 'kits/Structure/PageHeading/Wrapper'; - -export const Create = () => { - const { t } = useTranslation('pages'); - const { activeAccount } = useActiveAccounts(); - const { setOnPoolSetup, removeSetupProgress } = useSetup(); - - return ( - <> - <PageTitle title={t('pools.createAPool')} /> - <PageRow> - <PageHeadingWrapper> - <span> - <ButtonSecondary - text={t('pools.back')} - iconLeft={faChevronLeft} - iconTransform="shrink-3" - onClick={() => setOnPoolSetup(false)} - /> - </span> - <span> - <ButtonSecondary - text={t('pools.cancel')} - onClick={() => { - setOnPoolSetup(false); - removeSetupProgress('pool', activeAccount); - }} - /> - </span> - <div className="right" /> - </PageHeadingWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="metadata" style={{ position: 'absolute' }} /> - <PoolName section={1} /> - </CardWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="nominate" style={{ position: 'absolute' }} /> - <Nominate bondFor="pool" section={2} /> - </CardWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="roles" style={{ position: 'absolute' }} /> - <PoolRoles section={3} /> - </CardWrapper> - </PageRow> - <PageRow> - <CardWrapper> - <Element name="bond" style={{ position: 'absolute' }} /> - <Bond section={4} /> - </CardWrapper> - </PageRow> - - <PageRow> - <CardWrapper> - <Element name="summary" style={{ position: 'absolute' }} /> - <Summary section={5} /> - </CardWrapper> - </PageRow> - </> - ); -}; diff --git a/src/pages/Pools/Home/ClosurePrompts.tsx b/src/pages/Pools/Home/ClosurePrompts.tsx index 473506c0bd..0adf25705c 100644 --- a/src/pages/Pools/Home/ClosurePrompts.tsx +++ b/src/pages/Pools/Home/ClosurePrompts.tsx @@ -46,6 +46,7 @@ export const ClosurePrompts = () => { active.toNumber() === 0 && totalUnlockChunks === 0 && !targets.length; return ( + state === 'Destroying' && depositorCanClose && ( <PageRow> <CardWrapper style={{ border: `1px solid ${annuncementBorderColor}` }}> 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/ManagePool/index.tsx b/src/pages/Pools/Home/ManagePool/index.tsx index a588e32963..03adf2dbb9 100644 --- a/src/pages/Pools/Home/ManagePool/index.tsx +++ b/src/pages/Pools/Home/ManagePool/index.tsx @@ -9,18 +9,14 @@ import { useActivePool } from 'contexts/Pools/ActivePool'; import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; import { Nominations } from 'library/Nominations'; import { useOverlay } from 'kits/Overlay/Provider'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { useSyncing } from 'hooks/useSyncing'; import { useValidators } from 'contexts/Validators/ValidatorEntries'; import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; export const ManagePool = () => { const { t } = useTranslation(); - const { syncing } = useSyncing(['active-pools']); const { openCanvas } = useOverlay().canvas; const { formatWithPrefs } = useValidators(); - const { activeAccount } = useActiveAccounts(); const { isOwner, isNominator, activePoolNominations, activePool } = useActivePool(); @@ -38,9 +34,7 @@ export const ManagePool = () => { return ( <PageRow> <CardWrapper> - {syncing ? ( - <Nominations bondFor="pool" nominator={activeAccount} /> - ) : canNominate && !isNominating && state !== 'Destroying' ? ( + {canNominate && !isNominating && state !== 'Destroying' ? ( <> <CardHeaderWrapper $withAction $withMargin> <h3> diff --git a/src/pages/Pools/Home/PoolStats/index.tsx b/src/pages/Pools/Home/PoolStats/index.tsx index d9861e2dca..9f66c2afec 100644 --- a/src/pages/Pools/Home/PoolStats/index.tsx +++ b/src/pages/Pools/Home/PoolStats/index.tsx @@ -20,12 +20,12 @@ export const PoolStats = () => { const { networkData: { units, unit }, } = useNetwork(); + const { activePool } = useActivePool(); const { getCurrentCommission } = usePoolCommission(); - const { activePool, activePoolMemberCount } = useActivePool(); const poolId = activePool?.id || 0; - const { state, points } = activePool?.bondedPool || {}; + const { state, points, memberCounter } = activePool?.bondedPool || {}; const currentCommission = getCurrentCommission(poolId); const bonded = planckToUnit( @@ -65,13 +65,13 @@ export const PoolStats = () => { items.push( { label: t('pools.poolMembers'), - value: `${activePoolMemberCount}`, + value: `${memberCounter}`, button: { text: t('pools.browseMembers'), onClick: () => { openCanvas({ key: 'PoolMembers', size: 'xl' }); }, - disabled: activePoolMemberCount === 0, + disabled: memberCounter === '0', }, }, { diff --git a/src/pages/Pools/Home/Status/MembershipStatus.tsx b/src/pages/Pools/Home/Status/MembershipStatus.tsx index b76848427a..59b40f75f6 100644 --- a/src/pages/Pools/Home/Status/MembershipStatus.tsx +++ b/src/pages/Pools/Home/Status/MembershipStatus.tsx @@ -13,22 +13,18 @@ import { useOverlay } from 'kits/Overlay/Provider'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { useStatusButtons } from './useStatusButtons'; -import { useSyncing } from 'hooks/useSyncing'; +import type { MembershipStatusProps } from './types'; export const MembershipStatus = ({ showButtons = true, buttonType = 'primary', -}: { - showButtons?: boolean; - buttonType?: string; -}) => { +}: MembershipStatusProps) => { const { t } = useTranslation('pages'); const { isReady } = useApi(); - const { syncing } = useSyncing('*'); const { openModal } = useOverlay().modal; const { poolsMetaData } = useBondedPools(); const { activeAccount } = useActiveAccounts(); - const { label, buttons } = useStatusButtons(); + const { label } = useStatusButtons(); const { isReadOnlyAccount } = useImportedAccounts(); const { getTransferOptions } = useTransferOptions(); const { activePool, isOwner, isBouncer, isMember } = useActivePool(); @@ -52,18 +48,21 @@ export const MembershipStatus = ({ (poolState !== 'Destroying' && (isOwner() || isBouncer())) || (isMember() && active?.isGreaterThan(0)) ) { - membershipButtons.push({ - title: t('pools.manage'), - icon: faCog, - disabled: !isReady || isReadOnlyAccount(activeAccount), - small: true, - onClick: () => - openModal({ - key: 'ManagePool', - options: { disableWindowResize: true, disableScroll: true }, - size: 'sm', - }), - }); + // Display manage button if active account is not a read-only account. + if (!isReadOnlyAccount(activeAccount)) { + membershipButtons.push({ + title: t('pools.manage'), + icon: faCog, + disabled: !isReady, + small: true, + onClick: () => + openModal({ + key: 'ManagePool', + options: { disableWindowResize: true, disableScroll: true }, + size: 'sm', + }), + }); + } } } @@ -83,7 +82,6 @@ export const MembershipStatus = ({ label={t('pools.poolMembership')} helpKey="Pool Membership" stat={t('pools.notInPool')} - buttons={!showButtons || syncing ? [] : buttons} buttonType={buttonType} /> ); diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx new file mode 100644 index 0000000000..663d2dbbbd --- /dev/null +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -0,0 +1,121 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CallToActionWrapper } from 'library/CallToAction'; +import { faUserPlus } from '@fortawesome/free-solid-svg-icons'; +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 { PoolSync } from 'library/PoolSync'; +import { useJoinPools } from 'contexts/Pools/JoinPools'; +import { StyledLoader } from 'library/PoolSync/Loader'; +import { registerSaEvent } from 'Utils'; +import { useNetwork } from 'contexts/Network'; + +export const NewMember = ({ syncing }: NewMemberProps) => { + const { t } = useTranslation(); + const { network } = useNetwork(); + const { poolsForJoin } = useJoinPools(); + const { openCanvas } = useOverlay().canvas; + const { startJoinPoolFetch } = useJoinPools(); + const { getPoolPerformanceTask } = usePoolPerformance(); + const { getJoinDisabled, getCreateDisabled } = useStatusButtons(); + + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); + + // Alias for create button disabled state. + const createDisabled = getCreateDisabled(); + + // Disable opening the canvas if data is not ready. + const joinButtonDisabled = getJoinDisabled() || !poolsForJoin.length; + + return ( + <CallToActionWrapper> + <div className="inner"> + {syncing ? ( + <CallToActionLoader /> + ) : ( + <> + <section className="fixedWidth"> + <div className="buttons"> + <div + className={`button primary standalone${getJoinDisabled() ? ` disabled` : ``}${poolJoinPerformanceTask.status === 'synced' ? ` pulse` : ``}`} + > + <button + onClick={() => { + // Start sync process, otherwise, open canvas. + if (poolJoinPerformanceTask.status === 'unsynced') { + startJoinPoolFetch(); + } + registerSaEvent( + `${network.toLowerCase()}_pool_join_button_pressed` + ); + + openCanvas({ + key: 'JoinPool', + options: {}, + size: 'xl', + }); + }} + disabled={joinButtonDisabled} + > + {poolJoinPerformanceTask.status === 'unsynced' && ( + <> + {t('pools.joinPool', { ns: 'pages' })} + <FontAwesomeIcon icon={faUserPlus} /> + </> + )} + + {poolJoinPerformanceTask.status === 'syncing' && ( + <> + {t('syncingPoolData', { ns: 'library' })}{' '} + <StyledLoader /> + </> + )} + + {poolJoinPerformanceTask.status === 'synced' && ( + <> + {t('readyToJoinPool', { ns: 'library' })} + <FontAwesomeIcon icon={faUserPlus} /> + </> + )} + <PoolSync performanceKey="pool_join" /> + </button> + </div> + </div> + </section> + <section> + <div className="buttons"> + <div + className={`button secondary standalone${createDisabled ? ` disabled` : ``}`} + > + <button + onClick={() => { + registerSaEvent( + `${network.toLowerCase()}_pool_create_button_pressed` + ); + + openCanvas({ + key: 'CreatePool', + options: {}, + size: 'xl', + }); + }} + disabled={createDisabled} + > + {t('pools.createPool', { ns: 'pages' })} + </button> + </div> + </div> + </section> + </> + )} + </div> + </CallToActionWrapper> + ); +}; diff --git a/src/pages/Pools/Home/Status/RewardsStatus.tsx b/src/pages/Pools/Home/Status/RewardsStatus.tsx index d9f5df9885..c2d2e25862 100644 --- a/src/pages/Pools/Home/Status/RewardsStatus.tsx +++ b/src/pages/Pools/Home/Status/RewardsStatus.tsx @@ -14,7 +14,7 @@ import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { useSyncing } from 'hooks/useSyncing'; -export const RewardsStatus = () => { +export const RewardsStatus = ({ dimmed }: { dimmed: boolean }) => { const { t } = useTranslation('pages'); const { networkData: { units }, @@ -34,37 +34,37 @@ export const RewardsStatus = () => { : '0'; // Display Reward buttons if unclaimed rewards is a non-zero value. - const buttonsRewards = pendingPoolRewards.isGreaterThan(minUnclaimedDisplay) - ? [ - { - title: t('pools.withdraw'), - icon: faCircleDown, - disabled: !isReady || isReadOnlyAccount(activeAccount), - small: true, - onClick: () => - openModal({ - key: 'ClaimReward', - options: { claimType: 'withdraw' }, - size: 'sm', - }), - }, - { - title: t('pools.compound'), - icon: faPlus, - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - activePool?.bondedPool?.state === 'Destroying', - small: true, - onClick: () => - openModal({ - key: 'ClaimReward', - options: { claimType: 'bond' }, - size: 'sm', - }), - }, - ] - : undefined; + const buttonsRewards = isReadOnlyAccount(activeAccount) + ? [] + : pendingPoolRewards.isGreaterThan(minUnclaimedDisplay) + ? [ + { + title: t('pools.withdraw'), + icon: faCircleDown, + disabled: !isReady, + small: true, + onClick: () => + openModal({ + key: 'ClaimReward', + options: { claimType: 'withdraw' }, + size: 'sm', + }), + }, + { + title: t('pools.compound'), + icon: faPlus, + disabled: + !isReady || activePool?.bondedPool?.state === 'Destroying', + small: true, + onClick: () => + openModal({ + key: 'ClaimReward', + options: { claimType: 'bond' }, + size: 'sm', + }), + }, + ] + : undefined; return ( <Stat @@ -72,6 +72,7 @@ export const RewardsStatus = () => { helpKey="Pool Rewards" type="odometer" stat={{ value: labelRewards }} + dimmed={dimmed} buttons={syncing ? [] : buttonsRewards} /> ); diff --git a/src/pages/Pools/Home/Status/index.tsx b/src/pages/Pools/Home/Status/index.tsx index 119e7eb46f..145d1303fa 100644 --- a/src/pages/Pools/Home/Status/index.tsx +++ b/src/pages/Pools/Home/Status/index.tsx @@ -7,20 +7,43 @@ import { MembershipStatus } from './MembershipStatus'; import { PoolStatus } from './PoolStatus'; import { RewardsStatus } from './RewardsStatus'; import { Separator } from 'kits/Structure/Separator'; +import type { StatusProps } from './types'; +import { NewMember } from './NewMember'; +import { useSyncing } from 'hooks/useSyncing'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useBalances } from 'contexts/Balances'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const Status = ({ height }: { height: number }) => { +export const Status = ({ height }: StatusProps) => { const { activePool } = useActivePool(); + const { getPoolMembership } = useBalances(); + const { poolMembersipSyncing } = useSyncing(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + const membership = getPoolMembership(activeAccount); + const syncing = poolMembersipSyncing(); return ( - <CardWrapper height={height}> + <CardWrapper + height={height} + className={!syncing && !activePool && !membership ? 'prompt' : undefined} + > <MembershipStatus /> <Separator /> - <RewardsStatus /> - {activePool && ( - <> - <Separator /> - <PoolStatus /> - </> + <RewardsStatus dimmed={membership === null} /> + {!syncing ? ( + activePool && !!membership ? ( + <> + <Separator /> + <PoolStatus /> + </> + ) : ( + membership === null && + !isReadOnlyAccount(activeAccount) && <NewMember syncing={syncing} /> + ) + ) : ( + <NewMember syncing={syncing} /> )} </CardWrapper> ); diff --git a/src/pages/Pools/Home/Status/types.ts b/src/pages/Pools/Home/Status/types.ts new file mode 100644 index 0000000000..dc15b67805 --- /dev/null +++ b/src/pages/Pools/Home/Status/types.ts @@ -0,0 +1,15 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface StatusProps { + height: number; +} + +export interface MembershipStatusProps { + showButtons?: boolean; + buttonType?: string; +} + +export interface NewMemberProps { + syncing: boolean; +} diff --git a/src/pages/Pools/Home/Status/useStatusButtons.tsx b/src/pages/Pools/Home/Status/useStatusButtons.tsx index 7c131bc22f..7cf825d8ba 100644 --- a/src/pages/Pools/Home/Status/useStatusButtons.tsx +++ b/src/pages/Pools/Home/Status/useStatusButtons.tsx @@ -1,41 +1,32 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faPlusCircle, faUserPlus } from '@fortawesome/free-solid-svg-icons'; -import { registerSaEvent } from 'Utils'; import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useActivePool } from 'contexts/Pools/ActivePool'; import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useSetup } from 'contexts/Setup'; import { useTransferOptions } from 'contexts/TransferOptions'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -import { useNetwork } from 'contexts/Network'; -import { usePoolsTabs } from '../context'; import { useBalances } from 'contexts/Balances'; export const useStatusButtons = () => { const { t } = useTranslation('pages'); - const { network } = useNetwork(); const { isReady, poolsConfig: { maxPools }, } = useApi(); const { isOwner } = useActivePool(); - const { setActiveTab } = usePoolsTabs(); const { bondedPools } = useBondedPools(); const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); const { getTransferOptions } = useTransferOptions(); const { isReadOnlyAccount } = useImportedAccounts(); - const { setOnPoolSetup, getPoolSetupPercent } = useSetup(); const membership = getPoolMembership(activeAccount); const { active } = getTransferOptions(activeAccount).pool; - const poolSetupPercent = getPoolSetupPercent(activeAccount); - const disableCreate = () => { + const getCreateDisabled = () => { if (!isReady || isReadOnlyAccount(activeAccount) || !activeAccount) { return true; } @@ -49,40 +40,15 @@ export const useStatusButtons = () => { }; let label; - let buttons; - const createBtn = { - title: `${t('pools.create')}${ - poolSetupPercent > 0 ? `: ${poolSetupPercent}%` : `` - }`, - icon: faPlusCircle, - large: false, - transform: 'grow-1', - disabled: disableCreate(), - onClick: () => { - registerSaEvent(`${network.toLowerCase()}_pool_create_button_pressed`); - setOnPoolSetup(true); - }, - }; - const joinPoolBtn = { - title: `${t('pools.join')}`, - icon: faUserPlus, - large: false, - transform: 'grow-1', - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - !activeAccount || - !bondedPools.length, - onClick: () => { - registerSaEvent(`${network.toLowerCase()}_pool_join_button_pressed`); - setActiveTab(1); - }, - }; + const getJoinDisabled = () => + !isReady || + isReadOnlyAccount(activeAccount) || + !activeAccount || + !bondedPools.length; if (!membership) { label = t('pools.poolMembership'); - buttons = [createBtn, joinPoolBtn]; } else if (isOwner()) { label = `${t('pools.ownerOfPool')} ${membership.poolId}`; } else if (active?.isGreaterThan(0)) { @@ -90,5 +56,5 @@ export const useStatusButtons = () => { } else { label = `${t('pools.leavingPool')} ${membership.poolId}`; } - return { label, buttons }; + return { label, getJoinDisabled, getCreateDisabled }; }; diff --git a/src/pages/Pools/Home/index.tsx b/src/pages/Pools/Home/index.tsx index 7a347df83f..8d3bcf104e 100644 --- a/src/pages/Pools/Home/index.tsx +++ b/src/pages/Pools/Home/index.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { useActivePool } from 'contexts/Pools/ActivePool'; import { useBondedPools } from 'contexts/Pools/BondedPools'; import { CardWrapper } from 'library/Card/Wrappers'; -import { PoolList } from 'library/PoolList/Default'; +import { PoolList } from 'library/PoolList'; import { StatBoxList } from 'library/StatBoxList'; import { useFavoritePools } from 'contexts/Pools/FavoritePools'; import { useOverlay } from 'kits/Overlay/Provider'; @@ -23,7 +23,6 @@ import { MinCreateBondStat } from './Stats/MinCreateBond'; import { MinJoinBondStat } from './Stats/MinJoinBond'; import { Status } from './Status'; import { PoolsTabsProvider, usePoolsTabs } from './context'; -import { useApi } from 'contexts/Api'; import { useActivePools } from 'hooks/useActivePools'; import { useBalances } from 'contexts/Balances'; import { PageTitle } from 'kits/Structure/PageTitle'; @@ -31,27 +30,30 @@ import type { PageTitleTabProps } from 'kits/Structure/PageTitleTabs/types'; import { PageRow } from 'kits/Structure/PageRow'; import { RowSection } from 'kits/Structure/RowSection'; import { WithdrawPrompt } from 'library/WithdrawPrompt'; +import { useSyncing } from 'hooks/useSyncing'; +import { useNetwork } from 'contexts/Network'; export const HomeInner = () => { const { t } = useTranslation('pages'); + const { network } = useNetwork(); const { favorites } = useFavoritePools(); const { openModal } = useOverlay().modal; const { bondedPools } = useBondedPools(); const { getPoolMembership } = useBalances(); + const { poolMembersipSyncing } = useSyncing(); const { activeAccount } = useActiveAccounts(); const { activeTab, setActiveTab } = usePoolsTabs(); const { getPoolRoles, activePool } = useActivePool(); - const { counterForBondedPools } = useApi().poolsConfig; - const membership = getPoolMembership(activeAccount); - const { state } = activePool?.bondedPool || {}; const { activePools } = useActivePools({ - poolIds: '*', + who: activeAccount, }); - const activePoolsNoMembership = { ...activePools }; - delete activePoolsNoMembership[membership?.poolId || -1]; + // Calculate the number of _other_ pools the user has a role in. + const poolRoleCount = Object.keys(activePools).filter( + (poolId) => poolId !== String(membership?.poolId) + ).length; let tabs: PageTitleTabProps[] = [ { @@ -66,7 +68,6 @@ export const HomeInner = () => { title: t('pools.allPools'), active: activeTab === 1, onClick: () => setActiveTab(1), - badge: String(counterForBondedPools.toString()), }, { title: t('pools.favorites'), @@ -76,22 +77,20 @@ export const HomeInner = () => { } ); - // Back to tab 0 if not in a pool & on members tab. - useEffect(() => { - if (!activePool) { - setActiveTab(0); - } - }, [activePool]); - const ROW_HEIGHT = 220; + // Go back to tab 0 on network change. + useEffect(() => { + setActiveTab(0); + }, [network]); + return ( <> <PageTitle title={t('pools.pools')} tabs={tabs} button={ - Object.keys(activePoolsNoMembership).length > 0 + !poolMembersipSyncing() && poolRoleCount > 0 ? { title: t('pools.allRoles'), onClick: () => @@ -111,21 +110,18 @@ export const HomeInner = () => { <MinCreateBondStat /> </StatBoxList> - {state === 'Destroying' ? ( - <ClosurePrompts /> - ) : ( - <WithdrawPrompt bondFor="pool" /> - )} + <ClosurePrompts /> + <WithdrawPrompt bondFor="pool" /> <PageRow> - <RowSection hLast> - <Status height={ROW_HEIGHT} /> - </RowSection> - <RowSection secondary> + <RowSection secondary vLast> <CardWrapper height={ROW_HEIGHT}> <ManageBond /> </CardWrapper> </RowSection> + <RowSection hLast> + <Status height={ROW_HEIGHT} /> + </RowSection> </PageRow> {activePool !== null && ( <> @@ -148,10 +144,6 @@ export const HomeInner = () => { <PoolListProvider> <PoolList pools={bondedPools} - defaultFilters={{ - includes: ['active'], - excludes: ['locked', 'destroying'], - }} allowMoreCols allowSearch pagination diff --git a/src/pages/Pools/index.tsx b/src/pages/Pools/index.tsx index 6918c650ea..5d5f50cdff 100644 --- a/src/pages/Pools/index.tsx +++ b/src/pages/Pools/index.tsx @@ -1,11 +1,6 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { useSetup } from 'contexts/Setup'; -import { Create } from './Create'; import { Home } from './Home'; -export const Pools = () => { - const { onPoolSetup } = useSetup(); - return onPoolSetup ? <Create /> : <Home />; -}; +export const Pools = () => <Home />; diff --git a/src/theme/accents/polkadot-relay.css b/src/theme/accents/polkadot-relay.css index 3d7c824141..2e7448deca 100644 --- a/src/theme/accents/polkadot-relay.css +++ b/src/theme/accents/polkadot-relay.css @@ -6,7 +6,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --accent-color-primary-dark: rgb(211 48 121); --accent-color-secondary-light: #552bbf; - --accent-color-secondary-dark: #6d39ee; + --accent-color-secondary-dark: #aa90ec; --accent-color-tertiary-light: #dedae8; --accent-color-tertiary-dark: #32264c; diff --git a/src/theme/theme.scss b/src/theme/theme.scss index aab7daf572..c931951bba 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -9,9 +9,9 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-list-item: rgb(238 238 238 / 100%); --background-modal-card: rgb(237 237 237 / 75%); --background-canvas-card: rgb(245 245 245 / 90%); + --background-canvas-card-secondary: rgb(255 255 255 / 35%); --background-floating-card: rgb(255 255 255 / 90%); --background-app-footer: rgb(244 225 225 / 75%); - --background-warning: #fdf9eb; --background-modal: #f9f7f7; --background-modal-footer: #efefef; --background-status-overlay: rgb(255 255 255 / 85%); @@ -23,6 +23,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --button-secondary-background: #e7e5e5; --button-tertiary-background: #ececec; --button-tab-background: #e4e2e2; + --button-tab-canvas-background: #d9d9d9; --button-hover-background: #e8e6e6; --card-shadow-color: rgb(158 158 158 / 20%); --card-deep-shadow-color: rgb(158 158 158 / 50%); @@ -44,7 +45,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --status-danger-color: #ae2324; --status-danger-color-transparent: rgb(255 0 0 / 25%); --text-color-primary: #3f3f3f; - --text-color-secondary: #555; + --text-color-secondary: #585858; --text-color-tertiary: #888; --text-color-invert: #fafafa; --gradient-background: linear-gradient( @@ -86,9 +87,9 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-list-item: rgb(38 33 38 / 100%); --background-modal-card: rgb(32 26 32 / 50%); --background-canvas-card: rgb(44 40 44 / 90%); + --background-canvas-card-secondary: rgb(44 40 44 / 35%); --background-floating-card: rgb(43 38 43 / 95%); --background-app-footer: #262327; - --background-warning: #33332a; --background-modal: rgb(43 38 43); --background-modal-footer: rgb(37 32 37); --background-status-overlay: rgb(43 38 43 / 75%); @@ -100,6 +101,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --button-secondary-background: rgb(55 50 55); --button-tertiary-background: rgb(54 49 54); --button-tab-background: rgb(56 51 56); + --button-tab-canvas-background: rgb(60 59 60); --button-hover-background: rgb(66 61 68); --card-shadow-color: rgb(28 24 28 / 25%); --card-deep-shadow-color: rgb(28 24 28 / 50%); diff --git a/src/types.ts b/src/types.ts index 9289752501..77fb00fdfd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,7 +140,7 @@ export type Sync = 'unsynced' | 'syncing' | 'synced'; export type BondFor = 'pool' | 'nominator'; // which medium components are being displayed on. -export type DisplayFor = 'default' | 'modal' | 'canvas'; +export type DisplayFor = 'default' | 'modal' | 'canvas' | 'card'; // generic function with no args or return type. export type Fn = () => void; diff --git a/src/workers/poolPerformance.ts b/src/workers/poolPerformance.ts index 0b21c69f12..7ed330f984 100644 --- a/src/workers/poolPerformance.ts +++ b/src/workers/poolPerformance.ts @@ -2,9 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only /* eslint-disable no-await-in-loop */ +import BigNumber from 'bignumber.js'; +import type { PoolRewardPointsKey } from 'contexts/Pools/PoolPerformance/types'; import type { Exposure } from 'contexts/Staking/types'; import type { ErasRewardPoints } from 'contexts/Validators/types'; -import type { AnyApi, AnyJson } from 'types'; +import type { AnyJson } from 'types'; // eslint-disable-next-line no-restricted-globals, @typescript-eslint/no-explicit-any export const ctx: Worker = self as any; @@ -13,7 +15,7 @@ export const ctx: Worker = self as any; ctx.addEventListener('message', async (event: AnyJson) => { const { data } = event; const { task } = data; - let message: AnyJson = {}; + let message = {}; switch (task) { case 'processNominationPoolsRewardData': message = await processErasStakersForNominationPoolRewards(data); @@ -25,41 +27,59 @@ ctx.addEventListener('message', async (event: AnyJson) => { // Process `erasStakersClipped` and generate nomination pool reward data. const processErasStakersForNominationPoolRewards = async ({ - bondedPools, + key, + addresses, era, erasRewardPoints, exposures, }: { - bondedPools: string[]; + key: PoolRewardPointsKey; + addresses: string[]; era: string; erasRewardPoints: ErasRewardPoints; exposures: Exposure[]; }) => { const poolRewardData: Record<string, Record<string, string>> = {}; - for (const address of bondedPools) { - let validator = null; - for (const exposure of exposures) { - const { others } = exposure.val; - const inOthers = others.find((o: AnyApi) => o.who === address); + const validators: Record<string, string[]> = {}; - if (inOthers) { - validator = exposure.keys[1]; - break; + for (const exposure of exposures) { + const { others } = exposure.val; + + // Return the `addresses` that are present in `others` for this era. + const addressesInOthers = addresses.filter((a) => + others.find(({ who }) => who === a) + ); + + for (const addressInOthers of addressesInOthers) { + if (validators[addressInOthers]) { + validators[addressInOthers].push(exposure.keys[1]); + } else { + validators[addressInOthers] = [exposure.keys[1]]; } } + } - if (validator) { - const rewardPoints: string = - erasRewardPoints[era]?.individual?.[validator || ''] ?? 0; - if (!poolRewardData[address]) { - poolRewardData[address] = {}; - } - poolRewardData[address][era] = rewardPoints; + for (const entry of Object.entries(validators)) { + const [entryAddress, entryValidators] = entry; + + const rewardPoints = entryValidators.reduce( + (acc: BigNumber, entryValidator: string) => + acc.plus( + erasRewardPoints[era]?.individual?.[entryValidator || ''] ?? 0 + ), + new BigNumber(0) + ); + + if (!poolRewardData[entryAddress]) { + poolRewardData[entryAddress] = {}; } + poolRewardData[entryAddress][era] = rewardPoints.toString(); } return { + key, + addresses, poolRewardData, }; }; diff --git a/yarn.lock b/yarn.lock index 0dc1c6e092..32acedb245 100644 --- a/yarn.lock +++ b/yarn.lock @@ -268,16 +268,16 @@ __metadata: languageName: node linkType: hard -"@dotlottie/common@npm:0.7.10": - version: 0.7.10 - resolution: "@dotlottie/common@npm:0.7.10" +"@dotlottie/common@npm:0.7.11": + version: 0.7.11 + resolution: "@dotlottie/common@npm:0.7.11" dependencies: "@dotlottie/dotlottie-js": "npm:^0.7.0" "@preact/signals-core": "npm:^1.2.3" howler: "npm:^2.2.3" lottie-web: "npm:^5.12.2" xstate: "npm:^4.38.1" - checksum: 10c0/09d934b4a78670132ed337ff57cc5eed41e1aaf24abc08c8fda66844c3a20ea92d54b70570007d08e30fa1dfa34b7db496fa37205f94588a88a7e92ec96d8ed5 + checksum: 10c0/185446390c8c93eacce98a1d8dfa21415f26382c6523f3ce60e93cdbca37ce967ca6546809c2743fdcaedd657a872750b95bb1a711a35f64015fbe303ed0dc39 languageName: node linkType: hard @@ -294,13 +294,13 @@ __metadata: languageName: node linkType: hard -"@dotlottie/player-component@npm:^2.7.10": - version: 2.7.11 - resolution: "@dotlottie/player-component@npm:2.7.11" +"@dotlottie/player-component@npm:^2.7.12": + version: 2.7.12 + resolution: "@dotlottie/player-component@npm:2.7.12" dependencies: - "@dotlottie/common": "npm:0.7.10" + "@dotlottie/common": "npm:0.7.11" lit: "npm:^2.7.5" - checksum: 10c0/55d686a1ab0f5339e7c4e43c03b559fa6830d71aa90ef6a23e08a5b5d5bccfe29275c81f06efa07d0a44dab34e46c809e873a2da3ab6110470e63c0f38e07b7b + checksum: 10c0/0faed3adc8a734418f029f3858928e1d6436f4d31c05c4bbee7dadeadc7cffa9b01cd4fd9675b5b152e56aa44a53d3aa879a37465b2c36dcf6149adb74bb2c6f languageName: node linkType: hard @@ -1146,7 +1146,25 @@ __metadata: languageName: node linkType: hard -"@polkadot/extension-inject@npm:^0.46.5, @polkadot/extension-inject@npm:latest": +"@polkadot/extension-inject@npm:0.46.9": + version: 0.46.9 + resolution: "@polkadot/extension-inject@npm:0.46.9" + dependencies: + "@polkadot/api": "npm:^10.12.4" + "@polkadot/rpc-provider": "npm:^10.12.4" + "@polkadot/types": "npm:^10.12.4" + "@polkadot/util": "npm:^12.6.2" + "@polkadot/util-crypto": "npm:^12.6.2" + "@polkadot/x-global": "npm:^12.6.2" + tslib: "npm:^2.6.2" + peerDependencies: + "@polkadot/api": "*" + "@polkadot/util": "*" + checksum: 10c0/6afd3f8f5b1b803004eb50ab4588035c679933533042010cf55f685d21d8f34e2d3c8644f61831098b0cbd1abe8a669b48c22a1d19d0cc06175e9ff798e9a87c + languageName: node + linkType: hard + +"@polkadot/extension-inject@npm:^0.46.5": version: 0.46.8 resolution: "@polkadot/extension-inject@npm:0.46.8" dependencies: @@ -1264,6 +1282,30 @@ __metadata: languageName: node linkType: hard +"@polkadot/rpc-provider@npm:^10.12.4": + version: 10.12.6 + resolution: "@polkadot/rpc-provider@npm:10.12.6" + dependencies: + "@polkadot/keyring": "npm:^12.6.2" + "@polkadot/types": "npm:10.12.6" + "@polkadot/types-support": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + "@polkadot/util-crypto": "npm:^12.6.2" + "@polkadot/x-fetch": "npm:^12.6.2" + "@polkadot/x-global": "npm:^12.6.2" + "@polkadot/x-ws": "npm:^12.6.2" + "@substrate/connect": "npm:0.8.8" + eventemitter3: "npm:^5.0.1" + mock-socket: "npm:^9.3.1" + nock: "npm:^13.5.0" + tslib: "npm:^2.6.2" + dependenciesMeta: + "@substrate/connect": + optional: true + checksum: 10c0/c265e95967ff4224d93edee72dbe06ce23782e09f036f3a1a65f017cfd95ed992828bb6ccea83d75295d36540c9f1451bbecf1d1e958cc78d0865624f3fb3f03 + languageName: node + linkType: hard + "@polkadot/types-augment@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types-augment@npm:10.11.2" @@ -1288,6 +1330,18 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-augment@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-augment@npm:10.12.6" + dependencies: + "@polkadot/types": "npm:10.12.6" + "@polkadot/types-codec": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/be32309d68686a41ba1ddccfcbc4dab1e973c44a565fbfbfb177b217d787ac12d7aa68df595d08d7a600a30b275d17c334384aeef67dc856babba9bc74105854 + languageName: node + linkType: hard + "@polkadot/types-codec@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types-codec@npm:10.11.2" @@ -1310,6 +1364,17 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-codec@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-codec@npm:10.12.6" + dependencies: + "@polkadot/util": "npm:^12.6.2" + "@polkadot/x-bigint": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/804d2fcc299ef461ed370de84b9848470450ba9ed4afc9cd7665f3f1cce44402f06fd432e8e7bfba893759f939bd8452d6a6ea63309c67e0311c9e9581732046 + languageName: node + linkType: hard + "@polkadot/types-create@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types-create@npm:10.11.2" @@ -1332,6 +1397,17 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-create@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-create@npm:10.12.6" + dependencies: + "@polkadot/types-codec": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/62aaf5bf8bee78f1ed26d490f2d4a6c57338403ee1994abee7df39536d770c0603d144d6b3897a801dee6470d19d64de20c04d424dfb66d8c297a97e37258de6 + languageName: node + linkType: hard + "@polkadot/types-known@npm:10.12.4": version: 10.12.4 resolution: "@polkadot/types-known@npm:10.12.4" @@ -1366,6 +1442,16 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-support@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-support@npm:10.12.6" + dependencies: + "@polkadot/util": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/cf050fd816572e67430ae58ecc7f5035ec9dedc9e85c0f017580a10ba31d70f5edd0ccc8c43351bd3cb2423cb30053d26a857110e6b08633713bae5d5b4ac419 + languageName: node + linkType: hard + "@polkadot/types@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types@npm:10.11.2" @@ -1398,6 +1484,22 @@ __metadata: languageName: node linkType: hard +"@polkadot/types@npm:10.12.6, @polkadot/types@npm:^10.12.4": + version: 10.12.6 + resolution: "@polkadot/types@npm:10.12.6" + dependencies: + "@polkadot/keyring": "npm:^12.6.2" + "@polkadot/types-augment": "npm:10.12.6" + "@polkadot/types-codec": "npm:10.12.6" + "@polkadot/types-create": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + "@polkadot/util-crypto": "npm:^12.6.2" + rxjs: "npm:^7.8.1" + tslib: "npm:^2.6.2" + checksum: 10c0/4a54b3a2f999d24e54946b4411aadfdc91d1cb37e3430b795e208f6b33325571750d9a7d45e307eb0dd92bbe1850c2b5111b5c54213e554465dae82da7143249 + languageName: node + linkType: hard + "@polkadot/util-crypto@npm:12.6.2, @polkadot/util-crypto@npm:^12.6.2": version: 12.6.2 resolution: "@polkadot/util-crypto@npm:12.6.2" @@ -1587,11 +1689,11 @@ __metadata: languageName: node linkType: hard -"@polkagate/extension-dapp@npm:^0.46.7-22": - version: 0.46.7-22 - resolution: "@polkagate/extension-dapp@npm:0.46.7-22" +"@polkagate/extension-dapp@npm:^0.46.12": + version: 0.46.12 + resolution: "@polkagate/extension-dapp@npm:0.46.12" dependencies: - "@polkadot/extension-inject": "npm:latest" + "@polkadot/extension-inject": "npm:0.46.9" "@polkadot/types": "npm:^10.11.2" "@polkadot/util": "npm:^12.6.2" "@polkadot/util-crypto": "npm:^12.6.2" @@ -1600,7 +1702,7 @@ __metadata: "@polkadot/api": "*" "@polkadot/util": "*" "@polkadot/util-crypto": "*" - checksum: 10c0/7d91fb0a3c7ab71dbf69a3b80dd68ed28fe4f3611c55038e5492e4f7ebf7a7d929b3a4e6e7e996a73ef728c21dd09ff8ef7d5696ff356b3a6c63e93a9d718a43 + checksum: 10c0/a4e41d566eb6cf4d2b5092627b03ec1b1a4a029445264a340cb3ede6b2e9e60e6cc658a58291395194700542842626b859d82b60aca1deb1166a1fce9c76c3d8 languageName: node linkType: hard @@ -2218,12 +2320,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.2.22": - version: 18.2.22 - resolution: "@types/react-dom@npm:18.2.22" +"@types/react-dom@npm:^18.2.23": + version: 18.2.23 + resolution: "@types/react-dom@npm:18.2.23" dependencies: "@types/react": "npm:*" - checksum: 10c0/cd85b5f402126e44b8c7b573e74737389816abcc931b2b14d8f946ba81cce8637ea490419488fcae842efb1e2f69853bc30522e43fd8359e1007d4d14b8d8146 + checksum: 10c0/9348e93558aa67b4b237bd0eab62e72e85f3e17a1c45fde04d874476269730f7c671b3d62390c4fca588da2a026e90cc74148abc349dbfd4ee5535a82ccdf38e languageName: node linkType: hard @@ -2245,7 +2347,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.2.67": +"@types/react@npm:*": version: 18.2.67 resolution: "@types/react@npm:18.2.67" dependencies: @@ -2256,6 +2358,16 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.2.73": + version: 18.2.73 + resolution: "@types/react@npm:18.2.73" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.0.2" + checksum: 10c0/b6645ab3c20efa41cfccf58ce0be45419517a0ba4594e323dd400342fb1c1f9589d169cf9bfa85b5b0605e9097fe9de7734b6d0c533f5b9bc32aaadb624537a4 + languageName: node + linkType: hard + "@types/scheduler@npm:*": version: 0.16.8 resolution: "@types/scheduler@npm:0.16.8" @@ -2295,15 +2407,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^7.1.0": - version: 7.3.1 - resolution: "@typescript-eslint/eslint-plugin@npm:7.3.1" +"@typescript-eslint/eslint-plugin@npm:^7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.5.0" dependencies: "@eslint-community/regexpp": "npm:^4.5.1" - "@typescript-eslint/scope-manager": "npm:7.3.1" - "@typescript-eslint/type-utils": "npm:7.3.1" - "@typescript-eslint/utils": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" + "@typescript-eslint/scope-manager": "npm:7.5.0" + "@typescript-eslint/type-utils": "npm:7.5.0" + "@typescript-eslint/utils": "npm:7.5.0" + "@typescript-eslint/visitor-keys": "npm:7.5.0" debug: "npm:^4.3.4" graphemer: "npm:^1.4.0" ignore: "npm:^5.2.4" @@ -2316,44 +2428,54 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/446c36801ee434854c935fd09f267bd68d537c1e422cfca87237230313b2ea40b512bb2357bcf489225df10a6d2f14dcd3ac8db80517b982abe0b609dd606c6c + checksum: 10c0/932a7b5a09c0138ef5a0bf00f8e6039fa209d4047092ffc187de048543c21f7ce24dc14f25f4c87b6f3bbb62335fc952e259e271fde88baf793217bde6460cfa languageName: node linkType: hard -"@typescript-eslint/parser@npm:^7.1.0": - version: 7.3.1 - resolution: "@typescript-eslint/parser@npm:7.3.1" +"@typescript-eslint/parser@npm:^7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/parser@npm:7.4.0" dependencies: - "@typescript-eslint/scope-manager": "npm:7.3.1" - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/typescript-estree": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" + "@typescript-eslint/scope-manager": "npm:7.4.0" + "@typescript-eslint/types": "npm:7.4.0" + "@typescript-eslint/typescript-estree": "npm:7.4.0" + "@typescript-eslint/visitor-keys": "npm:7.4.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/c524e7021ea551cb83e19c7f1a697664171a6b227e16e33912243af659905a7effeaf9fc05e3c160cb99d8ba17552fa87e27be38261280daa733d4d4d4876eec + checksum: 10c0/70ae32d406685e83fc26b4f4d3eb90c59965e0ff4fec4fd89ecd3cb386376bedb75cd8c11691b9de4743243d61a7d17ae242fe6c689a7c443a8977bc9755700b + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/scope-manager@npm:7.4.0" + dependencies: + "@typescript-eslint/types": "npm:7.4.0" + "@typescript-eslint/visitor-keys": "npm:7.4.0" + checksum: 10c0/d1dddf6819d753063fbbcae2cd01e861d0162a9755c6c786901654ccb9d316ca1dcc5887a61fb70e72372db4c2e67c6d1890f09d8b0270971c18b48808765ba9 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/scope-manager@npm:7.3.1" +"@typescript-eslint/scope-manager@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/scope-manager@npm:7.5.0" dependencies: - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" - checksum: 10c0/08dd466b19445a8e2b093df7fcc59767289843d1cdc423b2f402a2a2c69a53e3cdf52dcc1497311346a45e875d77826a831b5b9a9fb7f709679f221344051c74 + "@typescript-eslint/types": "npm:7.5.0" + "@typescript-eslint/visitor-keys": "npm:7.5.0" + checksum: 10c0/a017b151a6b39ef591f8e2e65598e005e1b4b2d5494e4b91bddb5856b3a4d57dd8a58d2bc7a140e627eb574f93a2c8fe55f1307aa264c928ffd31d9e190bc5dd languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/type-utils@npm:7.3.1" +"@typescript-eslint/type-utils@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/type-utils@npm:7.5.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.3.1" - "@typescript-eslint/utils": "npm:7.3.1" + "@typescript-eslint/typescript-estree": "npm:7.5.0" + "@typescript-eslint/utils": "npm:7.5.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.0.1" peerDependencies: @@ -2361,23 +2483,49 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/0e9ad41fe9eac135e1f6b448a2e1660df83e93bd2c370f1aaabe8bbdd376cda0e00d02b884793a3ce3a51c962c1f5cac543bcc1f02e4d1de2af757031aa6cbed + checksum: 10c0/12915d4d1872638f5281e222a0d191676c478f250699c84864862e95a59e708222acefbf7ffdafc0872a007261219a3a2b1e667ff45eeafea7c4bcc5b955262c + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/types@npm:7.4.0" + checksum: 10c0/685df163cdd6d546de8a2d22896e461777a89756faf1f34342c959e7d3f4cc75b1f47a96da50483fe1da75d06515bb105f58339d277ad7e02c15ab61c90ad097 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/types@npm:7.5.0" + checksum: 10c0/f3394f71f422dbd89f63b230f20e9769c12e47a287ff30ca03a80714e57ea21279b6f12a8ab14bafb00b59926f20a88894b2d1e72679f7ff298bae112679d4b3 languageName: node linkType: hard -"@typescript-eslint/types@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/types@npm:7.3.1" - checksum: 10c0/d3b579829db901b2ea52000a6e343b7e3814fa06f62ba42711df2533365a247e97699f64fc15482cc433302ff81e8a0eed1ed2b0478d0709171d57910d46bdd5 +"@typescript-eslint/typescript-estree@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.4.0" + dependencies: + "@typescript-eslint/types": "npm:7.4.0" + "@typescript-eslint/visitor-keys": "npm:7.4.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:9.0.3" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/31910f9283bcb2db7d3dd77b5a3b0c52e9769cd296e78a5ba742360f9e1971a6a3e1b5eb31109b4d584a62c2caa3075a346c5413b55e28cda0226a73865d62b7 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/typescript-estree@npm:7.3.1" +"@typescript-eslint/typescript-estree@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.5.0" dependencies: - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" + "@typescript-eslint/types": "npm:7.5.0" + "@typescript-eslint/visitor-keys": "npm:7.5.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -2387,34 +2535,44 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/52dbfc590b01a43fae906dadd383c185b93fea5c8ac90aa2369f6c36d53a5d465fac02315a903a3b291974626045547ab53f346dc2271e93c8179deaad7a3961 + checksum: 10c0/ea3a270c725d6be273188b86110e0393052cd64d1c54a56eb5ea405e6d3fbbe84fb3b1ce1b8496a4078ac1eefd37aedcf12be91876764f6de31d5aa5131c7bcd languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/utils@npm:7.3.1" +"@typescript-eslint/utils@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/utils@npm:7.5.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" "@types/json-schema": "npm:^7.0.12" "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:7.3.1" - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/typescript-estree": "npm:7.3.1" + "@typescript-eslint/scope-manager": "npm:7.5.0" + "@typescript-eslint/types": "npm:7.5.0" + "@typescript-eslint/typescript-estree": "npm:7.5.0" semver: "npm:^7.5.4" peerDependencies: eslint: ^8.56.0 - checksum: 10c0/1d7b049b2c4de1937832ae8ed681bbcd3b06b0d0b476cce67af96b2f65ff606413cc7dfdaad1e01057d24ba39bf5f6d4ba2923d23dab784d2bed5a217ab7b825 + checksum: 10c0/c815ed6909769648953d6963c069038f7cac0c979051b25718feb30e0d3337b9647b75b8de00ac03fe960f0cc8dc4e8a81d4aac4719090a99785e0068712bd24 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.4.0" + dependencies: + "@typescript-eslint/types": "npm:7.4.0" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10c0/bd2ca99f4a771494b89124a1e4cd7f3c817ca4916b8a0168c5c226a245f25cf646b10095100fb8cb6d97134f63fa5bb15098daa94f48657b65332e8671ffdb52 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/visitor-keys@npm:7.3.1" +"@typescript-eslint/visitor-keys@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.5.0" dependencies: - "@typescript-eslint/types": "npm:7.3.1" + "@typescript-eslint/types": "npm:7.5.0" eslint-visitor-keys: "npm:^3.4.1" - checksum: 10c0/1765d9ee31adaa1cfaaa72a1acc987bba6cc382b5c6785ffcc2706a776c115e9310ea6761f70fe9b83bc7edf5ecb3cb6814c83704bd2bb807a6a35cf52f36958 + checksum: 10c0/eecf02b8dd54e83738a143aca87b902af4b357028a90fd34ed7a2f40a3ae2f6a188b9ba53903f23c80e868f1fffbb039e9ddb63525438d659707cc7bfb269317 languageName: node linkType: hard @@ -2490,6 +2648,15 @@ __metadata: languageName: node linkType: hard +"@w3ux/extension-assets@npm:0.2.6": + version: 0.2.6 + resolution: "@w3ux/extension-assets@npm:0.2.6" + peerDependencies: + react: ^18 + checksum: 10c0/e789249b579c8786669cbeb13e0835bd790a092b61964f7714ec758b6d8b211287e09b934b02890991ffa660ba8bc18626364f5d7bf111e42ae640bb48258691 + languageName: node + linkType: hard + "@w3ux/extension-assets@npm:^0.2.3": version: 0.2.3 resolution: "@w3ux/extension-assets@npm:0.2.3" @@ -2508,17 +2675,17 @@ __metadata: languageName: node linkType: hard -"@w3ux/react-connect-kit@npm:^0.1.2": - version: 0.1.2 - resolution: "@w3ux/react-connect-kit@npm:0.1.2" +"@w3ux/react-connect-kit@npm:0.1.8": + version: 0.1.8 + resolution: "@w3ux/react-connect-kit@npm:0.1.8" dependencies: "@chainsafe/metamask-polkadot-adapter": "npm:^0.6.0" "@polkadot/util": "npm:^12.6.2" - "@polkagate/extension-dapp": "npm:^0.46.7-22" + "@polkagate/extension-dapp": "npm:^0.46.12" "@w3ux/extension-assets": "npm:^0.2.3" "@w3ux/hooks": "npm:^0.0.3" "@w3ux/utils": "npm:^0.0.2" - checksum: 10c0/708cf3f44e7f52f3cb5862fde553252fac8906c1c6c574145fa5b3b90cb8abe98cb2abcfc5533b7bebe48db521b63f08a134fd8502a459740b83f9d586858a8f + checksum: 10c0/aabfedb3736f4a17a2afcf4f49f22cbcb2fd01157e384ceae545fdec2d668318ea5d1e966db738a01ea35beefa54c5e094c5bfdcd807de59cdfd0085191efe93 languageName: node linkType: hard @@ -2567,9 +2734,9 @@ __metadata: languageName: node linkType: hard -"@zondax/ledger-substrate@npm:^0.41.3": - version: 0.41.3 - resolution: "@zondax/ledger-substrate@npm:0.41.3" +"@zondax/ledger-substrate@npm:^0.41.4": + version: 0.41.4 + resolution: "@zondax/ledger-substrate@npm:0.41.4" dependencies: "@ledgerhq/hw-transport": "npm:^6.27.1" bip32: "npm:^4.0.0" @@ -2580,7 +2747,7 @@ __metadata: hash.js: "npm:^1.1.7" bin: ledger-substrate: dist/cmd/cli.js - checksum: 10c0/fc1c3ce3e1b169be3dae05c5fbaf8e89bed709a4c25abceaff3c9f5f7938d4340834bb4ac21a43e67c3b603368f69486c3476f7378855dd99164d837d8c1a207 + checksum: 10c0/ac04674236f09f76d0cc5045fd6b23afdbbce493ec2a7fa3d37c5efb48be9aac355b2f2c146bb88b42f3423f38f9c249603076e9a9fbeaf22b5dc643fb1772f3 languageName: node linkType: hard @@ -4476,9 +4643,9 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^11.0.18": - version: 11.0.18 - resolution: "framer-motion@npm:11.0.18" +"framer-motion@npm:^11.0.24": + version: 11.0.24 + resolution: "framer-motion@npm:11.0.24" dependencies: tslib: "npm:^2.4.0" peerDependencies: @@ -4492,7 +4659,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10c0/a02e9ed86f03d6a20fb3dbcb9d17819c32f75fbeeba4825cba3cd1c48bea3e9ef35f44d7c199fa0c226e6cf4aa58446b6801fa1ee60ea10e609d1a46c5d7f45b + checksum: 10c0/8d884a828ef3e03683fdd5bbe5de64751f2a97f379f332262142caf2ae76d5af5fb1e2810a1a5845fb3322c8990f568997d22ffe8f0ff0f2eb7e637c5e0a5266 languageName: node linkType: hard @@ -4965,12 +5132,12 @@ __metadata: languageName: node linkType: hard -"i18next-browser-languagedetector@npm:^7.2.0": - version: 7.2.0 - resolution: "i18next-browser-languagedetector@npm:7.2.0" +"i18next-browser-languagedetector@npm:^7.2.1": + version: 7.2.1 + resolution: "i18next-browser-languagedetector@npm:7.2.1" dependencies: "@babel/runtime": "npm:^7.23.2" - checksum: 10c0/d7676e6c9895d46e659effaeba11f10c39cbe99429560667de7689cb56db6977239d350be850c4caf4279781c19c50a7193cb9cc38bb485f391b8e1893e407ae + checksum: 10c0/44fa71af4efb4cd6cc8bfbbd3f3b2735159e17d8f4396346e4016c6dd0ecbcdd68f1ec17609fd0de8dd6754c3d847d6e7e03227c19c1879d4c265cb1918948bb languageName: node linkType: hard @@ -6434,7 +6601,7 @@ __metadata: version: 0.0.0-use.local resolution: "polkadot-staking-dashboard@workspace:." dependencies: - "@dotlottie/player-component": "npm:^2.7.10" + "@dotlottie/player-component": "npm:^2.7.12" "@fortawesome/fontawesome-svg-core": "npm:^6.5.1" "@fortawesome/free-brands-svg-icons": "npm:^6.5.1" "@fortawesome/free-regular-svg-icons": "npm:^6.5.1" @@ -6452,22 +6619,22 @@ __metadata: "@types/chroma-js": "npm:^2.4.4" "@types/lodash.debounce": "npm:^4.0.9" "@types/lodash.throttle": "npm:^4.1.9" - "@types/react": "npm:^18.2.67" - "@types/react-dom": "npm:^18.2.22" + "@types/react": "npm:^18.2.73" + "@types/react-dom": "npm:^18.2.23" "@types/react-helmet": "npm:^6.1.11" "@types/react-scroll": "npm:^1.8.10" "@types/styled-components": "npm:^5.1.34" - "@typescript-eslint/eslint-plugin": "npm:^7.1.0" - "@typescript-eslint/parser": "npm:^7.1.0" + "@typescript-eslint/eslint-plugin": "npm:^7.5.0" + "@typescript-eslint/parser": "npm:^7.4.0" "@vitejs/plugin-react-swc": "npm:^3.6.0" - "@w3ux/extension-assets": "npm:^0.2.3" + "@w3ux/extension-assets": "npm:0.2.6" "@w3ux/hooks": "npm:^0.0.3" - "@w3ux/react-connect-kit": "npm:^0.1.2" + "@w3ux/react-connect-kit": "npm:0.1.8" "@w3ux/react-odometer": "npm:^0.0.3" "@w3ux/react-polkicon": "npm:^0.0.2" "@w3ux/utils": "npm:^0.0.2" "@w3ux/validator-assets": "npm:^0.0.4" - "@zondax/ledger-substrate": "npm:^0.41.3" + "@zondax/ledger-substrate": "npm:^0.41.4" bignumber.js: "npm:^9.1.2" bn.js: "npm:^5.2.1" buffer: "npm:^6.0.3" @@ -6484,11 +6651,11 @@ __metadata: eslint-plugin-react: "npm:^7.34.1" eslint-plugin-react-hooks: "npm:^4.6.0" eslint-plugin-unused-imports: "npm:^3.1.0" - framer-motion: "npm:^11.0.18" + framer-motion: "npm:^11.0.24" gh-pages: "npm:^6.1.1" html5-qrcode: "npm:^2.3.8" i18next: "npm:^23.10.0" - i18next-browser-languagedetector: "npm:^7.2.0" + i18next-browser-languagedetector: "npm:^7.2.1" lodash.debounce: "npm:^4.0.8" lodash.throttle: "npm:^4.1.1" prettier: "npm:^3.2.5" @@ -6505,9 +6672,9 @@ __metadata: react-scroll: "npm:^1.9.0" sass: "npm:^1.72.0" styled-components: "npm:^6.1.8" - typescript: "npm:^5.3.3" - usehooks-ts: "npm:^3.0.1" - vite: "npm:^5.2.2" + typescript: "npm:^5.4.3" + usehooks-ts: "npm:^3.0.2" + vite: "npm:^5.2.7" vite-bundle-visualizer: "npm:^1.1.0" vite-plugin-checker: "npm:^0.6.4" vite-plugin-eslint: "npm:^1.8.1" @@ -6553,6 +6720,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.38": + version: 8.4.38 + resolution: "postcss@npm:8.4.38" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.0" + source-map-js: "npm:^1.2.0" + checksum: 10c0/955407b8f70cf0c14acf35dab3615899a2a60a26718a63c848cf3c29f2467b0533991b985a2b994430d890bd7ec2b1963e36352b0774a19143b5f591540f7c06 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -7945,23 +8123,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.3.3": - version: 5.4.2 - resolution: "typescript@npm:5.4.2" +"typescript@npm:^5.4.3": + version: 5.4.3 + resolution: "typescript@npm:5.4.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/583ff68cafb0c076695f72d61df6feee71689568179fb0d3a4834dac343df6b6ed7cf7b6f6c801fa52d43cd1d324e2f2d8ae4497b09f9e6cfe3d80a6d6c9ca52 + checksum: 10c0/22443a8760c3668e256c0b34b6b45c359ef6cecc10c42558806177a7d500ab1a7d7aac1f976d712e26989ddf6731d2fbdd3212b7c73290a45127c1c43ba2005a languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin<compat/typescript>": - version: 5.4.2 - resolution: "typescript@patch:typescript@npm%3A5.4.2#optional!builtin<compat/typescript>::version=5.4.2&hash=5adc0c" +"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin<compat/typescript>": + version: 5.4.3 + resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin<compat/typescript>::version=5.4.3&hash=5adc0c" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/fcf6658073d07283910d9a0e04b1d5d0ebc822c04dbb7abdd74c3151c7aa92fcddbac7d799404e358197222006ccdc4c0db219d223d2ee4ccd9e2b01333b49be + checksum: 10c0/6e51f8b7e6ec55b897b9e56b67e864fe8f44e30f4a14357aad5dc0f7432db2f01efc0522df0b6c36d361c51f2dc3dcac5c832efd96a404cfabf884e915d38828 languageName: node linkType: hard @@ -8039,14 +8217,14 @@ __metadata: languageName: node linkType: hard -"usehooks-ts@npm:^3.0.1": - version: 3.0.1 - resolution: "usehooks-ts@npm:3.0.1" +"usehooks-ts@npm:^3.0.2": + version: 3.0.2 + resolution: "usehooks-ts@npm:3.0.2" dependencies: lodash.debounce: "npm:^4.0.8" peerDependencies: react: ^16.8.0 || ^17 || ^18 - checksum: 10c0/c1673758100251c35a62d8d50aafe375fdce253eaa4374e7f4686bfc68505bd1b24dfe1c605db6d2ddfbe3a4ac1eca826c52ce454c9e58d8da96a45c351e0e05 + checksum: 10c0/8df3f65fa343838b0dfe359d7c12162180d62eac04efc611fbda43bc9703afaa1d70007c19eadcc9021671aaa2105e659c07f2acb707013e017c966e2a2aec82 languageName: node linkType: hard @@ -8226,13 +8404,13 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.2.2": - version: 5.2.2 - resolution: "vite@npm:5.2.2" +"vite@npm:^5.2.7": + version: 5.2.7 + resolution: "vite@npm:5.2.7" dependencies: esbuild: "npm:^0.20.1" fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.36" + postcss: "npm:^8.4.38" rollup: "npm:^4.13.0" peerDependencies: "@types/node": ^18.0.0 || >=20.0.0 @@ -8262,7 +8440,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/472c6a1d41707ef51a5056ccc9e347333a3a975beb6069998d3d7a134555662b856e27628cc1354200c32d63373d7e4ef73385a4e90cc3032e48d06fb77928e5 + checksum: 10c0/ca927a8df388f75df194d5a5ba2be4ee46dc1d99d5be277f13c6d1ed4a4df833cc953741ef8e984061ea38b531df84e15e2a9f5deea1626317bcbec63c8ca01c languageName: node linkType: hard