diff --git a/packages/extension-polkagate/src/util/misc.ts b/packages/extension-polkagate/src/util/misc.ts index e9513f4ec5..cc0d7be3e0 100644 --- a/packages/extension-polkagate/src/util/misc.ts +++ b/packages/extension-polkagate/src/util/misc.ts @@ -167,21 +167,8 @@ export function extractBaseUrl(url: string | undefined) { export async function fastestConnection(endpoints: DropdownOption[]): Promise { try { const urls = endpoints.map(({ value }) => ({ value: value as string })); - const { api, connections } = await fastestEndpoint(urls); - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - const selectedEndpoint = api.registry.knownTypes.provider.endpoint as string; - const connectionsToDisconnect = connections.filter(({ wsProvider }) => wsProvider.endpoint !== selectedEndpoint); - - connectionsToDisconnect.forEach(({ wsProvider }) => { - wsProvider.disconnect().catch(console.error); - }); - - return { - api, - selectedEndpoint - }; + return await fastestEndpoint(urls); } catch (error) { console.error('Unable to make an API connection!', error); diff --git a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnAssetHub.js b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnAssetHub.js index 77e05325ad..3e1e85945a 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnAssetHub.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnAssetHub.js @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { FETCHING_ASSETS_FUNCTION_NAMES } from '../../constants'; -import { closeWebsockets, fastestEndpoint, getChainEndpoints, metadataFromApi, toGetNativeToken } from '../utils'; +import { fastestEndpoint, getChainEndpoints, metadataFromApi, toGetNativeToken } from '../utils'; import { getAssets } from './getAssets.js'; /** @@ -15,7 +15,7 @@ import { getAssets } from './getAssets.js'; */ export async function getAssetOnAssetHub(addresses, assetsToBeFetched, chainName, userAddedEndpoints, port) { const endpoints = getChainEndpoints(chainName, userAddedEndpoints); - const { api, connections } = await fastestEndpoint(endpoints); + const { api } = await fastestEndpoint(endpoints); const { metadata } = metadataFromApi(api); @@ -42,5 +42,6 @@ export async function getAssetOnAssetHub(addresses, assetsToBeFetched, chainName console.info(chainName, ': account assets fetched.'); port.postMessage(JSON.stringify({ functionName: FETCHING_ASSETS_FUNCTION_NAMES.ASSET_HUB, results })); - closeWebsockets(connections); + + api.disconnect().catch(console.error); } diff --git a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnMultiAssetChain.js b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnMultiAssetChain.js index 6241f6bd48..78fb0a9c7b 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnMultiAssetChain.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnMultiAssetChain.js @@ -8,7 +8,7 @@ import { getSubstrateAddress } from '../../address'; import { FETCHING_ASSETS_FUNCTION_NAMES } from '../../constants'; import { toTitleCase } from '../../string'; // eslint-disable-next-line import/extensions -import { balancifyAsset, closeWebsockets, fastestEndpoint, getChainEndpoints, metadataFromApi, toGetNativeToken } from '../utils'; +import { balancifyAsset, fastestEndpoint, getChainEndpoints, metadataFromApi, toGetNativeToken } from '../utils'; /** * @@ -20,7 +20,7 @@ import { balancifyAsset, closeWebsockets, fastestEndpoint, getChainEndpoints, me */ export async function getAssetOnMultiAssetChain(assetsToBeFetched, addresses, chainName, userAddedEndpoints, port) { const endpoints = getChainEndpoints(chainName, userAddedEndpoints); - const { api, connections } = await fastestEndpoint(endpoints); + const { api } = await fastestEndpoint(endpoints); const { metadata } = metadataFromApi(api); @@ -37,7 +37,6 @@ export async function getAssetOnMultiAssetChain(assetsToBeFetched, addresses, ch return; } - // @ts-ignore const [formatted, assetIdRaw] = entry[0].toHuman() ?? []; let assetId; @@ -51,7 +50,6 @@ export async function getAssetOnMultiAssetChain(assetsToBeFetched, addresses, ch const storageKey = entry[0].toString(); - // @ts-ignore let maybeAssetInfo = assetsToBeFetched.find((_asset) => { const currencyId = _asset?.extras?.['currencyIdScale'].replace('0x', ''); @@ -79,7 +77,6 @@ export async function getAssetOnMultiAssetChain(assetsToBeFetched, addresses, ch } if (maybeAssetInfo) { - // @ts-ignore const totalBalance = balance.free.add(balance.reserved); const asset = { @@ -97,7 +94,6 @@ export async function getAssetOnMultiAssetChain(assetsToBeFetched, addresses, ch const address = getSubstrateAddress(formatted); - // @ts-ignore results[address]?.push(asset) ?? (results[address] = [asset]); } else { console.info(`NOTE: There is an asset on ${chainName} for ${formatted} which is not whitelisted. assetInfo`, storageKey, balance?.toHuman()); @@ -106,5 +102,6 @@ export async function getAssetOnMultiAssetChain(assetsToBeFetched, addresses, ch console.info(chainName, ': account assets fetched.'); port.postMessage(JSON.stringify({ functionName: FETCHING_ASSETS_FUNCTION_NAMES.MULTI_ASSET, results })); - closeWebsockets(connections); + + api.disconnect().catch(console.error); } diff --git a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnRelayChain.js b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnRelayChain.js index f3113f0a88..3c468323f2 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnRelayChain.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnRelayChain.js @@ -3,7 +3,7 @@ import { FETCHING_ASSETS_FUNCTION_NAMES, NATIVE_TOKEN_ASSET_ID, TEST_NETS } from '../../constants'; import { getPriceIdByChainName } from '../../misc'; -import { balancify, closeWebsockets } from '../utils'; +import { balancify } from '../utils'; import { getBalances } from './getBalances.js'; /** @@ -16,9 +16,9 @@ export async function getAssetOnRelayChain(addresses, chainName, userAddedEndpoi const results = {}; try { - const { api, balanceInfo, connectionsToBeClosed } = await getBalances(chainName, addresses, userAddedEndpoints, port) ?? {}; + const { api, balanceInfo } = await getBalances(chainName, addresses, userAddedEndpoints, port) ?? {}; - if (!api || !balanceInfo || !connectionsToBeClosed) { + if (!api || !balanceInfo) { return; } @@ -47,7 +47,7 @@ export async function getAssetOnRelayChain(addresses, chainName, userAddedEndpoi }]; }); - closeWebsockets(connectionsToBeClosed); + api.disconnect().catch(console.error); } catch (error) { console.error(`getAssetOnRelayChain: Error fetching balances for ${chainName}:`, error); } finally { diff --git a/packages/extension-polkagate/src/util/workers/shared-helpers/getBalances.js b/packages/extension-polkagate/src/util/workers/shared-helpers/getBalances.js index c4194ee6e7..ecef7868c1 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getBalances.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getBalances.js @@ -17,7 +17,7 @@ import { getStakingBalances } from './getStakingBalances'; */ export async function getBalances(chainName, addresses, userAddedEndpoints, port) { const chainEndpoints = getChainEndpoints(chainName, userAddedEndpoints); - const { api, connections } = await fastestEndpoint(chainEndpoints); + const { api } = await fastestEndpoint(chainEndpoints); if (api.isConnected && api.derive.balances) { const { metadata } = metadataFromApi(api); @@ -25,7 +25,7 @@ export async function getBalances(chainName, addresses, userAddedEndpoints, port console.info(chainName, 'metadata : fetched and saved.'); port.postMessage(JSON.stringify({ functionName: FETCHING_ASSETS_FUNCTION_NAMES.RELAY, metadata })); - const requests = addresses.map(async (address) => { + const requests = addresses.map(async(address) => { const allBalances = await api.derive.balances.all(address); const systemBalance = await api.query['system']['account'](address); const existentialDeposit = api.consts['balances']['existentialDeposit']; @@ -50,11 +50,7 @@ export async function getBalances(chainName, addresses, userAddedEndpoints, port }; }); - return { - api, - balanceInfo: await Promise.all(requests), - connectionsToBeClosed: connections - }; + return { api, balanceInfo: await Promise.all(requests) }; } return undefined; diff --git a/packages/extension-polkagate/src/util/workers/shared-helpers/getNFTs.js b/packages/extension-polkagate/src/util/workers/shared-helpers/getNFTs.js index 7c5d4b342a..73f843cabe 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getNFTs.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getNFTs.js @@ -5,7 +5,7 @@ import { SUPPORTED_NFT_CHAINS } from '../../../fullscreen/nft/utils/constants'; import { getFormattedAddress } from '../../address'; -import { closeWebsockets, fastestEndpoint, getChainEndpoints } from '../utils'; +import { fastestEndpoint, getChainEndpoints } from '../utils'; const NFT_FUNCTION_NAME = 'getNFTs'; @@ -191,9 +191,9 @@ async function getNFTs(addresses) { const formattedAddresses = addresses.map((address) => getFormattedAddress(address, undefined, prefix)); const endpoints = getChainEndpoints(name, undefined); - const { api, connections } = await fastestEndpoint(endpoints); + const { api } = await fastestEndpoint(endpoints); - return ({ api, chainName, connections, formattedAddresses, originalAddresses: addresses }); + return ({ api, chainName, formattedAddresses, originalAddresses: addresses }); }); const apis = await Promise.all(apiPromises); @@ -224,7 +224,9 @@ async function getNFTs(addresses) { return itemsByAddress; } finally { // Ensure all websocket connections are closed - apis.forEach(({ connections }) => closeWebsockets(connections)); + apis.forEach(({ api }) => { + api.disconnect().catch(console.error); + }); } } diff --git a/packages/extension-polkagate/src/util/workers/shared-helpers/getPool.js b/packages/extension-polkagate/src/util/workers/shared-helpers/getPool.js index d53596a60c..479ecac511 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getPool.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getPool.js @@ -5,7 +5,7 @@ import { BN, BN_ZERO, bnMax, hexToString } from '@polkadot/util'; import getChainName from '../../getChainName'; import getPoolAccounts from '../../getPoolAccounts'; -import { closeWebsockets, fastestEndpoint, getChainEndpoints } from '../utils'; +import { fastestEndpoint, getChainEndpoints } from '../utils'; /** * Get all information regarding a pool @@ -31,7 +31,7 @@ export async function getPool(genesisHash, stakerAddress, id, port) { const chainName = getChainName(genesisHash); const endpoints = getChainEndpoints(chainName ?? ''); - const { api, connections } = await fastestEndpoint(endpoints); + const { api } = await fastestEndpoint(endpoints); console.log(`getPool is called for ${stakerAddress} on chain ${chainName}`); id && console.log('getPool is called to fetch the pool with poolId:', id); @@ -133,5 +133,5 @@ export async function getPool(genesisHash, stakerAddress, id, port) { port.postMessage(JSON.stringify({ functionName: 'getPool', results: JSON.stringify(poolInfo) })); - closeWebsockets(connections); + api.disconnect().catch(console.error); } diff --git a/packages/extension-polkagate/src/util/workers/shared-helpers/getValidatorsInformation.js b/packages/extension-polkagate/src/util/workers/shared-helpers/getValidatorsInformation.js index d390c7aec1..b678da1888 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getValidatorsInformation.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getValidatorsInformation.js @@ -6,7 +6,7 @@ import { hexToString } from '@polkadot/util'; import { KUSAMA_GENESIS_HASH, POLKADOT_GENESIS_HASH } from '../../constants'; import getChainName from '../../getChainName'; -import { closeWebsockets, fastestEndpoint, getChainEndpoints } from '../utils'; +import { fastestEndpoint, getChainEndpoints } from '../utils'; const BATCH_SIZE = 50; @@ -72,7 +72,7 @@ export default async function getValidatorsInformation(genesisHash, port) { const endpoints = getChainEndpoints(chainName); try { - const { api, connections } = await fastestEndpoint(endpoints); + const { api } = await fastestEndpoint(endpoints); console.log('getting validators information on ' + chainName); @@ -84,14 +84,14 @@ export default async function getValidatorsInformation(genesisHash, port) { console.log('electedInfo, waitingInfo, currentEra fetched successfully'); - // Close the initial connections to the relay chain - closeWebsockets(connections); + // Close the initial connection to the relay chain + api.disconnect().catch(console.error); // Start connect to the People chain endpoints in order to fetch identities console.log('Connecting to People chain endpoints...'); const peopleChainName = getPeopleChainName(genesisHash); const peopleEndpoints = getChainEndpoints(peopleChainName); - const { api: peopleApi, connections: peopleConnections } = await fastestEndpoint(peopleEndpoints); + const { api: peopleApi } = await fastestEndpoint(peopleEndpoints); // Keep elected and waiting validators separate const electedValidatorsInfo = electedInfo.info; @@ -136,7 +136,7 @@ export default async function getValidatorsInformation(genesisHash, port) { await processSubIdentities(peopleApi, waitingMayHaveSubId, waitingValidatorsInformation, waitingAccountSubInfo); await processParentIdentities(peopleApi, waitingAccountSubInfo, waitingValidatorsInformation); - closeWebsockets(peopleConnections); + peopleApi.disconnect().catch(console.error); const results = { eraIndex: Number(currentEra?.toString() || '0'), diff --git a/packages/extension-polkagate/src/util/workers/utils/closeWebsockets.js b/packages/extension-polkagate/src/util/workers/utils/closeWebsockets.js deleted file mode 100644 index a3c1939d09..0000000000 --- a/packages/extension-polkagate/src/util/workers/utils/closeWebsockets.js +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2019-2026 @polkadot/extension-polkagate authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -/** - * @typedef {Object} ConnectionInfo - * @property {Promise} connection - The connection promise - * @property {string} connectionEndpoint - The endpoint URL - * @property {import('@polkadot/api').WsProvider} wsProvider - The WebSocket provider - */ - -/** - * Closes the open connections - * @param {ConnectionInfo[]} connections - */ -export function closeWebsockets(connections) { - Promise.allSettled( - connections.map(({ wsProvider }) => wsProvider.disconnect()) - ).catch(console.error); -} diff --git a/packages/extension-polkagate/src/util/workers/utils/fastestApi.js b/packages/extension-polkagate/src/util/workers/utils/fastestApi.js deleted file mode 100644 index 8a4a1be172..0000000000 --- a/packages/extension-polkagate/src/util/workers/utils/fastestApi.js +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019-2026 @polkadot/extension-polkagate authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import getChainName from '../../getChainName'; -import { fastestEndpoint } from './fastestEndpoint'; -import { getChainEndpoints } from './getChainEndpoints'; - -/** - * @param {string | undefined} genesisHash - */ -export async function fastestApi(genesisHash) { - const chainName = getChainName(genesisHash); - const endpoints = getChainEndpoints(chainName ?? ''); - - const { api, connections } = await fastestEndpoint(endpoints); - - return { - api, - connections - }; -} diff --git a/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js b/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js index 83ddbd6706..4b42f0f08a 100644 --- a/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js +++ b/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js @@ -1,46 +1,85 @@ // Copyright 2019-2026 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 +// @ts-nocheck + import { ApiPromise, WsProvider } from '@polkadot/api'; +// helper to wrap API creation with timeout +const createApiWithTimeout = async(provider, timeout = 7000) => { + const apiPromise = ApiPromise.create({ provider }); + const timeoutPromise = new Promise((_resolve, reject) => + setTimeout(() => reject(new Error('API isReady timeout')), timeout) + ); + const api = await Promise.race([apiPromise, timeoutPromise]); + + await api.isReady; + + return { api, provider }; +}; + /** - * @param {{ value: string; }[]} endpoints + * Connects to multiple endpoints and returns the fastest working API. + * Automatically disconnects all other providers. + * + * @param {{ value: string }[]} endpoints + * @param {number} [timeout=7000] - max ms to wait for API readiness per endpoint */ -export async function fastestEndpoint(endpoints) { - let connection; - - const connections = endpoints.map(({ value }) => { - // Check if e.value matches the pattern 'wss://' - // ignore due to its rate limits - if (/^wss:\/\/\d+$/.test(value) || (value).includes('onfinality') || value.startsWith('light')) { - return undefined; +export async function fastestEndpoint(endpoints, timeout = 7000) { + // filter out known-bad or light endpoints + const validEndpoints = endpoints.reduce((acc, { value }) => { + if ( + !/^wss:\/\/\d+$/.test(value) && + !value.includes('onfinality') && + !value.startsWith('light') + ) { + acc.push(value); } - const wsProvider = new WsProvider(value); + return acc; + }, []); + + if (!validEndpoints.length) { + throw new Error('No valid endpoints provided'); + } + + const providers = validEndpoints.map((endpoint) => new WsProvider(endpoint)); + + // start all API initializations in parallel + const race = providers.map((p) => + createApiWithTimeout(p, timeout).catch((e) => { + // log and continue; will be ignored by Promise.any + console.warn(`${p.endpoint} failed: ${e.message}`); - connection = ApiPromise.create({ provider: wsProvider }); + return Promise.reject(e); + }) + ); - return { - connection, - connectionEndpoint: value, - wsProvider - }; - }).filter((i) => !!i); + let fastest; - const api = await Promise.any(connections.map(({ connection }) => connection)); + try { + fastest = await Promise.any(race); + } catch (e) { + // all endpoints failed → disconnect all + await Promise.all( + providers.map((p) => p.disconnect().catch(() => undefined)) + ); + throw new Error('All endpoints failed to connect', { cause: e }); + } - // Find the matching connection that created this API - // @ts-ignore - const notConnectedEndpoint = connections.filter(({ connectionEndpoint }) => connectionEndpoint !== api?._options?.provider?.endpoint); - // @ts-ignore - const connectedEndpoint = connections.find(({ connectionEndpoint }) => connectionEndpoint === api?._options?.provider?.endpoint); + // fire-and-forget disconnect of losing APIs + Promise.all( + providers.map((p) => { + if (p !== fastest.provider) { + return p.disconnect().catch(() => undefined); + } - notConnectedEndpoint.forEach(({ wsProvider }) => { - wsProvider.disconnect().catch(() => null); - }); + return Promise.resolve(); + }) + ).catch((e) => console.error('Error disconnecting losing providers:', e)); return { - api, - connections: connectedEndpoint ? [connectedEndpoint] : [] + api: fastest.api, + selectedEndpoint: fastest.provider.endpoint }; } diff --git a/packages/extension-polkagate/src/util/workers/utils/index.ts b/packages/extension-polkagate/src/util/workers/utils/index.ts index d14f889b6c..679255e805 100644 --- a/packages/extension-polkagate/src/util/workers/utils/index.ts +++ b/packages/extension-polkagate/src/util/workers/utils/index.ts @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 export * from './balancify.js'; -export * from './closeWebsockets.js'; -export * from './fastestApi.js'; export * from './fastestEndpoint.js'; export * from './getChainEndpoints.js'; export * from './getChainEndpointsFromGenesisHash.js'; diff --git a/packages/extension-polkagate/tsconfig.build.json b/packages/extension-polkagate/tsconfig.build.json index 1a4bbc9cf3..9a9c6d4d4f 100644 --- a/packages/extension-polkagate/tsconfig.build.json +++ b/packages/extension-polkagate/tsconfig.build.json @@ -2,6 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "baseUrl": "..", + "allowJs": true, + "checkJs": false, "outDir": "./build", "rootDir": "./src" },