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
This commit is contained in:
Arjun Porwal 2025-10-01 22:10:42 +05:30 committed by GitHub
parent 082b338f22
commit bf0cc2fcfb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 122 additions and 66 deletions

View File

@ -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';

View File

@ -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<Props> {
const { api, apiEndpoint } = useApi();
const [ahApi, setAhApi] = useState<ApiPromise>();
const [rcApi, setRcApi] = useState<ApiPromise>();
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 (
<StyledMain className={`${className} staking--App`}>

View File

@ -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;
};

View File

@ -30,6 +30,7 @@ const EMPTY_KEY = '0x';
function InjectKeys ({ onClose }: Props): React.ReactElement<Props> | 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<KeypairType>('sr25519');
const [publicKey, setPublicKey] = useState(EMPTY_KEY);
@ -86,6 +87,9 @@ function InjectKeys ({ onClose }: Props): React.ReactElement<Props> | null {
size='large'
>
<Modal.Content>
<Modal.Columns>
<MarkWarning content={t('This operation will be performed on the relay chain.')} />
</Modal.Columns>
<Modal.Columns hint={t('The seed and derivation path will be submitted to the validator node. this is an advanced operation, only to be performed when you are sure of the security and connection risks.')}>
<Input
autoFocus

View File

@ -5,8 +5,8 @@ import type { SessionInfo } from './types.js';
import React, { useEffect, useState } from 'react';
import { Input, Modal } from '@polkadot/react-components';
import { useApi } from '@polkadot/react-hooks';
import { Input, MarkWarning, Modal } from '@polkadot/react-components';
import { useApi, useStakingAsyncApis } from '@polkadot/react-hooks';
import { isHex } from '@polkadot/util';
import { useTranslation } from '../../translate.js';
@ -26,22 +26,26 @@ const EMPTY_PROOF = new Uint8Array();
function SessionKey ({ className = '', controllerId, onChange, stashId, withFocus, withSenders }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { isStakingAsyncPage, rcApi } = useStakingAsyncApis();
const [keys, setKeys] = useState<string | null>(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 (
<div className={className}>
<Modal.Columns>
<MarkWarning content={t('This operation will be performed on the relay chain.')} />
</Modal.Columns>
{withSenders && (
<SenderInfo
controllerId={controllerId}

View File

@ -83,6 +83,7 @@ export { useRegions } from './useRegions.js';
export { useRegistrars } from './useRegistrars.js';
export { useSavedFlags } from './useSavedFlags.js';
export { useScroll } from './useScroll.js';
export { useStakingAsyncApis } from './useStakingAsyncApis.js';
export { useStakingInfo } from './useStakingInfo.js';
export { useStepper } from './useStepper.js';
export { useSubidentities } from './useSubidentities.js';

View File

@ -0,0 +1,93 @@
// Copyright 2017-2025 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useEffect, useMemo, useState } from 'react';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { createWsEndpoints } from '@polkadot/apps-config';
import { createNamedHook, useApi } from '@polkadot/react-hooks';
export const getApi = async (url: string[]|string) => {
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<ApiPromise>();
const [rcApi, setRcApi] = useState<ApiPromise>();
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);

View File

@ -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<QueueTxResult> {
@ -93,6 +94,7 @@ function Signer ({ children, className = '' }: Props): React.ReactElement<Props>
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<Props>
}, [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 => {