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}
-
- )}
-
+
+
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 (
- <>
-
-
-
- {targets.length > 0 ? (
-
- ) : (
- {t('poolIsNotNominating')}
- )}
-
-
- >
- );
-};
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 (
+
+
+ {syncing ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+};
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 (
{
- 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 }) => (
-
-
-
-
-
-
-
-);
+export const Status = ({ height }: { height: number }) => {
+ const { syncing } = useSyncing();
+ const { inSetup } = useStaking();
+ const { activeAccount } = useActiveAccounts();
+ const { isReadOnlyAccount } = useImportedAccounts();
+
+ return (
+
+
+
+
+
+ {!syncing ? (
+ !inSetup() ? (
+ <>
+
+
+ >
+ ) : (
+ !isReadOnlyAccount(activeAccount) && (
+
+ )
+ )
+ ) : (
+
+ )}
+
+ );
+};
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 = () => {
-
-
-
-
+
+
+
+
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 (
- <>
-
-
-
-
- {
- if (extractUrlValue('f') === 'overview') {
- navigate('/overview');
- } else {
- removeVarFromUrlHash('f');
- setOnNominatorSetup(false);
- }
- }}
- />
-
-
- {
- removeVarFromUrlHash('f');
- setOnNominatorSetup(false);
- removeSetupProgress('nominator', activeAccount);
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
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 {onNominatorSetup ? : };
-};
+export const Nominate = () => (
+
+
+
+);
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(1);
- // current render iteration
- const [renderIteration, _setRenderIteration] = useState(1);
-
// manipulated list (ordering, filtering) of payouts
const [payouts, setPayouts] = useState(initialPayouts);
// is this the initial fetch
const [fetched, setFetched] = useState(false);
- // render throttle iteration
- const renderIterationRef = useRef(renderIteration);
- const setRenderIteration = (iter: number) => {
- renderIterationRef.current = iter;
- _setRenderIteration(iter);
- };
-
// pagination
- const totalPages = Math.ceil(payouts.length / listItemsPerPage);
- const pageEnd = page * listItemsPerPage - 1;
- const pageStart = pageEnd - (listItemsPerPage - 1);
-
- // render batch
- const batchEnd = Math.min(
- renderIteration * listItemsPerBatch - 1,
- listItemsPerPage
- );
+ const totalPages = Math.ceil(payouts.length / payoutsPerPage);
+ const pageEnd = page * payoutsPerPage - 1;
+ const pageStart = pageEnd - (payoutsPerPage - 1);
// refetch list when list changes
useEffect(() => {
@@ -87,24 +70,11 @@ export const PayoutListInner = ({
}
}, [isReady, fetched, activeEra.index]);
- // render throttle
- useEffect(() => {
- if (!(batchEnd >= pageEnd || disableThrottle)) {
- setTimeout(() => {
- setRenderIteration(renderIterationRef.current + 1);
- }, 500);
- }
- }, [renderIterationRef.current]);
-
// get list items to render
let listPayouts = [];
// get throttled subset or entire list
- if (!disableThrottle) {
- listPayouts = payouts.slice(pageStart).slice(0, listItemsPerPage);
- } else {
- listPayouts = payouts;
- }
+ listPayouts = payouts.slice(pageStart).slice(0, payoutsPerPage);
if (!payouts.length) {
return null;
diff --git a/src/pages/Payouts/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 (
- <>
-
-
-
-
- setOnPoolSetup(false)}
- />
-
-
- {
- setOnPoolSetup(false);
- removeSetupProgress('pool', activeAccount);
- }}
- />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
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 && (
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 (
- {syncing ? (
-
- ) : canNominate && !isNominating && state !== 'Destroying' ? (
+ {canNominate && !isNominating && state !== 'Destroying' ? (
<>
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 (
+
+
+ {syncing ? (
+
+ ) : (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+};
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 (
{
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 (
-
+
-
- {activePool && (
- <>
-
-
- >
+
+ {!syncing ? (
+ activePool && !!membership ? (
+ <>
+
+
+ >
+ ) : (
+ membership === null &&
+ !isReadOnlyAccount(activeAccount) &&
+ )
+ ) : (
+
)}
);
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 (
<>
0
+ !poolMembersipSyncing() && poolRoleCount > 0
? {
title: t('pools.allRoles'),
onClick: () =>
@@ -111,21 +110,18 @@ export const HomeInner = () => {
- {state === 'Destroying' ? (
-
- ) : (
-
- )}
+
+
-
-
-
-
+
+
+
+
{activePool !== null && (
<>
@@ -148,10 +144,6 @@ export const HomeInner = () => {
{
- const { onPoolSetup } = useSetup();
- return onPoolSetup ? : ;
-};
+export const Pools = () => ;
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> = {};
- 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 = {};
- 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":
- version: 5.4.2
- resolution: "typescript@patch:typescript@npm%3A5.4.2#optional!builtin::version=5.4.2&hash=5adc0c"
+"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin":
+ version: 5.4.3
+ resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin::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