From f66c5c2b472f1d4c2ab786882d710ad60bc8a4f2 Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Fri, 26 Sep 2025 13:31:34 +0530 Subject: [PATCH 1/5] feat: improved validator set keys ux --- .../src/CommandCenter/index.tsx | 2 +- packages/page-staking-async/src/index.tsx | 46 +----------- packages/page-staking-async/src/utils.ts | 14 ---- .../src/Actions/Account/InjectKeys.tsx | 19 +++-- .../src/Actions/partials/SessionKey.tsx | 12 ++- packages/react-hooks/src/index.ts | 1 + .../react-hooks/src/useStakingAsyncApis.ts | 74 +++++++++++++++++++ 7 files changed, 100 insertions(+), 68 deletions(-) delete mode 100644 packages/page-staking-async/src/utils.ts create mode 100644 packages/react-hooks/src/useStakingAsyncApis.ts diff --git a/packages/page-staking-async/src/CommandCenter/index.tsx b/packages/page-staking-async/src/CommandCenter/index.tsx index 474678f5cc1f..02650564a80d 100644 --- a/packages/page-staking-async/src/CommandCenter/index.tsx +++ b/packages/page-staking-async/src/CommandCenter/index.tsx @@ -10,8 +10,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Dropdown, styled } from '@polkadot/react-components'; import { useApi } from '@polkadot/react-hooks'; +import { getApi } from '@polkadot/react-hooks/useStakingAsyncApis'; -import { getApi } from '../utils.js'; import AssetHubSection from './ah.js'; import RelaySection from './relay.js'; diff --git a/packages/page-staking-async/src/index.tsx b/packages/page-staking-async/src/index.tsx index 6fd49c2ebf49..db733e631023 100644 --- a/packages/page-staking-async/src/index.tsx +++ b/packages/page-staking-async/src/index.tsx @@ -1,58 +1,18 @@ // Copyright 2017-2025 @polkadot/app-staking-async authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { ApiPromise } from '@polkadot/api'; import type { AppProps as Props } from '@polkadot/react-components/types'; -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; -import { createWsEndpoints } from '@polkadot/apps-config'; import { styled } from '@polkadot/react-components'; -import { useApi } from '@polkadot/react-hooks'; +import { useStakingAsyncApis } from '@polkadot/react-hooks'; import StakingRelayApp from './Relay/index.js'; import StakingSystemApp from './System/index.js'; -import { getApi } from './utils.js'; - -const allEndPoints = createWsEndpoints((k, v) => v?.toString() || k); function StakingApp ({ basePath, className = '', onStatusChange }: Props): React.ReactElement { - const { api, apiEndpoint } = useApi(); - - const [ahApi, setAhApi] = useState(); - const [rcApi, setRcApi] = useState(); - - const isRelayChain = useMemo(() => !!api.tx.stakingAhClient, [api.tx.stakingAhClient]); - - const rcEndPoints = useMemo(() => { - return (isRelayChain - ? apiEndpoint?.providers - : apiEndpoint?.valueRelay) || []; - }, [apiEndpoint?.providers, apiEndpoint?.valueRelay, isRelayChain]); - - const ahEndPoints: string[] = useMemo(() => { - if (isRelayChain) { - return allEndPoints.find(({ genesisHashRelay, paraId }) => - paraId === 1000 && genesisHashRelay === api.genesisHash.toHex() - )?.providers || []; - } - - return apiEndpoint?.providers || []; - }, [api.genesisHash, apiEndpoint?.providers, isRelayChain]); - - useEffect(() => { - if (isRelayChain) { - // Pick random endpoint - const ahUrl = ahEndPoints.at(Math.floor(Math.random() * ahEndPoints.length)); - - !!ahUrl && getApi(ahUrl).then((ahApi) => setAhApi(ahApi)).catch(console.log); - } else { - // Pick random endpoint - const rcUrl = rcEndPoints.at(Math.floor(Math.random() * rcEndPoints.length)); - - !!rcUrl && getApi(rcUrl).then((rcApi) => setRcApi(rcApi)).catch(console.log); - } - }, [ahEndPoints, isRelayChain, rcEndPoints]); + const { ahApi, ahEndPoints, isRelayChain, rcApi, rcEndPoints } = useStakingAsyncApis(); return ( diff --git a/packages/page-staking-async/src/utils.ts b/packages/page-staking-async/src/utils.ts deleted file mode 100644 index ae8667fc95f0..000000000000 --- a/packages/page-staking-async/src/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ApiPromise, WsProvider } from '@polkadot/api'; - -export const getApi = async (url: string[]|string) => { - const api = await ApiPromise.create({ - provider: new WsProvider(url) - }); - - await api.isReadyOrError; - - return api; -}; diff --git a/packages/page-staking/src/Actions/Account/InjectKeys.tsx b/packages/page-staking/src/Actions/Account/InjectKeys.tsx index 87fa30be2ada..ad8d430a730d 100644 --- a/packages/page-staking/src/Actions/Account/InjectKeys.tsx +++ b/packages/page-staking/src/Actions/Account/InjectKeys.tsx @@ -6,7 +6,7 @@ import type { KeypairType } from '@polkadot/util-crypto/types'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Dropdown, Input, MarkWarning, Modal } from '@polkadot/react-components'; -import { useQueue } from '@polkadot/react-hooks'; +import { useQueue, useStakingAsyncApis } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { assert, u8aToHex } from '@polkadot/util'; import { keyExtractSuri, mnemonicValidate } from '@polkadot/util-crypto'; @@ -30,6 +30,8 @@ const EMPTY_KEY = '0x'; function InjectKeys ({ onClose }: Props): React.ReactElement | null { const { t } = useTranslation(); const { queueRpc } = useQueue(); + const { isStakingAsyncPage, rcApi } = useStakingAsyncApis(); + // this needs to align with what is set as the first value in `type` const [crypto, setCrypto] = useState('sr25519'); const [publicKey, setPublicKey] = useState(EMPTY_KEY); @@ -61,12 +63,14 @@ function InjectKeys ({ onClose }: Props): React.ReactElement | null { }, [crypto, suri]); const _onSubmit = useCallback( - (): void => queueRpc({ + (): void => isStakingAsyncPage + ? rcApi?.rpc.author.insertKey(keyType, suri, publicKey) as void + : queueRpc({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - rpc: { method: 'insertKey', section: 'author' } as any, - values: [keyType, suri, publicKey] - }), - [keyType, publicKey, queueRpc, suri] + rpc: { method: 'insertKey', section: 'author' } as any, + values: [keyType, suri, publicKey] + }), + [isStakingAsyncPage, keyType, publicKey, queueRpc, rcApi?.rpc.author, suri] ); const _cryptoOptions = useMemo( @@ -86,6 +90,9 @@ function InjectKeys ({ onClose }: Props): React.ReactElement | null { size='large' > + + + { const { t } = useTranslation(); const { api } = useApi(); + const { isStakingAsyncPage, rcApi } = useStakingAsyncApis(); const [keys, setKeys] = useState(null); useEffect((): void => { try { onChange({ sessionTx: isHex(keys) - ? api.tx.session.setKeys(keys, EMPTY_PROOF) + ? (isStakingAsyncPage ? rcApi : api)?.tx.session.setKeys(keys, EMPTY_PROOF) : null }); } catch { onChange({ sessionTx: null }); } - }, [api, keys, onChange]); + }, [api, isStakingAsyncPage, keys, onChange, rcApi]); return (
+ + + {withSenders && ( { + const api = await ApiPromise.create({ + provider: new WsProvider(url) + }); + + await api.isReadyOrError; + + return api; +}; + +const allEndPoints = createWsEndpoints((k, v) => v?.toString() || k); + +function useStakingAsyncApisImpl () { + const { api, apiEndpoint } = useApi(); + + const [ahApi, setAhApi] = useState(); + const [rcApi, setRcApi] = useState(); + + const isRelayChain = useMemo(() => !!api.tx.stakingAhClient, [api.tx.stakingAhClient]); + + const isStakingAsyncPage = useMemo(() => { + return !!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient)); + }, [api.tx.staking, api.tx.stakingAhClient, api.tx.stakingRcClient]); + + const rcEndPoints = useMemo(() => { + return (isRelayChain + ? apiEndpoint?.providers + : apiEndpoint?.valueRelay) || []; + }, [apiEndpoint?.providers, apiEndpoint?.valueRelay, isRelayChain]); + + const ahEndPoints: string[] = useMemo(() => { + if (isRelayChain) { + return allEndPoints.find(({ genesisHashRelay, paraId }) => + paraId === 1000 && genesisHashRelay === api.genesisHash.toHex() + )?.providers || []; + } + + return apiEndpoint?.providers || []; + }, [api.genesisHash, apiEndpoint?.providers, isRelayChain]); + + useEffect(() => { + if (isRelayChain) { + // Pick random endpoint + const ahUrl = ahEndPoints.at(Math.floor(Math.random() * ahEndPoints.length)); + + !!ahUrl && getApi(ahUrl).then((ahApi) => setAhApi(ahApi)).catch(console.log); + } else { + // Pick random endpoint + const rcUrl = rcEndPoints.at(Math.floor(Math.random() * rcEndPoints.length)); + + !!rcUrl && getApi(rcUrl).then((rcApi) => setRcApi(rcApi)).catch(console.log); + } + }, [ahEndPoints, isRelayChain, rcEndPoints]); + + return { + ahApi, + ahEndPoints, + isRelayChain, + isStakingAsyncPage, + rcApi, + rcEndPoints + }; +} + +export const useStakingAsyncApis = createNamedHook('useStakingAsyncApis', useStakingAsyncApisImpl); From 9a0d9fe9713b798e13c5b7fbf2a1696baeeac26d Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Mon, 29 Sep 2025 10:26:02 +0530 Subject: [PATCH 2/5] feat: refactor InjectKeys and useStakingAsyncApis for improved API readiness checks --- .../src/Actions/Account/InjectKeys.tsx | 15 +++---- .../react-hooks/src/useStakingAsyncApis.ts | 41 ++++++++++++++----- packages/react-signer/src/index.tsx | 16 ++++++-- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/packages/page-staking/src/Actions/Account/InjectKeys.tsx b/packages/page-staking/src/Actions/Account/InjectKeys.tsx index ad8d430a730d..d727898128e1 100644 --- a/packages/page-staking/src/Actions/Account/InjectKeys.tsx +++ b/packages/page-staking/src/Actions/Account/InjectKeys.tsx @@ -6,7 +6,7 @@ import type { KeypairType } from '@polkadot/util-crypto/types'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, Dropdown, Input, MarkWarning, Modal } from '@polkadot/react-components'; -import { useQueue, useStakingAsyncApis } from '@polkadot/react-hooks'; +import { useQueue } from '@polkadot/react-hooks'; import { keyring } from '@polkadot/ui-keyring'; import { assert, u8aToHex } from '@polkadot/util'; import { keyExtractSuri, mnemonicValidate } from '@polkadot/util-crypto'; @@ -30,7 +30,6 @@ const EMPTY_KEY = '0x'; function InjectKeys ({ onClose }: Props): React.ReactElement | null { const { t } = useTranslation(); const { queueRpc } = useQueue(); - const { isStakingAsyncPage, rcApi } = useStakingAsyncApis(); // this needs to align with what is set as the first value in `type` const [crypto, setCrypto] = useState('sr25519'); @@ -63,14 +62,12 @@ function InjectKeys ({ onClose }: Props): React.ReactElement | null { }, [crypto, suri]); const _onSubmit = useCallback( - (): void => isStakingAsyncPage - ? rcApi?.rpc.author.insertKey(keyType, suri, publicKey) as void - : queueRpc({ + (): void => queueRpc({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - rpc: { method: 'insertKey', section: 'author' } as any, - values: [keyType, suri, publicKey] - }), - [isStakingAsyncPage, keyType, publicKey, queueRpc, rcApi?.rpc.author, suri] + rpc: { method: 'insertKey', section: 'author' } as any, + values: [keyType, suri, publicKey] + }), + [keyType, publicKey, queueRpc, suri] ); const _cryptoOptions = useMemo( diff --git a/packages/react-hooks/src/useStakingAsyncApis.ts b/packages/react-hooks/src/useStakingAsyncApis.ts index 0598004bbf68..32b717ad08ea 100644 --- a/packages/react-hooks/src/useStakingAsyncApis.ts +++ b/packages/react-hooks/src/useStakingAsyncApis.ts @@ -20,24 +20,41 @@ export const getApi = async (url: string[]|string) => { const allEndPoints = createWsEndpoints((k, v) => v?.toString() || k); function useStakingAsyncApisImpl () { - const { api, apiEndpoint } = useApi(); - + const { api, apiEndpoint, isApiReady } = useApi(); // <- check readiness const [ahApi, setAhApi] = useState(); const [rcApi, setRcApi] = useState(); - const isRelayChain = useMemo(() => !!api.tx.stakingAhClient, [api.tx.stakingAhClient]); + const isRelayChain = useMemo(() => { + if (!isApiReady) { + return false; + } + + return !!api.tx.stakingAhClient; + }, [isApiReady, api]); const isStakingAsyncPage = useMemo(() => { + if (!isApiReady) { + return false; + } + return !!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient)); - }, [api.tx.staking, api.tx.stakingAhClient, api.tx.stakingRcClient]); + }, [isApiReady, api]); const rcEndPoints = useMemo(() => { + if (!isApiReady) { + return []; + } + return (isRelayChain ? apiEndpoint?.providers : apiEndpoint?.valueRelay) || []; - }, [apiEndpoint?.providers, apiEndpoint?.valueRelay, isRelayChain]); + }, [isApiReady, apiEndpoint, isRelayChain]); const ahEndPoints: string[] = useMemo(() => { + if (!isApiReady) { + return []; + } + if (isRelayChain) { return allEndPoints.find(({ genesisHashRelay, paraId }) => paraId === 1000 && genesisHashRelay === api.genesisHash.toHex() @@ -45,21 +62,23 @@ function useStakingAsyncApisImpl () { } return apiEndpoint?.providers || []; - }, [api.genesisHash, apiEndpoint?.providers, isRelayChain]); + }, [isApiReady, api, apiEndpoint, isRelayChain]); useEffect(() => { + if (!isApiReady) { + return; + } + if (isRelayChain) { - // Pick random endpoint const ahUrl = ahEndPoints.at(Math.floor(Math.random() * ahEndPoints.length)); - !!ahUrl && getApi(ahUrl).then((ahApi) => setAhApi(ahApi)).catch(console.log); + !!ahUrl && getApi(ahUrl).then(setAhApi).catch(console.error); } else { - // Pick random endpoint const rcUrl = rcEndPoints.at(Math.floor(Math.random() * rcEndPoints.length)); - !!rcUrl && getApi(rcUrl).then((rcApi) => setRcApi(rcApi)).catch(console.log); + !!rcUrl && getApi(rcUrl).then(setRcApi).catch(console.error); } - }, [ahEndPoints, isRelayChain, rcEndPoints]); + }, [ahEndPoints, isApiReady, isRelayChain, rcEndPoints]); return { ahApi, diff --git a/packages/react-signer/src/index.tsx b/packages/react-signer/src/index.tsx index c7f21fc4c910..0ee0a3b1e956 100644 --- a/packages/react-signer/src/index.tsx +++ b/packages/react-signer/src/index.tsx @@ -9,7 +9,7 @@ import type { DefinitionRpcExt } from '@polkadot/types/types'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Modal, styled } from '@polkadot/react-components'; -import { useApi, useQueue } from '@polkadot/react-hooks'; +import { useApi, useQueue, useStakingAsyncApis } from '@polkadot/react-hooks'; import { assert, isFunction, loggerFormat } from '@polkadot/util'; import { useTranslation } from './translate.js'; @@ -28,6 +28,7 @@ interface ItemState { const NOOP = () => undefined; +const STAKING_RELAY_CHAIN_RPC = ['author.insertKey']; const AVAIL_STATUS = ['queued', 'qr', 'signing']; async function submitRpc (api: ApiPromise, { method, section }: DefinitionRpcExt, values: unknown[]): Promise { @@ -93,6 +94,7 @@ function Signer ({ children, className = '' }: Props): React.ReactElement const { t } = useTranslation(); const { queueSetTxStatus, txqueue } = useQueue(); const [isQueueSubmit, setIsQueueSubmit] = useState(false); + const { isStakingAsyncPage, rcApi } = useStakingAsyncApis(); const { currentItem, isRpc, isVisible, queueSize, requestAddress } = useMemo( () => extractCurrent(txqueue), @@ -104,9 +106,15 @@ function Signer ({ children, className = '' }: Props): React.ReactElement }, [queueSize]); useEffect((): void => { - isRpc && currentItem && - sendRpc(api, queueSetTxStatus, currentItem).catch(console.error); - }, [api, isRpc, currentItem, queueSetTxStatus]); + if (isRpc && currentItem) { + const apiForCall = isStakingAsyncPage + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ? STAKING_RELAY_CHAIN_RPC.includes(`${currentItem?.rpc.section}.${currentItem?.rpc.method}`) ? (rcApi!) : api + : api; + + sendRpc(apiForCall, queueSetTxStatus, currentItem).catch(console.error); + } + }, [api, currentItem, isRpc, isStakingAsyncPage, queueSetTxStatus, rcApi]); const _onCancel = useCallback( (): void => { From 3bfabb026435b7fdffc0ad06c814df1bef9a5867 Mon Sep 17 00:00:00 2001 From: Arjun Porwal Date: Tue, 30 Sep 2025 14:39:25 +0530 Subject: [PATCH 3/5] feat: migrated actions to staking async --- .../src/Actions/Account/BondExtra.tsx | 109 +++++ .../src/Actions/Account/InjectKeys.tsx | 137 +++++++ .../Actions/Account/InputValidateAmount.tsx | 111 ++++++ .../Account/InputValidationController.tsx | 86 ++++ .../Account/InputValidationSessionKey.tsx | 43 ++ .../InputValidationUnstakeThreshold.tsx | 46 +++ .../src/Actions/Account/KickNominees.tsx | 93 +++++ .../src/Actions/Account/ListNominees.tsx | 129 ++++++ .../src/Actions/Account/Nominate.tsx | 71 ++++ .../src/Actions/Account/Rebond.tsx | 73 ++++ .../Actions/Account/SetControllerAccount.tsx | 92 +++++ .../Actions/Account/SetRewardDestination.tsx | 90 +++++ .../src/Actions/Account/SetSessionKey.tsx | 52 +++ .../src/Actions/Account/Unbond.tsx | 92 +++++ .../src/Actions/Account/Validate.tsx | 55 +++ .../src/Actions/Account/WarnBond.tsx | 32 ++ .../src/Actions/Account/index.tsx | 377 ++++++++++++++++++ .../src/Actions/Accounts.tsx | 59 +++ .../src/Actions/NewNominator.tsx | 135 +++++++ .../src/Actions/NewStash.tsx | 60 +++ .../src/Actions/NewValidator.tsx | 143 +++++++ .../src/Actions/Pool/Account.tsx | 201 ++++++++++ .../src/Actions/Pool/BondExtra.tsx | 92 +++++ .../src/Actions/Pool/Unbond.tsx | 84 ++++ .../src/Actions/Pool/index.tsx | 49 +++ .../src/Actions/Pool/types.ts | 10 + .../src/Actions/Pool/useAccountInfo.ts | 36 ++ .../page-staking-async/src/Actions/Pools.tsx | 61 +++ .../src/Actions/destOptions.tsx | 19 + .../page-staking-async/src/Actions/index.tsx | 210 ++++++++++ .../src/Actions/partials/Bond.tsx | 218 ++++++++++ .../src/Actions/partials/Nominate.tsx | 175 ++++++++ .../src/Actions/partials/PoolInfo.tsx | 44 ++ .../src/Actions/partials/SenderInfo.tsx | 50 +++ .../src/Actions/partials/SessionKey.tsx | 68 ++++ .../src/Actions/partials/Validate.tsx | 104 +++++ .../src/Actions/partials/types.ts | 23 ++ .../page-staking-async/src/Actions/types.ts | 25 ++ .../src/Actions/useInactives.ts | 255 ++++++++++++ .../src/Actions/useSlashingSpans.ts | 22 + .../src/Actions/useUnbondDuration.ts | 24 ++ tsconfig.base.json | 1 + 42 files changed, 3856 insertions(+) create mode 100644 packages/page-staking-async/src/Actions/Account/BondExtra.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/InjectKeys.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/InputValidateAmount.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/InputValidationController.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/InputValidationSessionKey.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/InputValidationUnstakeThreshold.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/KickNominees.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/ListNominees.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/Nominate.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/Rebond.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/SetControllerAccount.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/SetRewardDestination.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/SetSessionKey.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/Unbond.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/Validate.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/WarnBond.tsx create mode 100644 packages/page-staking-async/src/Actions/Account/index.tsx create mode 100644 packages/page-staking-async/src/Actions/Accounts.tsx create mode 100644 packages/page-staking-async/src/Actions/NewNominator.tsx create mode 100644 packages/page-staking-async/src/Actions/NewStash.tsx create mode 100644 packages/page-staking-async/src/Actions/NewValidator.tsx create mode 100644 packages/page-staking-async/src/Actions/Pool/Account.tsx create mode 100644 packages/page-staking-async/src/Actions/Pool/BondExtra.tsx create mode 100644 packages/page-staking-async/src/Actions/Pool/Unbond.tsx create mode 100644 packages/page-staking-async/src/Actions/Pool/index.tsx create mode 100644 packages/page-staking-async/src/Actions/Pool/types.ts create mode 100644 packages/page-staking-async/src/Actions/Pool/useAccountInfo.ts create mode 100644 packages/page-staking-async/src/Actions/Pools.tsx create mode 100644 packages/page-staking-async/src/Actions/destOptions.tsx create mode 100644 packages/page-staking-async/src/Actions/index.tsx create mode 100644 packages/page-staking-async/src/Actions/partials/Bond.tsx create mode 100644 packages/page-staking-async/src/Actions/partials/Nominate.tsx create mode 100644 packages/page-staking-async/src/Actions/partials/PoolInfo.tsx create mode 100644 packages/page-staking-async/src/Actions/partials/SenderInfo.tsx create mode 100644 packages/page-staking-async/src/Actions/partials/SessionKey.tsx create mode 100644 packages/page-staking-async/src/Actions/partials/Validate.tsx create mode 100644 packages/page-staking-async/src/Actions/partials/types.ts create mode 100644 packages/page-staking-async/src/Actions/types.ts create mode 100644 packages/page-staking-async/src/Actions/useInactives.ts create mode 100644 packages/page-staking-async/src/Actions/useSlashingSpans.ts create mode 100644 packages/page-staking-async/src/Actions/useUnbondDuration.ts diff --git a/packages/page-staking-async/src/Actions/Account/BondExtra.tsx b/packages/page-staking-async/src/Actions/Account/BondExtra.tsx new file mode 100644 index 000000000000..945cc01aa870 --- /dev/null +++ b/packages/page-staking-async/src/Actions/Account/BondExtra.tsx @@ -0,0 +1,109 @@ +// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { ApiPromise } from '@polkadot/api'; +import type { DeriveBalancesAll, DeriveStakingAccount } from '@polkadot/api-derive/types'; +import type { AmountValidateState } from '../types.js'; + +import React, { useMemo, useState } from 'react'; + +import { InputAddress, InputBalance, Modal, TxButton } from '@polkadot/react-components'; +import { useApi, useCall } from '@polkadot/react-hooks'; +import { BalanceFree } from '@polkadot/react-query'; +import { BN, BN_ZERO } from '@polkadot/util'; + +import { useTranslation } from '../../translate.js'; +import ValidateAmount from './InputValidateAmount.js'; + +interface Props { + controllerId: string | null; + onClose: () => void; + stakingInfo?: DeriveStakingAccount; + stashId: string; +} + +function calcBalance (api: ApiPromise, stakingInfo?: DeriveStakingAccount, stashBalance?: DeriveBalancesAll): BN | null { + if (stakingInfo?.stakingLedger && stashBalance) { + const sumUnlocking = (stakingInfo.unlocking || []).reduce((acc, { value }) => acc.iadd(value), new BN(0)); + const redeemable = stakingInfo.redeemable || BN_ZERO; + const available = stashBalance.freeBalance.sub(stakingInfo.stakingLedger.active?.unwrap() || BN_ZERO).sub(sumUnlocking).sub(redeemable); + + return available.gt(api.consts.balances.existentialDeposit) + ? available.sub(api.consts.balances.existentialDeposit) + : BN_ZERO; + } + + return null; +} + +function BondExtra ({ controllerId, onClose, stakingInfo, stashId }: Props): React.ReactElement { + const { t } = useTranslation(); + const { api } = useApi(); + const [amountError, setAmountError] = useState(null); + const [maxAdditional, setMaxAdditional] = useState(); + const stashBalance = useCall(api.derive.balances?.all, [stashId]); + const currentAmount = useMemo( + () => stakingInfo?.stakingLedger?.active?.unwrap(), + [stakingInfo] + ); + + const startBalance = useMemo( + () => calcBalance(api, stakingInfo, stashBalance), + [api, stakingInfo, stashBalance] + ); + + return ( + + + + + + {startBalance && ( + + {t('balance')}} + params={stashId} + /> + } + onChange={setMaxAdditional} + /> + + + )} + + + + + + ); +} + +export default React.memo(BondExtra); diff --git a/packages/page-staking-async/src/Actions/Account/InjectKeys.tsx b/packages/page-staking-async/src/Actions/Account/InjectKeys.tsx new file mode 100644 index 000000000000..1d9cd0430156 --- /dev/null +++ b/packages/page-staking-async/src/Actions/Account/InjectKeys.tsx @@ -0,0 +1,137 @@ +// Copyright 2017-2025 @polkadot/app-staking-async authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { KeypairType } from '@polkadot/util-crypto/types'; + +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { Button, Dropdown, Input, MarkWarning, Modal } from '@polkadot/react-components'; +import { useQueue } from '@polkadot/react-hooks'; +import { keyring } from '@polkadot/ui-keyring'; +import { assert, u8aToHex } from '@polkadot/util'; +import { keyExtractSuri, mnemonicValidate } from '@polkadot/util-crypto'; + +import { useTranslation } from '../../translate.js'; + +interface Props { + onClose: () => void; +} + +const CRYPTO_MAP: Record = { + aura: ['ed25519', 'sr25519'], + babe: ['sr25519'], + gran: ['ed25519'], + imon: ['ed25519', 'sr25519'], + para: ['sr25519'] +}; + +const EMPTY_KEY = '0x'; + +function InjectKeys ({ onClose }: Props): React.ReactElement | null { + const { t } = useTranslation(); + const { queueRpc } = useQueue(); + + // this needs to align with what is set as the first value in `type` + const [crypto, setCrypto] = useState('sr25519'); + const [publicKey, setPublicKey] = useState(EMPTY_KEY); + const [suri, setSuri] = useState(''); + const [keyType, setKeyType] = useState('babe'); + + const keyTypeOptRef = useRef([ + { text: t('Aura'), value: 'aura' }, + { text: t('Babe'), value: 'babe' }, + { text: t('Grandpa'), value: 'gran' }, + { text: t('I\'m Online'), value: 'imon' }, + { text: t('Parachains'), value: 'para' } + ]); + + useEffect((): void => { + setCrypto(CRYPTO_MAP[keyType][0]); + }, [keyType]); + + useEffect((): void => { + try { + const { phrase } = keyExtractSuri(suri); + + assert(mnemonicValidate(phrase), 'Invalid mnemonic phrase'); + + setPublicKey(u8aToHex(keyring.createFromUri(suri, {}, crypto).publicKey)); + } catch { + setPublicKey(EMPTY_KEY); + } + }, [crypto, suri]); + + const _onSubmit = useCallback( + (): void => queueRpc({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + rpc: { method: 'insertKey', section: 'author' } as any, + values: [keyType, suri, publicKey] + }), + [keyType, publicKey, queueRpc, suri] + ); + + const _cryptoOptions = useMemo( + () => CRYPTO_MAP[keyType].map((value): { text: string; value: KeypairType } => ({ + text: value === 'ed25519' + ? t('ed25519, Edwards') + : t('sr15519, Schnorrkel'), + value + })), + [keyType, t] + ); + + return ( + + + + + + + + + + + + + + + + + + +