diff --git a/packages/apps-config/src/links/polkassembly.ts b/packages/apps-config/src/links/polkassembly.ts index 7e09a68200..399b5fd3a2 100644 --- a/packages/apps-config/src/links/polkassembly.ts +++ b/packages/apps-config/src/links/polkassembly.ts @@ -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', diff --git a/packages/apps-config/src/links/subsquare.ts b/packages/apps-config/src/links/subsquare.ts index d8b362a864..e34d238bd2 100644 --- a/packages/apps-config/src/links/subsquare.ts +++ b/packages/apps-config/src/links/subsquare.ts @@ -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', diff --git a/packages/apps/src/Root.tsx b/packages/apps/src/Root.tsx index d6f9d08bc3..b6ebc335f9 100644 --- a/packages/apps/src/Root.tsx +++ b/packages/apps/src/Root.tsx @@ -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 { - + + + diff --git a/packages/page-referenda/src/Referenda/RefEnd.tsx b/packages/page-referenda/src/Referenda/RefEnd.tsx index 1c7c71b231..b5c3b2af96 100644 --- a/packages/page-referenda/src/Referenda/RefEnd.tsx +++ b/packages/page-referenda/src/Referenda/RefEnd.tsx @@ -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 { - const bestNumber = useBestNumber(); + const bestNumber = useBestNumberRelay(); return ( diff --git a/packages/page-staking-async/src/CommandCenter/index.tsx b/packages/page-staking-async/src/CommandCenter/index.tsx index 02650564a8..9233467996 100644 --- a/packages/page-staking-async/src/CommandCenter/index.tsx +++ b/packages/page-staking-async/src/CommandCenter/index.tsx @@ -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'; diff --git a/packages/page-staking/src/Actions/partials/SessionKey.tsx b/packages/page-staking/src/Actions/partials/SessionKey.tsx index 68f61226ee..624ee3a243 100644 --- a/packages/page-staking/src/Actions/partials/SessionKey.tsx +++ b/packages/page-staking/src/Actions/partials/SessionKey.tsx @@ -26,20 +26,20 @@ const EMPTY_PROOF = new Uint8Array(); function SessionKey ({ className = '', controllerId, onChange, stashId, withFocus, withSenders }: Props): React.ReactElement { const { t } = useTranslation(); const { api } = useApi(); - const { isStakingAsyncPage, rcApi } = useStakingAsyncApis(); + const { isStakingAsync, rcApi } = useStakingAsyncApis(); const [keys, setKeys] = useState(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 (
diff --git a/packages/react-components/src/AddressInfo.tsx b/packages/react-components/src/AddressInfo.tsx index 78e4e8117c..b134d0be13 100644 --- a/packages/react-components/src/AddressInfo.tsx +++ b/packages/react-components/src/AddressInfo.tsx @@ -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, bestNumbe function AddressInfo (props: Props): React.ReactElement { const { t } = useTranslation(); - const bestNumber = useBestNumber(); + const bestNumber = useBestNumberRelay(); const { children, className = '', extraInfo, withBalanceToggle, withHexSessionId } = props; const lookup = useRef>({ diff --git a/packages/react-hooks/src/ctx/StakingAsync.tsx b/packages/react-hooks/src/ctx/StakingAsync.tsx new file mode 100644 index 0000000000..c8a057d8b7 --- /dev/null +++ b/packages/react-hooks/src/ctx/StakingAsync.tsx @@ -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(EMPTY_STATE); + +export const StakingAsyncApisCtxRoot = ({ children }: PropsWithChildren) => { + const { api, isApiReady } = useApi(); + + return isApiReady && + !!((api.tx.stakingAhClient) || (api.tx.staking && api.tx.stakingRcClient)) + ? {children} + : <>{children}; +}; + +export const StakingAsyncProvider = ({ children }: PropsWithChildren) => { + const { api, apiEndpoint } = useApi(); + const [ahApi, setAhApi] = useState(); + const [rcApi, setRcApi] = useState(); + + 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 + {children} + ; +}; diff --git a/packages/react-hooks/src/ctx/index.ts b/packages/react-hooks/src/ctx/index.ts index fe2e5ecb53..b1a6b9610d 100644 --- a/packages/react-hooks/src/ctx/index.ts +++ b/packages/react-hooks/src/ctx/index.ts @@ -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'; diff --git a/packages/react-hooks/src/ctx/types.ts b/packages/react-hooks/src/ctx/types.ts index e0eae90fea..9b6b679da3 100644 --- a/packages/react-hooks/src/ctx/types.ts +++ b/packages/react-hooks/src/ctx/types.ts @@ -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[] +} diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts index 1d1d4941bf..b8f0e5fb41 100644 --- a/packages/react-hooks/src/index.ts +++ b/packages/react-hooks/src/index.ts @@ -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'; diff --git a/packages/react-hooks/src/useBestNumber.ts b/packages/react-hooks/src/useBestNumber.ts index 64340ded4e..50bc69e90b 100644 --- a/packages/react-hooks/src/useBestNumber.ts +++ b/packages/react-hooks/src/useBestNumber.ts @@ -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(api.derive.chain.bestNumber); } +function useBestNumberRelayImpl (): BlockNumber | undefined { + const { api } = useApi(); + const { isStakingAsync, rcApi } = useStakingAsyncApis(); + + return useCall((isStakingAsync ? rcApi : api)?.derive.chain.bestNumber); +} + export const useBestNumber = createNamedHook('useBestNumber', useBestNumberImpl); +export const useBestNumberRelay = createNamedHook('useBestNumberRelay', useBestNumberRelayImpl); diff --git a/packages/react-hooks/src/useStakingAsyncApis.ts b/packages/react-hooks/src/useStakingAsyncApis.ts index 32b717ad08..5aceeb47a7 100644 --- a/packages/react-hooks/src/useStakingAsyncApis.ts +++ b/packages/react-hooks/src/useStakingAsyncApis.ts @@ -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(); - 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 - }; +function useStakingAsyncApisImpl (): StakingAsyncApis { + return useContext(StakingAsyncApisCtx); } export const useStakingAsyncApis = createNamedHook('useStakingAsyncApis', useStakingAsyncApisImpl); diff --git a/packages/react-signer/src/index.tsx b/packages/react-signer/src/index.tsx index 0ee0a3b1e9..00182218dc 100644 --- a/packages/react-signer/src/index.tsx +++ b/packages/react-signer/src/index.tsx @@ -94,7 +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 { isStakingAsync, rcApi } = useStakingAsyncApis(); const { currentItem, isRpc, isVisible, queueSize, requestAddress } = useMemo( () => extractCurrent(txqueue), @@ -107,14 +107,14 @@ function Signer ({ children, className = '' }: Props): React.ReactElement 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 => {