Refactor: Move Relay and Asset Hub API Connections to Context (#11891)

* feat: add StakingAsync context and hooks for improved staking API integration

* refactor: rename isStakingAsyncPage to isStakingAsync for consistency across components

* feat: introduce useBestNumberRelay hook for enhanced best number retrieval based on staking async state

* feat: add 'Kusama Asset Hub' link to Polkassembly and Subsquare configurations
This commit is contained in:
Arjun Porwal 2025-10-09 09:47:24 +05:30 committed by GitHub
parent 7393de3071
commit 5630635d4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 139 additions and 98 deletions

View File

@ -25,6 +25,7 @@ export const PolkassemblyIo: ExternalDef = {
Khala: 'khala',
Kintsugi: 'kintsugi',
Kusama: 'kusama',
'Kusama Asset Hub': 'kusama',
'Kusama CC3': 'kusama',
'Kusama People': 'kusama',
Parallel: 'parallel',

View File

@ -24,6 +24,7 @@ export const Subsquare: ExternalDef = {
Karura: 'karura',
Khala: 'khala',
Kusama: 'kusama',
'Kusama Asset Hub': 'kusama',
'Paseo Testnet': 'paseo',
Phala: 'phala',
Polkadot: 'polkadot',

View File

@ -9,7 +9,7 @@ import { HashRouter } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';
import { ApiCtxRoot } from '@polkadot/react-api';
import { ApiStatsCtxRoot, BlockAuthorsCtxRoot, BlockEventsCtxRoot, KeyringCtxRoot, PayWithAssetCtxRoot, QueueCtxRoot, WindowSizeCtxRoot } from '@polkadot/react-hooks';
import { ApiStatsCtxRoot, BlockAuthorsCtxRoot, BlockEventsCtxRoot, KeyringCtxRoot, PayWithAssetCtxRoot, QueueCtxRoot, StakingAsyncApisCtxRoot, WindowSizeCtxRoot } from '@polkadot/react-hooks';
import { settings } from '@polkadot/ui-settings';
import BeforeApiInit from './overlays/BeforeInit.js';
@ -56,7 +56,9 @@ function Root ({ isElectron, store }: Props): React.ReactElement<Props> {
<HashRouter>
<WindowSizeCtxRoot>
<PayWithAssetCtxRoot>
<Apps />
<StakingAsyncApisCtxRoot>
<Apps />
</StakingAsyncApisCtxRoot>
</PayWithAssetCtxRoot>
</WindowSizeCtxRoot>
</HashRouter>

View File

@ -5,7 +5,7 @@ import type { BN } from '@polkadot/util';
import React from 'react';
import { useBestNumber } from '@polkadot/react-hooks';
import { useBestNumberRelay } from '@polkadot/react-hooks';
import { BlockToTime } from '@polkadot/react-query';
import { formatNumber } from '@polkadot/util';
@ -16,7 +16,7 @@ interface Props {
}
function RefEnd ({ className = '', label, when }: Props): React.ReactElement<Props> {
const bestNumber = useBestNumber();
const bestNumber = useBestNumberRelay();
return (
<td className={`${className} number`}>

View File

@ -10,7 +10,7 @@ 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 '@polkadot/react-hooks/ctx/StakingAsync';
import AssetHubSection from './ah.js';
import RelaySection from './relay.js';

View File

@ -26,20 +26,20 @@ 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 { isStakingAsync, rcApi } = useStakingAsyncApis();
const [keys, setKeys] = useState<string | null>(null);
useEffect((): void => {
try {
onChange({
sessionTx: isHex(keys)
? (isStakingAsyncPage ? rcApi : api)?.tx.session.setKeys(keys, EMPTY_PROOF)
? (isStakingAsync ? rcApi : api)?.tx.session.setKeys(keys, EMPTY_PROOF)
: null
});
} catch {
onChange({ sessionTx: null });
}
}, [api, isStakingAsyncPage, keys, onChange, rcApi]);
}, [api, isStakingAsync, keys, onChange, rcApi]);
return (
<div className={className}>

View File

@ -10,7 +10,7 @@ import type { BN } from '@polkadot/util';
import React, { useRef } from 'react';
import { withCalls, withMulti } from '@polkadot/react-api/hoc';
import { useBestNumber } from '@polkadot/react-hooks';
import { useBestNumberRelay } from '@polkadot/react-hooks';
import { BlockToTime, FormatBalance } from '@polkadot/react-query';
import { BN_MAX_INTEGER, BN_ZERO, bnMax, formatBalance, formatNumber, isObject } from '@polkadot/util';
@ -550,7 +550,7 @@ function renderBalances (props: Props, lookup: Record<string, string>, bestNumbe
function AddressInfo (props: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const bestNumber = useBestNumber();
const bestNumber = useBestNumberRelay();
const { children, className = '', extraInfo, withBalanceToggle, withHexSessionId } = props;
const lookup = useRef<Record<string, string>>({

View File

@ -0,0 +1,96 @@
// Copyright 2017-2025 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PropsWithChildren } from 'react';
import type { StakingAsyncApis } from './types.js';
import React, { useEffect, useMemo, useState } from 'react';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { createWsEndpoints } from '@polkadot/apps-config';
import { useApi } from '@polkadot/react-hooks';
const allEndPoints = createWsEndpoints((k, v) => v?.toString() || k);
export const getApi = async (url: string[]|string) => {
const api = await ApiPromise.create({
provider: new WsProvider(url)
});
await api.isReadyOrError;
return api;
};
const EMPTY_STATE: StakingAsyncApis = {
ahEndPoints: [],
isRelayChain: false,
isStakingAsync: false,
rcEndPoints: []
};
export const StakingAsyncApisCtx = React.createContext<StakingAsyncApis>(EMPTY_STATE);
export const StakingAsyncApisCtxRoot = ({ children }: PropsWithChildren) => {
const { api, isApiReady } = useApi();
return isApiReady &&
!!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient))
? <StakingAsyncProvider>{children}</StakingAsyncProvider>
: <>{children}</>;
};
export const StakingAsyncProvider = ({ children }: PropsWithChildren) => {
const { api, apiEndpoint } = useApi();
const [ahApi, setAhApi] = useState<ApiPromise>();
const [rcApi, setRcApi] = useState<ApiPromise>();
const isRelayChain = useMemo(() => {
return !!api.tx.stakingAhClient;
}, [api]);
const isStakingAsync = useMemo(() => {
return !!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient));
}, [api]);
const rcEndPoints = useMemo(() => {
return (isRelayChain
? apiEndpoint?.providers
: apiEndpoint?.valueRelay) || [];
}, [apiEndpoint, isRelayChain]);
const ahEndPoints: string[] = useMemo(() => {
if (isRelayChain) {
return allEndPoints.find(({ genesisHashRelay, paraId }) =>
paraId === 1000 && genesisHashRelay === api.genesisHash.toHex()
)?.providers || [];
}
return apiEndpoint?.providers || [];
}, [api, apiEndpoint, isRelayChain]);
useEffect(() => {
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, isRelayChain, rcEndPoints]);
const state = useMemo(() => ({
ahApi,
ahEndPoints,
isRelayChain,
isStakingAsync,
rcApi,
rcEndPoints
}), [ahApi, ahEndPoints, isRelayChain, isStakingAsync, rcApi, rcEndPoints]);
return <StakingAsyncApisCtx.Provider value={state}>
{children}
</StakingAsyncApisCtx.Provider>;
};

View File

@ -7,4 +7,5 @@ export { BlockEventsCtxRoot } from './BlockEvents.js';
export { KeyringCtxRoot } from './Keyring.js';
export { PayWithAssetCtxRoot } from './PayWithAsset.js';
export { QueueCtxRoot } from './Queue.js';
export { StakingAsyncApisCtxRoot } from './StakingAsync.js';
export { WindowSizeCtxRoot } from './WindowSize.js';

View File

@ -114,3 +114,12 @@ export interface WindowSize {
height: number;
width: number;
}
export interface StakingAsyncApis {
rcApi?: ApiPromise,
ahApi?: ApiPromise,
ahEndPoints: string[],
isRelayChain: boolean,
isStakingAsync: boolean,
rcEndPoints: string[]
}

View File

@ -18,7 +18,7 @@ export { useAssetInfos } from './useAssetInfos.js';
export { useAvailableSlashes } from './useAvailableSlashes.js';
export { useBalancesAll } from './useBalancesAll.js';
export { useBestHash } from './useBestHash.js';
export { useBestNumber } from './useBestNumber.js';
export { useBestNumber, useBestNumberRelay } from './useBestNumber.js';
export { useBlockAuthors } from './useBlockAuthors.js';
export { useBlockEvents } from './useBlockEvents.js';
export { useBlockInterval } from './useBlockInterval.js';

View File

@ -6,6 +6,7 @@ import type { BlockNumber } from '@polkadot/types/interfaces';
import { createNamedHook } from './createNamedHook.js';
import { useApi } from './useApi.js';
import { useCall } from './useCall.js';
import { useStakingAsyncApis } from './useStakingAsyncApis.js';
function useBestNumberImpl (): BlockNumber | undefined {
const { api } = useApi();
@ -13,4 +14,12 @@ function useBestNumberImpl (): BlockNumber | undefined {
return useCall<BlockNumber>(api.derive.chain.bestNumber);
}
function useBestNumberRelayImpl (): BlockNumber | undefined {
const { api } = useApi();
const { isStakingAsync, rcApi } = useStakingAsyncApis();
return useCall<BlockNumber>((isStakingAsync ? rcApi : api)?.derive.chain.bestNumber);
}
export const useBestNumber = createNamedHook('useBestNumber', useBestNumberImpl);
export const useBestNumberRelay = createNamedHook('useBestNumberRelay', useBestNumberRelayImpl);

View File

@ -1,93 +1,15 @@
// Copyright 2017-2025 @polkadot/react-hooks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { useEffect, useMemo, useState } from 'react';
import type { StakingAsyncApis } from './ctx/types.js';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { createWsEndpoints } from '@polkadot/apps-config';
import { createNamedHook, useApi } from '@polkadot/react-hooks';
import { useContext } from 'react';
export const getApi = async (url: string[]|string) => {
const api = await ApiPromise.create({
provider: new WsProvider(url)
});
import { StakingAsyncApisCtx } from './ctx/StakingAsync.js';
import { createNamedHook } from './createNamedHook.js';
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
};
function useStakingAsyncApisImpl (): StakingAsyncApis {
return useContext(StakingAsyncApisCtx);
}
export const useStakingAsyncApis = createNamedHook('useStakingAsyncApis', useStakingAsyncApisImpl);

View File

@ -94,7 +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 { isStakingAsync, rcApi } = useStakingAsyncApis();
const { currentItem, isRpc, isVisible, queueSize, requestAddress } = useMemo(
() => extractCurrent(txqueue),
@ -107,14 +107,14 @@ function Signer ({ children, className = '' }: Props): React.ReactElement<Props>
useEffect((): void => {
if (isRpc && currentItem) {
const apiForCall = isStakingAsyncPage
const apiForCall = isStakingAsync
// 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]);
}, [api, currentItem, isRpc, isStakingAsync, queueSetTxStatus, rcApi]);
const _onCancel = useCallback(
(): void => {