From bf0cc2fcfb4e7c8bbeba5f03e31deb27d0b430be Mon Sep 17 00:00:00 2001 From: Arjun Porwal <65214523+ap211unitech@users.noreply.github.com> Date: Wed, 1 Oct 2025 22:10:42 +0530 Subject: [PATCH] Improved Validator DX for `set_keys` in staking POST AHM (#11863) * feat: improved validator set keys ux * feat: refactor InjectKeys and useStakingAsyncApis for improved API readiness checks --- .../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 | 4 + .../src/Actions/partials/SessionKey.tsx | 12 ++- packages/react-hooks/src/index.ts | 1 + .../react-hooks/src/useStakingAsyncApis.ts | 93 +++++++++++++++++++ packages/react-signer/src/index.tsx | 16 +++- 8 files changed, 122 insertions(+), 66 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 474678f5cc..02650564a8 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 6fd49c2ebf..db733e6310 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 ae8667fc95..0000000000 --- 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 87fa30be2a..d727898128 100644 --- a/packages/page-staking/src/Actions/Account/InjectKeys.tsx +++ b/packages/page-staking/src/Actions/Account/InjectKeys.tsx @@ -30,6 +30,7 @@ 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); @@ -86,6 +87,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, isApiReady } = useApi(); // <- check readiness + const [ahApi, setAhApi] = useState(); + const [rcApi, setRcApi] = useState(); + + 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)); + }, [isApiReady, api]); + + const rcEndPoints = useMemo(() => { + if (!isApiReady) { + return []; + } + + return (isRelayChain + ? apiEndpoint?.providers + : apiEndpoint?.valueRelay) || []; + }, [isApiReady, apiEndpoint, isRelayChain]); + + const ahEndPoints: string[] = useMemo(() => { + if (!isApiReady) { + return []; + } + + if (isRelayChain) { + return allEndPoints.find(({ genesisHashRelay, paraId }) => + paraId === 1000 && genesisHashRelay === api.genesisHash.toHex() + )?.providers || []; + } + + return apiEndpoint?.providers || []; + }, [isApiReady, api, apiEndpoint, isRelayChain]); + + useEffect(() => { + if (!isApiReady) { + return; + } + + if (isRelayChain) { + const ahUrl = ahEndPoints.at(Math.floor(Math.random() * ahEndPoints.length)); + + !!ahUrl && getApi(ahUrl).then(setAhApi).catch(console.error); + } else { + const rcUrl = rcEndPoints.at(Math.floor(Math.random() * rcEndPoints.length)); + + !!rcUrl && getApi(rcUrl).then(setRcApi).catch(console.error); + } + }, [ahEndPoints, isApiReady, isRelayChain, rcEndPoints]); + + return { + ahApi, + ahEndPoints, + isRelayChain, + isStakingAsyncPage, + rcApi, + rcEndPoints + }; +} + +export const useStakingAsyncApis = createNamedHook('useStakingAsyncApis', useStakingAsyncApisImpl); diff --git a/packages/react-signer/src/index.tsx b/packages/react-signer/src/index.tsx index c7f21fc4c9..0ee0a3b1e9 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 => {