diff --git a/.gitignore b/.gitignore index 2c1b387ec..ac9606bde 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ _book docs/html .idea .vscode +.env.sentry-build-plugin diff --git a/packages/extension-polkagate/src/components/contexts.tsx b/packages/extension-polkagate/src/components/contexts.tsx index 4a7a6223a..a08fbb28f 100644 --- a/packages/extension-polkagate/src/components/contexts.tsx +++ b/packages/extension-polkagate/src/components/contexts.tsx @@ -27,7 +27,7 @@ const ToastContext = React.createContext<({ show: (message: string) => void })>( const UserAddedChainContext = React.createContext({}); const GenesisHashOptionsContext = React.createContext([]); const AccountIconThemeContext = React.createContext({ accountIconTheme: undefined, setAccountIconTheme: noop }); -const WorkerContext = React.createContext(undefined); +const WorkerContext = React.createContext(undefined); export { AccountContext, AccountIconThemeContext, diff --git a/packages/extension-polkagate/src/hooks/useAssetsBalances.ts b/packages/extension-polkagate/src/hooks/useAssetsBalances.ts index b0849742f..41f1b04f4 100644 --- a/packages/extension-polkagate/src/hooks/useAssetsBalances.ts +++ b/packages/extension-polkagate/src/hooks/useAssetsBalances.ts @@ -133,7 +133,7 @@ const FUNCTIONS = ['getAssetOnRelayChain', 'getAssetOnAssetHub', 'getAssetOnMult * @param addresses a list of users accounts' addresses * @returns a list of assets balances on different selected chains and a fetching timestamp */ -export default function useAssetsBalances (accounts: AccountJson[] | null, setAlerts: Dispatch>, genesisOptions: DropdownOption[], userAddedEndpoints: UserAddedChains, worker?: Worker): SavedAssets | undefined | null { +export default function useAssetsBalances (accounts: AccountJson[] | null, setAlerts: Dispatch>, genesisOptions: DropdownOption[], userAddedEndpoints: UserAddedChains, worker?: MessagePort): SavedAssets | undefined | null { const { t } = useTranslation(); const isTestnetEnabled = useIsTestnetEnabled(); @@ -282,22 +282,33 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl } setFetchedAssets((fetchedAssets) => { - const combinedAsset = fetchedAssets || DEFAULT_SAVED_ASSETS; + // Create a new object reference each time + const combinedAsset = { + ...(fetchedAssets || DEFAULT_SAVED_ASSETS), + balances: { + ...(fetchedAssets?.balances || DEFAULT_SAVED_ASSETS.balances) + } + }; Object.keys(assets).forEach((address) => { - if (combinedAsset.balances[address] === undefined) { + if (!combinedAsset.balances[address]) { combinedAsset.balances[address] = {}; } - /** to group assets by their chain's genesisHash */ const { genesisHash } = assets[address][0]; - combinedAsset.balances[address][genesisHash] = assets[address]; + // Create a new reference for this specific balances entry + combinedAsset.balances[address] = { + ...(combinedAsset.balances[address] || {}), + [genesisHash]: assets[address] + }; }); - combinedAsset.timeStamp = Date.now(); - - return combinedAsset; + // Ensure a new timestamp and object reference + return { + ...combinedAsset, + timeStamp: Date.now() + }; }); }, [addresses]); @@ -306,8 +317,8 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl return; } - worker.onmessage = (e: MessageEvent) => { - const message = e.data; + const handleMessage = (messageEvent: MessageEvent) => { + const message = messageEvent.data; if (!message) { return; // may receive unknown messages! @@ -372,6 +383,12 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl combineAndSetAssets(_assets); }; + + worker.addEventListener('message', handleMessage); + + return () => { + worker.removeEventListener('message', handleMessage); + }; }, [combineAndSetAssets, handleRequestCount, worker]); const fetchAssetOnRelayChain = useCallback((_addresses: string[], chainName: string) => { @@ -382,13 +399,7 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl const functionName = 'getAssetOnRelayChain'; worker.postMessage({ functionName, parameters: { address: _addresses, chainName, userAddedEndpoints } }); - - worker.onerror = (err) => { - console.log(err); - }; - - handleWorkerMessages(); - }, [handleWorkerMessages, userAddedEndpoints, worker]); + }, [userAddedEndpoints, worker]); const fetchAssetOnAssetHubs = useCallback((_addresses: string[], chainName: string, assetsToBeFetched?: Asset[]) => { if (!worker) { @@ -398,10 +409,6 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl const functionName = 'getAssetOnAssetHub'; worker.postMessage({ functionName, parameters: { address: _addresses, assetsToBeFetched, chainName, userAddedEndpoints } }); - - worker.onerror = (err) => { - console.log(err); - }; }, [userAddedEndpoints, worker]); const fetchAssetOnMultiAssetChain = useCallback((addresses: string[], chainName: string) => { @@ -412,13 +419,7 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl const functionName = 'getAssetOnMultiAssetChain'; worker.postMessage({ functionName, parameters: { addresses, chainName, userAddedEndpoints } }); - - worker.onerror = (err) => { - console.log(err); - }; - - handleWorkerMessages(); - }, [handleWorkerMessages, userAddedEndpoints, worker]); + }, [userAddedEndpoints, worker]); const fetchMultiAssetChainAssets = useCallback((chainName: string) => { return addresses && fetchAssetOnMultiAssetChain(addresses, chainName); @@ -480,6 +481,8 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl !multipleAssetsChainsNames.includes(toCamelCase(text) || '') ); + handleWorkerMessages(); + /** Fetch assets for all the selected chains by default */ _selectedChains?.forEach((genesisHash) => { const isSingleTokenChain = !!singleAssetChains.find(({ value }) => value === genesisHash); @@ -487,7 +490,7 @@ export default function useAssetsBalances (accounts: AccountJson[] | null, setAl fetchAssets(genesisHash, isSingleTokenChain, maybeMultiAssetChainName); }); - }, [FETCH_PATHS, addresses, fetchAssets, worker, isTestnetEnabled, isUpdate, selectedChains, genesisOptions]); + }, [FETCH_PATHS, addresses, fetchAssets, worker, isTestnetEnabled, isUpdate, selectedChains, genesisOptions, handleWorkerMessages]); return fetchedAssets; } diff --git a/packages/extension-polkagate/src/hooks/useNFT.tsx b/packages/extension-polkagate/src/hooks/useNFT.tsx index 654b5c29d..49250133b 100644 --- a/packages/extension-polkagate/src/hooks/useNFT.tsx +++ b/packages/extension-polkagate/src/hooks/useNFT.tsx @@ -2,19 +2,27 @@ // SPDX-License-Identifier: Apache-2.0 import type { AccountJson } from '@polkadot/extension-base/background/types'; -import type { NftItemsType} from '../util/types'; +import type { NftItemsType } from '../util/types'; import { useCallback, useEffect, useState } from 'react'; import NftManager from '../class/nftManager'; import { useTranslation } from '../components/translate'; import useAlerts from './useAlerts'; +import { useWorker } from './useWorker'; + +export interface NftItemsWorker { + functionName: string; + results: NftItemsType; +} const nftManager = new NftManager(); +const NFT_FUNCTION_NAME = 'getNFTs'; export default function useNFT (accountsFromContext: AccountJson[] | null) { const { t } = useTranslation(); const { notify } = useAlerts(); + const worker = useWorker(); const [fetching, setFetching] = useState(false); @@ -27,18 +35,10 @@ export default function useNFT (accountsFromContext: AccountJson[] | null) { const fetchNFTs = useCallback((addresses: string[]) => { setFetching(true); - const getNFTsWorker: Worker = new Worker(new URL('../util/workers/getNFTs.js', import.meta.url)); - - getNFTsWorker.postMessage({ addresses }); + worker.postMessage({ functionName: NFT_FUNCTION_NAME, parameters: { addresses } }); - getNFTsWorker.onerror = (err) => { - console.error('Worker error:', err); - setFetching(false); - getNFTsWorker.terminate(); - }; - - getNFTsWorker.onmessage = (e: MessageEvent) => { - const NFTs = e.data; + const handleMessage = (messageEvent: MessageEvent) => { + const NFTs = messageEvent.data; if (!NFTs) { notify(t('Unable to fetch NFT/Unique items!'), 'info'); @@ -47,27 +47,35 @@ export default function useNFT (accountsFromContext: AccountJson[] | null) { return; } - let parsedNFTsInfo: NftItemsType; + let parsedNFTsInfo: NftItemsWorker; try { - parsedNFTsInfo = JSON.parse(NFTs) as NftItemsType; + parsedNFTsInfo = JSON.parse(NFTs) as NftItemsWorker; + + // console.log('All fetched NFTs:', parsedNFTsInfo); + + if (parsedNFTsInfo.functionName !== NFT_FUNCTION_NAME) { + return; + } } catch (error) { console.error('Failed to parse NFTs JSON:', error); // setFetching(false); - getNFTsWorker.terminate(); return; } - // console.log('All fetched NFTs:', parsedNFTsInfo); - // Save all fetched items to Chrome storage - saveToStorage(parsedNFTsInfo); + saveToStorage(parsedNFTsInfo.results); // setFetching(false); - getNFTsWorker.terminate(); }; - }, [notify, saveToStorage, t]); + + worker.addEventListener('message', handleMessage); + + return () => { + worker.removeEventListener('message', handleMessage); + }; + }, [notify, saveToStorage, t, worker]); useEffect(() => { if (!fetching && addresses && addresses.length > 0 && onWhitelistedPath) { 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 07a37f654..d9b460ae8 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnAssetHub.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnAssetHub.js @@ -4,15 +4,22 @@ import { closeWebsockets, fastestEndpoint, getChainEndpoints, metadataFromApi, toGetNativeToken } from '../utils'; import { getAssets } from './getAssets.js'; -// @ts-ignore - -export async function getAssetOnAssetHub (addresses, assetsToBeFetched, chainName, userAddedEndpoints) { +/** + * + * @param {string[]} addresses + * @param {import('@polkagate/apps-config/assets/types').Asset[]} assetsToBeFetched + * @param {string} chainName + * @param {import('../../types').UserAddedChains} userAddedEndpoints + * @param {MessagePort} port + */ +export async function getAssetOnAssetHub (addresses, assetsToBeFetched, chainName, userAddedEndpoints, port) { const endpoints = getChainEndpoints(chainName, userAddedEndpoints); const { api, connections } = await fastestEndpoint(endpoints); const { metadata } = metadataFromApi(api); - postMessage(JSON.stringify({ functionName: 'getAssetOnAssetHub', metadata })); + console.info('Shared worker, metadata fetched and sent for chain:', chainName); + port.postMessage(JSON.stringify({ functionName: 'getAssetOnAssetHub', metadata })); const results = await toGetNativeToken(addresses, api, chainName); @@ -32,6 +39,7 @@ export async function getAssetOnAssetHub (addresses, assetsToBeFetched, chainNam await getAssets(addresses, api, nonNativeAssets, chainName, results); - postMessage(JSON.stringify({ functionName: 'getAssetOnAssetHub', results })); + console.info('Shared worker, account assets fetched and send on chain:', chainName); + port.postMessage(JSON.stringify({ functionName: 'getAssetOnAssetHub', results })); closeWebsockets(connections); } 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 41891992c..0a0894cdb 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnMultiAssetChain.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnMultiAssetChain.js @@ -1,23 +1,30 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 -// @ts-nocheck - import { getSubstrateAddress } from '../../utils'; // eslint-disable-next-line import/extensions import { balancifyAsset, closeWebsockets, fastestEndpoint, getChainEndpoints, metadataFromApi, toGetNativeToken } from '../utils'; -export async function getAssetOnMultiAssetChain (assetsToBeFetched, addresses, chainName, userAddedEndpoints) { +/** + * + * @param {import('@polkagate/apps-config/assets/types').Asset[]} assetsToBeFetched + * @param {string[]} addresses + * @param {string} chainName + * @param {import('../../types').UserAddedChains} userAddedEndpoints + * @param {MessagePort} port + */ +export async function getAssetOnMultiAssetChain (assetsToBeFetched, addresses, chainName, userAddedEndpoints, port) { const endpoints = getChainEndpoints(chainName, userAddedEndpoints); const { api, connections } = await fastestEndpoint(endpoints); const { metadata } = metadataFromApi(api); - postMessage(JSON.stringify({ functionName: 'getAssetOnMultiAssetChain', metadata })); + console.info('Shared worker, metadata fetched and sent for chain:', chainName); + port.postMessage(JSON.stringify({ functionName: 'getAssetOnMultiAssetChain', metadata })); const results = await toGetNativeToken(addresses, api, chainName); - const maybeTheAssetOfAddresses = addresses.map((address) => api.query.tokens.accounts.entries(address)); + const maybeTheAssetOfAddresses = addresses.map((address) => api.query['tokens']['accounts'].entries(address)); const balanceOfAssetsOfAddresses = await Promise.all(maybeTheAssetOfAddresses); balanceOfAssetsOfAddresses.flat().forEach((entry) => { @@ -25,16 +32,19 @@ export async function getAssetOnMultiAssetChain (assetsToBeFetched, addresses, c return; } + // @ts-ignore const formatted = entry[0].toHuman()[0]; const storageKey = entry[0].toString(); + // @ts-ignore const foundAsset = assetsToBeFetched.find((_asset) => { - const currencyId = _asset?.extras?.currencyIdScale.replace('0x', ''); + const currencyId = _asset?.extras?.['currencyIdScale'].replace('0x', ''); return currencyId && storageKey.endsWith(currencyId); }); const balance = entry[1]; + // @ts-ignore const totalBalance = balance.free.add(balance.reserved); if (foundAsset) { @@ -52,12 +62,14 @@ export async function getAssetOnMultiAssetChain (assetsToBeFetched, addresses, c 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()); } }); - postMessage(JSON.stringify({ functionName: 'getAssetOnMultiAssetChain', results })); + console.info('Shared worker, account assets fetched and send on chain:', chainName); + port.postMessage(JSON.stringify({ functionName: 'getAssetOnMultiAssetChain', results })); closeWebsockets(connections); } 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 1398801b4..4cb305014 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnRelayChain.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getAssetOnRelayChain.js @@ -1,45 +1,55 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 -// @ts-nocheck - import { NATIVE_TOKEN_ASSET_ID, TEST_NETS } from '../../constants'; import { getPriceIdByChainName } from '../../utils'; import { balancify, closeWebsockets } from '../utils'; import { getBalances } from './getBalances.js'; -export async function getAssetOnRelayChain (addresses, chainName, userAddedEndpoints) { +/** + * @param {string[]} addresses + * @param {string} chainName + * @param {import('../../types').UserAddedChains} userAddedEndpoints + * @param {MessagePort } port + */ +export async function getAssetOnRelayChain (addresses, chainName, userAddedEndpoints, port) { const results = {}; - await getBalances(chainName, addresses, userAddedEndpoints) - .then(({ api, balanceInfo, connectionsToBeClosed }) => { - balanceInfo.forEach(({ address, balances, pooledBalance, soloTotal }) => { - const totalBalance = balances.freeBalance.add(balances.reservedBalance).add(pooledBalance); - const genesisHash = api.genesisHash.toString(); - - const priceId = TEST_NETS.includes(genesisHash) - ? undefined - : getPriceIdByChainName(chainName, userAddedEndpoints); - - results[address] = [{ // since some chains may have more than one asset hence we use an array here! even thought its not needed for relay chains but just to be as a general rule. - assetId: NATIVE_TOKEN_ASSET_ID, - balanceDetails: balancify({ ...balances, pooledBalance, soloTotal }), - chainName, - decimal: api.registry.chainDecimals[0], - genesisHash, - priceId, - token: api.registry.chainTokens[0], - totalBalance: String(totalBalance) - }]; - }); - - closeWebsockets(connectionsToBeClosed); - }) - .catch((error) => { - console.error(`getAssetOnRelayChain: Error fetching balances for ${chainName}:`, error); - }).finally(() => { - Object.keys(results).length - ? postMessage(JSON.stringify({ functionName: 'getAssetOnRelayChain', results })) - : postMessage({ functionName: 'getAssetOnRelayChain' }); + try { + const { api, balanceInfo, connectionsToBeClosed } = await getBalances(chainName, addresses, userAddedEndpoints, port) ?? {}; + + if (!api || !balanceInfo || !connectionsToBeClosed) { + return; + } + + balanceInfo.forEach(({ address, balances, pooledBalance, soloTotal }) => { + const totalBalance = balances.freeBalance.add(balances.reservedBalance).add(pooledBalance); + const genesisHash = api.genesisHash.toString(); + + const priceId = TEST_NETS.includes(genesisHash) + ? undefined + : getPriceIdByChainName(chainName, userAddedEndpoints); + + // @ts-ignore + results[address] = [{ // since some chains may have more than one asset hence we use an array here! even thought its not needed for relay chains but just to be as a general rule. + assetId: NATIVE_TOKEN_ASSET_ID, + balanceDetails: balancify({ ...balances, pooledBalance, soloTotal }), + chainName, + decimal: api.registry.chainDecimals[0], + genesisHash, + priceId, + token: api.registry.chainTokens[0], + totalBalance: String(totalBalance) + }]; }); + + closeWebsockets(connectionsToBeClosed); + } catch (error) { + console.error(`getAssetOnRelayChain: Error fetching balances for ${chainName}:`, error); + } finally { + console.info('Shared worker, account assets fetched and send on chain:', chainName); + Object.keys(results).length + ? port.postMessage(JSON.stringify({ functionName: 'getAssetOnRelayChain', results })) + : port.postMessage(JSON.stringify({ functionName: 'getAssetOnRelayChain' })); + } } 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 85716120a..56a14a0f6 100644 --- a/packages/extension-polkagate/src/util/workers/shared-helpers/getBalances.js +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getBalances.js @@ -1,39 +1,49 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 -// @ts-nocheck - import { BN_ZERO } from '@polkadot/util'; import { fastestEndpoint, getChainEndpoints, metadataFromApi } from '../utils'; import { getPooledBalance } from './getPooledBalance.js'; -export async function getBalances (chainName, addresses, userAddedEndpoints) { +/** + * + * @param {string} chainName + * @param {string[]} addresses + * @param {import('../../types').UserAddedChains} userAddedEndpoints + * @param {MessagePort } port + * @returns + */ +export async function getBalances (chainName, addresses, userAddedEndpoints, port) { const chainEndpoints = getChainEndpoints(chainName, userAddedEndpoints); const { api, connections } = await fastestEndpoint(chainEndpoints); if (api.isConnected && api.derive.balances) { const { metadata } = metadataFromApi(api); - postMessage(JSON.stringify({ functionName: 'getAssetOnRelayChain', metadata })); + console.info('Shared worker, metadata fetched and sent for chain:', chainName); + port.postMessage(JSON.stringify({ functionName: 'getAssetOnRelayChain', metadata })); const requests = addresses.map(async (address) => { const balances = await api.derive.balances.all(address); - const systemBalance = await api.query.system.account(address); + const systemBalance = await api.query['system']['account'](address); + // @ts-ignore balances.frozenBalance = systemBalance.frozen; let soloTotal = BN_ZERO; let pooledBalance = BN_ZERO; - if (api.query.nominationPools) { + if (api.query['nominationPools']) { pooledBalance = await getPooledBalance(api, address); } - if (api.query.staking?.ledger) { - const ledger = await api.query.staking.ledger(address); + if (api.query['staking']?.['ledger']) { + const ledger = await api.query['staking']['ledger'](address); + // @ts-ignore if (ledger.isSome) { + // @ts-ignore soloTotal = ledger?.unwrap()?.total?.toString(); } } @@ -43,4 +53,6 @@ export async function getBalances (chainName, addresses, userAddedEndpoints) { return { api, balanceInfo: await Promise.all(requests), connectionsToBeClosed: connections }; } + + 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 new file mode 100644 index 000000000..18a59a524 --- /dev/null +++ b/packages/extension-polkagate/src/util/workers/shared-helpers/getNFTs.js @@ -0,0 +1,261 @@ +// Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// @ts-nocheck + +import { SUPPORTED_NFT_CHAINS } from '../../../fullscreen/nft/utils/constants'; +import { getFormattedAddress } from '../../utils'; +import { closeWebsockets, fastestEndpoint, getChainEndpoints } from '../utils'; + +const NFT_FUNCTION_NAME = 'getNFTs'; + +/** + * Fetches NFT or unique collections for a given chain and set of addresses + * @param {ApiPromise} api - The API instance for interacting with the blockchain + * @param {string[]} addresses - Array of addresses to fetch items for + * @param {string} chainName - The chain identifier + * @param {boolean} isNft - Whether to fetch NFT collections (true) or unique collections (false) + * @returns {Promise} Array of collection information + */ +async function fetchCollections (api, addresses, chainName, isNft) { + // Determine which query method to use based on item type + const queryMethod = isNft ? api.query.nfts.collectionAccount : api.query.uniques.classAccount; + const requests = addresses.map(async (address) => await queryMethod.entries(address)); + const entries = await Promise.all(requests); + + // collection id + const collectionsId = entries + .flat() + .map(([key, _info]) => { + const info = key.args.map((k) => k.toPrimitive()); + + info.shift(); // first item is the address which we do not need it to fetch the collection information + + return info[0]; + }); + + const collectionInfoQueryMethod = isNft ? [api.query.nfts.collection, api.query.nfts.collectionMetadataOf] : [api.query.uniques.class, api.query.uniques.classMetadataOf]; + const collectionsInformation = + await Promise.all(collectionsId.map((collectionId) => + Promise.all([collectionInfoQueryMethod[0](collectionId), collectionInfoQueryMethod[1](collectionId)])) + ); + + const myCollections = collectionsInformation + .map(([collection, collectionMetadata], index) => { + const collectionId = collectionsId[index]; + const { items, owner } = collection.toPrimitive(); + const collectionMetadataInfo = collectionMetadata?.toPrimitive(); + + return { + chainName, + collectionId, + data: collectionMetadataInfo?.data ?? null, + isCollection: true, + isNft, + items, + owner: String(owner) + }; + }); + + return myCollections; +} + +/** + * Fetches NFT or unique items for a given chain and set of addresses + * @param {ApiPromise} api - The API instance for interacting with the blockchain + * @param {string[]} addresses - Array of addresses to fetch items for + * @param {string} chainName - The chain identifier + * @param {boolean} isNft - Whether to fetch NFTs (true) or uniques (false) + * @returns {Promise} Array of item information + */ +async function fetchItems (api, addresses, chainName, isNft) { + // Determine which query method to use based on item type + const queryMethod = isNft ? api.query.nfts.account : api.query.uniques.account; + const requests = addresses.map(async (address) => await queryMethod.entries(address)); + const entries = await Promise.all(requests); + + // owner, collection id, nft id + const itemsInfo = entries + .flat() + .map(([key, _info]) => { + const info = key.args.map((k) => k.toPrimitive()); + + info.shift(); // first item is the address which we do not need it to fetch the item information + + return info; + }); + + const itemInfoQueryMethod = isNft ? api.query.nfts.item : api.query.uniques.asset; + + const itemsInformation = await Promise.all(itemsInfo.map(async (itemInfo) => await itemInfoQueryMethod(...itemInfo))); + + const myItems = itemsInformation + .map((item, index) => { + const [collectionId, itemId] = itemsInfo[index]; + + return { + ids: { collectionId, itemId }, + itemInfo: item.toPrimitive() + }; + }); + + if (myItems.length === 0) { + return []; + } + + // Fetch metadata for all items + const metadataPromises = myItems.map(({ ids }) => + isNft + ? api.query.nfts.itemMetadataOf(ids.collectionId, ids.itemId) + : api.query.uniques.instanceMetadataOf(ids.collectionId, ids.itemId) + ); + const metadataRequests = await Promise.all(metadataPromises); + const metadata = metadataRequests.map((metadata) => { + const data = metadata.toPrimitive()?.data; + + return data ? data.toString() : null; + }); + + // Fetch prices for NFTs + let prices = null; + + if (isNft) { + const pricePromises = myItems.map(({ ids }) => api.query.nfts.itemPriceOf(ids.collectionId, ids.itemId)); + const priceRequests = await Promise.all(pricePromises); + + prices = priceRequests.map((price) => { + const priceData = price.toPrimitive(); + + return priceData ? priceData[0] : null; + }); + } + + // Fetch creators for uniques + let creators = null; + + if (!isNft) { + const uniqueCollectionIds = [...new Set(myItems.map(({ ids }) => ids.collectionId))]; + const collectionsMetadata = await Promise.all(uniqueCollectionIds.map((id) => api.query.uniques.class(id))); + + creators = uniqueCollectionIds.reduce((acc, id, index) => { + acc[id] = collectionsMetadata[index].toPrimitive()?.owner.toString(); + + return acc; + }, {}); + } + + return myItems + .map(({ ids: { collectionId, itemId }, itemInfo }, index) => ({ + chainName, + collectionId, + creator: isNft ? String(itemInfo.deposit.account) : creators?.[collectionId], + data: metadata[index], + isCollection: false, + isNft, + itemId, + owner: isNft ? String(itemInfo.owner) : itemInfo?.owner?.toString(), + price: prices?.[index] ?? null + })); +} + +/** + * Fetches both NFTs and uniques for a given chain + * @param {ApiPromise} api - The API instance for interacting with the blockchain + * @param {string[]} addresses - Array of addresses to fetch items for + * @param {string} chainName - The chain identifier + * @returns {Promise} Combined array of NFTs and uniques + */ +async function fetchNFTsForChain (api, addresses, chainName) { + const [nfts, uniques, nftCollections, uniqueCollections] = await Promise.all([ + fetchItems(api, addresses, chainName, true), + fetchItems(api, addresses, chainName, false), + fetchCollections(api, addresses, chainName, true), + fetchCollections(api, addresses, chainName, false) + ]); + + return [...nftCollections, ...uniqueCollections, ...nfts, ...uniques]; +} + +/** + * Fetches all NFTs and uniques across all configured chains + * @param {string[]} addresses - Array of addresses to fetch items for + * @returns {Promise>} Combined array of all NFT and unique items and collections across all chains, categorized by addresses + */ +async function getNFTs (addresses) { + const chainNames = Object.entries(SUPPORTED_NFT_CHAINS); + + // Initialize API connections for all chainNames + const apiPromises = chainNames.map(async ([chainName, { name, prefix }]) => { + const formattedAddresses = addresses.map((address) => getFormattedAddress(address, undefined, prefix)); + const endpoints = getChainEndpoints(name, undefined); + + const { api, connections } = await fastestEndpoint(endpoints); + + return ({ api, chainName, connections, formattedAddresses, originalAddresses: addresses }); + }); + + const apis = await Promise.all(apiPromises); + + try { + // Fetch NFTs and uniques for all chains in parallel + const itemsByChain = await Promise.all(apis.map(({ api, chainName, formattedAddresses, originalAddresses }) => + fetchNFTsForChain(api, formattedAddresses, chainName, originalAddresses) + )); + + // Organize items by address + const itemsByAddress = addresses.reduce((acc, address) => { + acc[address] = []; + + return acc; + }, {}); + + itemsByChain.flat().forEach((item) => { + const matchingAddress = addresses.find((addr) => + [item.owner, item.creator].includes(getFormattedAddress(addr, undefined, SUPPORTED_NFT_CHAINS[item.chainName].prefix)) + ); + + if (matchingAddress) { + itemsByAddress[matchingAddress].push(item); + } + }); + + return itemsByAddress; + } finally { + // Ensure all websocket connections are closed + apis.forEach(({ connections }) => closeWebsockets(connections)); + } +} + +/** + * + * @param {string[]} addresses + * @param {MessagePort } port + */ +export default async function getNftHandler (addresses, port) { + if (!addresses) { + console.warn('Shared worker, No addresses to NFTs'); + + return port.postMessage(JSON.stringify({ functionName: NFT_FUNCTION_NAME, results: undefined })); + } + + for (let tryCount = 1; tryCount <= 5; tryCount++) { + try { + const allItems = await getNFTs(addresses); + + console.info('Shared worker, accounts NFTs fetched and send on chain'); + port.postMessage(JSON.stringify({ functionName: NFT_FUNCTION_NAME, results: allItems })); + + return; + } catch (error) { + console.error(`Shared worker, getNFTs: Error while fetching NFTs on asset hubs, ${5 - tryCount} times to retry`, error); + + if (tryCount === 5) { + console.warn('Shared worker, Unable to fetch NFTs'); + port.postMessage(JSON.stringify({ functionName: NFT_FUNCTION_NAME, results: undefined })); + } else { + // Wait for a delay before retrying (e.g., exponential backoff) + await new Promise((resolve) => setTimeout(resolve, tryCount * 1000)); + } + } + } +} diff --git a/packages/extension-polkagate/src/util/workers/sharedWorker.js b/packages/extension-polkagate/src/util/workers/sharedWorker.js index 78b05d28e..5e5949fe4 100644 --- a/packages/extension-polkagate/src/util/workers/sharedWorker.js +++ b/packages/extension-polkagate/src/util/workers/sharedWorker.js @@ -1,57 +1,75 @@ // Copyright 2019-2024 @polkadot/extension-polkagate authors & contributors // SPDX-License-Identifier: Apache-2.0 +// @ts-nocheck + import { createAssets } from '@polkagate/apps-config/assets'; import { getAssetOnAssetHub } from './shared-helpers/getAssetOnAssetHub.js'; import { getAssetOnMultiAssetChain } from './shared-helpers/getAssetOnMultiAssetChain.js'; import { getAssetOnRelayChain } from './shared-helpers/getAssetOnRelayChain.js'; +import getNFTs from './shared-helpers/getNFTs.js'; const assetsChains = createAssets(); -onmessage = (e) => { - const { functionName, parameters } = e.data; +// Handle connections to the shared worker +onconnect = (event) => { + const port = event.ports[0]; // Get the MessagePort from the connection + + console.info('Shared worker: port connected'); + + port.onmessage = (e) => { + const { functionName, parameters } = e.data; + + const params = Object.values(parameters); - const params = Object.values(parameters); + console.info('Shared worker, message received for:', functionName, parameters); - try { - switch (functionName) { - case 'getAssetOnRelayChain': - getAssetOnRelayChain(...params).catch(console.error); - break; + try { + switch (functionName) { + case 'getAssetOnRelayChain': + getAssetOnRelayChain(...params, port).catch(console.error); + break; - case 'getAssetOnMultiAssetChain': - // eslint-disable-next-line no-case-declarations - const assetsToBeFetched = assetsChains[parameters.chainName]; + case 'getAssetOnMultiAssetChain': { + // eslint-disable-next-line no-case-declarations + const assetsToBeFetched = assetsChains[parameters.chainName]; - /** if assetsToBeFetched === undefined then we don't fetch assets by default at first, but wil fetch them on-demand later in account details page*/ - if (!assetsToBeFetched) { - console.info(`getAssetOnMultiAssetChain: No assets to be fetched on ${parameters.chainName}`); + /** if assetsToBeFetched === undefined then we don't fetch assets by default at first, but will fetch them on-demand later in account details page*/ + if (!assetsToBeFetched) { + console.info(`Shared worker, getAssetOnMultiAssetChain: No assets to be fetched on ${parameters.chainName}`); - return postMessage({ functionName }); // FIXME: if this happens, should be handled in caller + return port.postMessage(JSON.stringify({ functionName })); // FIXME: if this happens, should be handled in caller + } + + getAssetOnMultiAssetChain(assetsToBeFetched, ...params, port).catch(console.error); + break; } - getAssetOnMultiAssetChain(assetsToBeFetched, ...params).catch(console.error); - break; + case 'getAssetOnAssetHub': { + if (!parameters.assetsToBeFetched) { + console.warn('getAssetOnAssetHub: No assets to be fetched on, but just Native Token'); - case 'getAssetOnAssetHub': - if (!parameters.assetsToBeFetched) { - console.warn('getAssetOnAssetHub: No assets to be fetched on, but just Native Token'); + parameters.assetsToBeFetched = []; + } - parameters.assetsToBeFetched = []; + getAssetOnAssetHub(...params, port).catch(console.error); + break; } - getAssetOnAssetHub(...params).catch(console.error); - break; + case 'getNFTs': + getNFTs(...params, port).catch(console.error); + break; - default: - console.error('unknown function sent to shared worker!'); + default: + console.error('unknown function sent to shared worker!'); - return postMessage({ functionName }); - } - } catch (error) { - console.error(`Error while shared worker probably running ${functionName}`, error); + return port.postMessage(JSON.stringify({ functionName })); + } + } catch (error) { + console.error(`Error while shared worker probably running ${functionName}`, error); - return postMessage({ functionName }); - } + return port.postMessage(JSON.stringify({ functionName })); + } + }; }; diff --git a/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js b/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js index a558fbf1b..340913d64 100644 --- a/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js +++ b/packages/extension-polkagate/src/util/workers/utils/fastestEndpoint.js @@ -22,14 +22,25 @@ export async function fastestEndpoint (endpoints) { return { connection, + connectionEndpoint: value, wsProvider }; }).filter((i) => !!i); const api = await Promise.any(connections.map(({ connection }) => connection)); + // 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); + + notConnectedEndpoint.forEach(({ wsProvider }) => { + wsProvider.disconnect().catch(() => null); + }); + return { api, - connections + connections: connectedEndpoint ? [connectedEndpoint] : [] }; } diff --git a/packages/extension-ui/src/Popup/contexts/WorkerProvider.tsx b/packages/extension-ui/src/Popup/contexts/WorkerProvider.tsx index 5b0b9a0e6..b81bb45c7 100644 --- a/packages/extension-ui/src/Popup/contexts/WorkerProvider.tsx +++ b/packages/extension-ui/src/Popup/contexts/WorkerProvider.tsx @@ -9,24 +9,30 @@ interface WorkerProviderProps { children: React.ReactNode; } -export default function WorkerProvider ({ children }: WorkerProviderProps) { - const [worker, setWorker] = useState(undefined); +export default function WorkerProvider({ children }: WorkerProviderProps) { + const [port, setPort] = useState(undefined); useEffect(() => { - const newWorker = new Worker(new URL('@polkadot/extension-polkagate/src/util/workers/sharedWorker.js', import.meta.url)); + const sharedWorker = new SharedWorker(new URL('@polkadot/extension-polkagate/src/util/workers/sharedWorker.js', import.meta.url)); - setWorker(newWorker); + // Access the worker's communication port + const workerPort = sharedWorker.port; + + // Start the port for communication + workerPort.start(); + + setPort(workerPort); return () => { // Cleanup on unmount - if (newWorker) { - newWorker.terminate(); + if (workerPort) { + workerPort.close(); } }; }, []); return ( - + {children} ); diff --git a/packages/extension/package.json b/packages/extension/package.json index 3107fd35a..487ea023a 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -15,7 +15,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.33.2", + "version": "0.33.1", "dependencies": { "@polkadot/api": "^11.2.1", "@polkadot/extension-base": "^0.47.5", diff --git a/packages/extension/src/packageInfo.ts b/packages/extension/src/packageInfo.ts index 799f1855d..7500741da 100644 --- a/packages/extension/src/packageInfo.ts +++ b/packages/extension/src/packageInfo.ts @@ -3,4 +3,4 @@ // Do not edit, auto-generated by @polkadot/dev -export const packageInfo = { name: '@polkadot/extension', path: 'auto', type: 'auto', version: '0.33.2' }; +export const packageInfo = { name: '@polkadot/extension', path: 'auto', type: 'auto', version: '0.33.1' };