From 698f0f87226e79459bad86411bfb4e075246edbd Mon Sep 17 00:00:00 2001 From: Apotheosis <0xapotheosis@gmail.com> Date: Thu, 26 Feb 2026 15:45:07 +1100 Subject: [PATCH 1/6] feat: route custom token metadata through proxy service --- .env | 10 +-- headers/csps/chains/ethereum.ts | 1 - headers/csps/customTokenImport.ts | 1 + .../Header/GlobalSearch/GlobalSearchModal.tsx | 11 +-- .../components/SearchTermAssetList.tsx | 15 ++-- .../hooks/useGetCustomTokensQuery.tsx | 80 ++++++++++--------- src/config.ts | 4 +- src/lib/alchemySdkInstance.ts | 74 ----------------- src/lib/customTokenImportSupportedChainIds.ts | 18 +++++ src/vite-env.d.ts | 4 +- 10 files changed, 73 insertions(+), 145 deletions(-) delete mode 100644 src/lib/alchemySdkInstance.ts create mode 100644 src/lib/customTokenImportSupportedChainIds.ts diff --git a/.env b/.env index 2f73c970f40..f5bc1a55c7b 100644 --- a/.env +++ b/.env @@ -186,7 +186,6 @@ VITE_NEAR_NODE_URL=https://rpc.mainnet.near.org VITE_NEAR_NODE_URL_FALLBACK_1=https://near.lava.build VITE_NEAR_NODE_URL_FALLBACK_2=https://rpc.fastnear.com VITE_FASTNEAR_API_URL=https://api.fastnear.com -VITE_ALCHEMY_POLYGON_URL=https://polygon-mainnet.g.alchemy.com/v2/anoTMcIc2hbPUxri37h4DeuUwg2p5_xZ # midgard VITE_THORCHAIN_MIDGARD_URL=https://api.thorchain.shapeshift.com/midgard/v2 @@ -202,15 +201,9 @@ VITE_COINCAP_API_KEY=dab646843b251ce2d28864982989c335e1f0d32fa14e4ecc6b40cd057ec VITE_EXCHANGERATEHOST_BASE_URL=https://api.exchangerate.host VITE_EXCHANGERATEHOST_API_KEY=8f7515ffddef9d3e449b45f93108ca4d -# Alchemy API key - to be used either with Alchemy SDK or directly with the REST endpoints -VITE_ALCHEMY_API_KEY=anoTMcIc2hbPUxri37h4DeuUwg2p5_xZ - # Moralis API key - to be used either with Alchemy SDK or directly with the REST endpoints VITE_MORALIS_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6ImY1ZmVjMTg4LTc0YzUtNDk0ZC05ZmFjLTZmYzQ5MTAyZTVhOCIsIm9yZ0lkIjoiNDYzNTU5IiwidXNlcklkIjoiNDc2OTA5IiwidHlwZUlkIjoiODE0NWUwYjEtYjEwNi00NzQyLTg2NDAtNzk1NmU4ZGQ5ZGFkIiwidHlwZSI6IlBST0pFQ1QiLCJpYXQiOjE3NTQ0NzEyNTksImV4cCI6NDkxMDIzMTI1OX0.Aa-ELM_vZR0i7Z1nQfh0rsx6ES21DHNEbNsjL28AMMw -# Alchemy Solana endpoint for custom token import -VITE_ALCHEMY_SOLANA_BASE_URL=https://solana-mainnet.g.alchemy.com/v2 - # boardroom VITE_BOARDROOM_API_BASE_URL=https://api.boardroom.info/v1/protocols/shapeshift/ VITE_BOARDROOM_APP_BASE_URL=https://boardroom.io/shapeshift/ @@ -256,6 +249,9 @@ VITE_WALLET_CONNECT_RELAY_URL=wss://relay.walletconnect.com # Portals VITE_PORTALS_BASE_URL=https://api.proxy.shapeshift.com/api/v1/portals +# Proxy API +VITE_PROXY_API_BASE_URL=https://api.proxy.shapeshift.com + VITE_SNAP_ID=npm:@shapeshiftoss/metamask-snaps VITE_SNAP_VERSION=1.0.9 # VITE_SNAP_ID=local:http://localhost:9000 diff --git a/headers/csps/chains/ethereum.ts b/headers/csps/chains/ethereum.ts index c0142a59bc7..97b4e88bb26 100644 --- a/headers/csps/chains/ethereum.ts +++ b/headers/csps/chains/ethereum.ts @@ -10,7 +10,6 @@ export const csp: Csp = { env.VITE_ETHEREUM_NODE_URL, env.VITE_UNCHAINED_ETHEREUM_HTTP_URL, env.VITE_UNCHAINED_ETHEREUM_WS_URL, - env.VITE_ALCHEMY_POLYGON_URL, 'https://eth.llamarpc.com', ], } diff --git a/headers/csps/customTokenImport.ts b/headers/csps/customTokenImport.ts index 66dbafe7d80..4b0ee328c39 100644 --- a/headers/csps/customTokenImport.ts +++ b/headers/csps/customTokenImport.ts @@ -5,5 +5,6 @@ export const csp: Csp = { // Common metadata sources 'https://arweave.net/', 'https://*.arweave.net/', + 'https://api.proxy.shapeshift.com', ], } diff --git a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx index b8bf5a3ec19..372157a14a1 100644 --- a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx +++ b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx @@ -9,7 +9,7 @@ import { useUpdateEffect, } from '@chakra-ui/react' import { captureException, setContext } from '@sentry/react' -import { solanaChainId, toAssetId } from '@shapeshiftoss/caip' +import { toAssetId } from '@shapeshiftoss/caip' import type { Asset, KnownChainIds } from '@shapeshiftoss/types' import { getAssetNamespaceFromChainId, makeAsset } from '@shapeshiftoss/utils' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -22,7 +22,7 @@ import { AssetSearchResults } from './AssetSearchResults' import { GlobalFilter } from '@/components/StakingVaults/GlobalFilter' import { useGetCustomTokensQuery } from '@/components/TradeAssetSearch/hooks/useGetCustomTokensQuery' import { useModalRegistration } from '@/context/ModalStackProvider' -import { ALCHEMY_SDK_SUPPORTED_CHAIN_IDS } from '@/lib/alchemySdkInstance' +import { CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS } from '@/lib/customTokenImportSupportedChainIds' import { isSome } from '@/lib/utils' import { assets as assetsSlice } from '@/state/slices/assetsSlice/assetsSlice' import { selectAssets, selectAssetsBySearchQuery } from '@/state/slices/selectors' @@ -61,14 +61,9 @@ export const GlobalSearchModal = memo( onClose: handleClose, }) - const customTokenSupportedChainIds = useMemo(() => { - // Solana _is_ supported by Alchemy, but not by the SDK - return [...ALCHEMY_SDK_SUPPORTED_CHAIN_IDS, solanaChainId] - }, []) - const { data: customTokens, isLoading: isLoadingCustomTokens } = useGetCustomTokensQuery({ contractAddress: searchQuery, - chainIds: customTokenSupportedChainIds, + chainIds: CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS, }) const customAssets = useMemo(() => { diff --git a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx index 86543a8928e..34388d3034f 100644 --- a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx +++ b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx @@ -1,6 +1,6 @@ import { Box, useMediaQuery } from '@chakra-ui/react' import type { AssetId, ChainId } from '@shapeshiftoss/caip' -import { fromAssetId, isNft, solanaChainId, toAssetId } from '@shapeshiftoss/caip' +import { fromAssetId, isNft, toAssetId } from '@shapeshiftoss/caip' import type { Asset, KnownChainIds } from '@shapeshiftoss/types' import type { MinimalAsset } from '@shapeshiftoss/utils' import { bnOrZero, getAssetNamespaceFromChainId, makeAsset } from '@shapeshiftoss/utils' @@ -12,8 +12,8 @@ import { useGetCustomTokensQuery } from '../hooks/useGetCustomTokensQuery' import { AssetList } from '@/components/AssetSearch/components/AssetList' import { Text } from '@/components/Text' -import { ALCHEMY_SDK_SUPPORTED_CHAIN_IDS } from '@/lib/alchemySdkInstance' import { searchAssets } from '@/lib/assetSearch' +import { CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS } from '@/lib/customTokenImportSupportedChainIds' import { isSome } from '@/lib/utils' import { isContractAddress } from '@/lib/utils/isContractAddress' import { @@ -56,20 +56,15 @@ export const SearchTermAssetList = ({ const assetsById = useAppSelector(selectPrimaryAssets) const walletConnectedChainIds = useAppSelector(selectWalletConnectedChainIds) - const customTokenSupportedChainIds = useMemo(() => { - // Solana _is_ supported by Alchemy, but not by the SDK - return [...ALCHEMY_SDK_SUPPORTED_CHAIN_IDS, solanaChainId] - }, []) - const chainIds = useMemo(() => { if (activeChainId === 'All') { - return customTokenSupportedChainIds - } else if (customTokenSupportedChainIds.includes(activeChainId)) { + return CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS + } else if (CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS.includes(activeChainId)) { return [activeChainId] } else { return [] } - }, [activeChainId, customTokenSupportedChainIds]) + }, [activeChainId]) const { data: customTokens, isLoading: isLoadingCustomTokens } = useGetCustomTokensQuery({ contractAddress: searchString, diff --git a/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx b/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx index 49e305afff9..94434e050c8 100644 --- a/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx +++ b/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx @@ -1,21 +1,22 @@ -import { Metaplex } from '@metaplex-foundation/js' import type { ChainId } from '@shapeshiftoss/caip' import { solanaChainId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' -import { Connection, PublicKey } from '@solana/web3.js' import type { UseQueryResult } from '@tanstack/react-query' import { skipToken, useQueries } from '@tanstack/react-query' -import type { TokenMetadataResponse } from 'alchemy-sdk' +import axios, { isAxiosError } from 'axios' import { useCallback, useMemo } from 'react' import { isAddress } from 'viem' import { getConfig } from '@/config' import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' -import { getAlchemyInstanceByChainId } from '@/lib/alchemySdkInstance' import { isSolanaAddress } from '@/lib/utils/isSolanaAddress' import { mergeQueryOutputs } from '@/react-queries/helpers' -type TokenMetadata = TokenMetadataResponse & { +type TokenMetadata = { + name?: string + symbol?: string + decimals?: number + logo?: string chainId: ChainId contractAddress: string price: string @@ -40,35 +41,42 @@ export const useGetCustomTokensQuery = ({ }: UseGetCustomTokensQueryProps): UseQueryResult<(TokenMetadata | undefined)[], Error[]> => { const customTokenImportEnabled = useFeatureFlag('CustomTokenImport') - const getEvmTokenMetadata = useCallback( - async (chainId: ChainId) => { - const alchemy = getAlchemyInstanceByChainId(chainId) - const tokenMetadataResponse = await alchemy.core.getTokenMetadata(contractAddress) - return { ...tokenMetadataResponse, chainId, contractAddress, price: '0' } - }, - [contractAddress], - ) + const getTokenMetadata = useCallback( + async (chainId: ChainId): Promise => { + try { + const { data } = await axios.get<{ + chainId: ChainId + tokenAddress: string + name?: string + symbol?: string + decimals?: number + logo?: string + }>(`${getConfig().VITE_PROXY_API_BASE_URL}/api/v1/tokens/metadata`, { + params: { + chainId, + tokenAddress: contractAddress, + }, + timeout: 10_000, + }) - const getSolanaTokenMetadata = useCallback( - async (mintAddress: string): Promise => { - const solanaRpcUrl = `${getConfig().VITE_ALCHEMY_SOLANA_BASE_URL}/${ - getConfig().VITE_ALCHEMY_API_KEY - }` - const connection = new Connection(solanaRpcUrl) - const metaplex = Metaplex.make(connection) - const metadata = await metaplex.nfts().findByMint({ mintAddress: new PublicKey(mintAddress) }) + return { + name: data.name, + symbol: data.symbol, + decimals: data.decimals, + chainId: data.chainId, + contractAddress: data.tokenAddress, + price: '0', + logo: data.logo, + } + } catch (e) { + if (isAxiosError(e) && (e.response?.status === 404 || e.response?.status === 422)) { + return undefined + } - return { - name: metadata.name, - symbol: metadata.symbol, - decimals: metadata.mint.currency.decimals, - chainId: solanaChainId, - contractAddress: mintAddress, - price: '0', - logo: metadata.json?.image ?? '', + throw e } }, - [], + [contractAddress], ) const isValidEvmAddress = useMemo( @@ -81,20 +89,14 @@ export const useGetCustomTokensQuery = ({ const getQueryFn = useCallback( (chainId: ChainId) => () => { if (isValidSolanaAddress && chainId === solanaChainId) { - return getSolanaTokenMetadata(contractAddress) + return getTokenMetadata(chainId) } else if (isValidEvmAddress && isEvmChainId(chainId)) { - return getEvmTokenMetadata(chainId) + return getTokenMetadata(chainId) } else { return skipToken } }, - [ - contractAddress, - getEvmTokenMetadata, - getSolanaTokenMetadata, - isValidEvmAddress, - isValidSolanaAddress, - ], + [getTokenMetadata, isValidEvmAddress, isValidSolanaAddress], ) const isTokenMetadata = ( diff --git a/src/config.ts b/src/config.ts index 9c705672a1c..f2a4f2c0785 100644 --- a/src/config.ts +++ b/src/config.ts @@ -94,7 +94,6 @@ const validators = { VITE_NEAR_NODE_URL_FALLBACK_1: url({ default: '' }), VITE_NEAR_NODE_URL_FALLBACK_2: url({ default: '' }), VITE_FASTNEAR_API_URL: url(), - VITE_ALCHEMY_POLYGON_URL: url(), VITE_KEEPKEY_VERSIONS_URL: url(), VITE_KEEPKEY_LATEST_RELEASE_URL: url(), VITE_WALLET_MIGRATION_URL: url(), @@ -211,9 +210,7 @@ const validators = { VITE_FEATURE_PORTALS_SWAPPER: bool({ default: false }), VITE_FEATURE_ONE_INCH: bool({ default: false }), VITE_SENTRY_DSN_URL: url(), - VITE_ALCHEMY_API_KEY: str(), VITE_MORALIS_API_KEY: str(), - VITE_ALCHEMY_SOLANA_BASE_URL: url(), VITE_CHATWOOT_TOKEN: str(), VITE_CHATWOOT_URL: str(), VITE_FEATURE_CHATWOOT: bool({ default: false }), @@ -240,6 +237,7 @@ const validators = { VITE_FEATURE_RUNEPOOL_WITHDRAW: bool({ default: false }), VITE_FEATURE_MARKETS: bool({ default: false }), VITE_PORTALS_BASE_URL: url(), + VITE_PROXY_API_BASE_URL: url({ default: 'https://api.proxy.shapeshift.com' }), VITE_ZERION_BASE_URL: url(), VITE_FEATURE_PHANTOM_WALLET: bool({ default: false }), VITE_FEATURE_FOX_PAGE_FOX_SECTION: bool({ default: true }), diff --git a/src/lib/alchemySdkInstance.ts b/src/lib/alchemySdkInstance.ts deleted file mode 100644 index 726b94c718f..00000000000 --- a/src/lib/alchemySdkInstance.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { ChainId } from '@shapeshiftoss/caip' -import { - arbitrumChainId, - baseChainId, - ethChainId, - optimismChainId, - polygonChainId, -} from '@shapeshiftoss/caip' -import { Alchemy, Network } from 'alchemy-sdk' - -import { getConfig } from '@/config' - -const alchemyInstanceMap: Map = new Map() - -export const ALCHEMY_SDK_SUPPORTED_CHAIN_IDS = [ - ethChainId, - polygonChainId, - optimismChainId, - arbitrumChainId, - baseChainId, -] as const - -export const getAlchemyInstanceByChainId = (chainId: ChainId): Alchemy => { - // Note, make sure to not unify this guy and `instance` below. - // This is a set, not an array, calling .set() will not automagically update `maybeInstance` to the new reference - // This should probably be an Array for dev QoL but cba to change it as part of this eslint PR - const maybeInstance = alchemyInstanceMap.get(chainId) - if (maybeInstance) return maybeInstance - - const apiKey = (() => { - switch (chainId) { - case polygonChainId: - case ethChainId: - case optimismChainId: - case arbitrumChainId: - case baseChainId: - return getConfig().VITE_ALCHEMY_API_KEY - default: - return undefined - } - })() - - const network = (() => { - switch (chainId) { - case polygonChainId: - return Network.MATIC_MAINNET - case ethChainId: - return Network.ETH_MAINNET - case optimismChainId: - return Network.OPT_MAINNET - case arbitrumChainId: - return Network.ARB_MAINNET - case baseChainId: - return Network.BASE_MAINNET - default: - return undefined - } - })() - - if (!apiKey || !network) throw new Error(`Cannot get Alchemy Instance for chainId: ${chainId}`) - - const config = { - apiKey, - network, - } - - alchemyInstanceMap.set(chainId, new Alchemy(config)) - - const instance = alchemyInstanceMap.get(chainId) - - if (!instance) throw new Error(`Cannot get Alchemy Instance for chainId: ${chainId}`) - - return instance -} diff --git a/src/lib/customTokenImportSupportedChainIds.ts b/src/lib/customTokenImportSupportedChainIds.ts new file mode 100644 index 00000000000..3664c292743 --- /dev/null +++ b/src/lib/customTokenImportSupportedChainIds.ts @@ -0,0 +1,18 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import { + arbitrumChainId, + baseChainId, + ethChainId, + optimismChainId, + polygonChainId, + solanaChainId, +} from '@shapeshiftoss/caip' + +export const CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS: ChainId[] = [ + ethChainId, + polygonChainId, + optimismChainId, + arbitrumChainId, + baseChainId, + solanaChainId, +] diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index a62102f518d..07c759392da 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -82,14 +82,11 @@ interface ImportMetaEnv { readonly VITE_KEEPKEY_VERSIONS_URL: string readonly VITE_KEEPKEY_LATEST_RELEASE_URL: string readonly VITE_COWSWAP_BASE_URL: string - readonly VITE_ALCHEMY_POLYGON_URL: string readonly VITE_TOKEMAK_STATS_URL: string readonly VITE_COINCAP_API_KEY: string readonly VITE_EXCHANGERATEHOST_BASE_URL: string readonly VITE_EXCHANGERATEHOST_API_KEY: string - readonly VITE_ALCHEMY_API_KEY: string readonly VITE_MORALIS_API_KEY: string - readonly VITE_ALCHEMY_SOLANA_BASE_URL: string readonly VITE_BOARDROOM_API_BASE_URL: string readonly VITE_BOARDROOM_APP_BASE_URL: string readonly VITE_SNAPSHOT_BASE_URL: string @@ -113,6 +110,7 @@ interface ImportMetaEnv { readonly VITE_WALLET_CONNECT_WALLET_PROJECT_ID: string readonly VITE_WALLET_CONNECT_RELAY_URL: string readonly VITE_PORTALS_BASE_URL: string + readonly VITE_PROXY_API_BASE_URL: string readonly VITE_SNAP_ID: string readonly VITE_SNAP_VERSION: string readonly VITE_EXPERIMENTAL_CUSTOM_SEND_NONCE: string From dae44ac790e798b656947a9e199e2108002e28e6 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:10:15 -0600 Subject: [PATCH 2/6] cleanup --- .env | 2 +- .env.development | 3 + headers/csps/alchemy.ts | 5 - headers/csps/chains/ethereum.ts | 2 - headers/csps/customTokenImport.ts | 1 - headers/csps/index.ts | 2 - package.json | 1 - pnpm-lock.yaml | 140 ------------------ .../GlobalSearch/AssetSearchResults.tsx | 10 +- .../Header/GlobalSearch/GlobalSearchModal.tsx | 56 ++----- .../components/SearchTermAssetList.tsx | 47 ++---- .../hooks/useGetCustomTokensQuery.tsx | 88 +++++------ src/index.tsx | 8 +- src/lib/assetSearch/deduplicateAssets.ts | 7 +- src/state/slices/common-selectors.ts | 12 +- src/utils/sentry/httpclient.ts | 2 +- 16 files changed, 91 insertions(+), 295 deletions(-) delete mode 100644 headers/csps/alchemy.ts diff --git a/.env b/.env index 66214022e22..012099c765a 100644 --- a/.env +++ b/.env @@ -204,7 +204,7 @@ VITE_COINCAP_API_KEY=dab646843b251ce2d28864982989c335e1f0d32fa14e4ecc6b40cd057ec VITE_EXCHANGERATEHOST_BASE_URL=https://api.exchangerate.host VITE_EXCHANGERATEHOST_API_KEY=8f7515ffddef9d3e449b45f93108ca4d -# Moralis API key - to be used either with Alchemy SDK or directly with the REST endpoints +# Moralis API key VITE_MORALIS_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6ImY1ZmVjMTg4LTc0YzUtNDk0ZC05ZmFjLTZmYzQ5MTAyZTVhOCIsIm9yZ0lkIjoiNDYzNTU5IiwidXNlcklkIjoiNDc2OTA5IiwidHlwZUlkIjoiODE0NWUwYjEtYjEwNi00NzQyLTg2NDAtNzk1NmU4ZGQ5ZGFkIiwidHlwZSI6IlBST0pFQ1QiLCJpYXQiOjE3NTQ0NzEyNTksImV4cCI6NDkxMDIzMTI1OX0.Aa-ELM_vZR0i7Z1nQfh0rsx6ES21DHNEbNsjL28AMMw # boardroom diff --git a/.env.development b/.env.development index 5666ec50e22..0da3372f524 100644 --- a/.env.development +++ b/.env.development @@ -116,6 +116,9 @@ VITE_NOTIFICATIONS_SERVER_URL=https://dev-api.notifications-service.shapeshift.c # VITE_USER_SERVER_URL=/user-api # VITE_NOTIFICATIONS_SERVER_URL=/notifications-api +# Proxy API +VITE_PROXY_API_BASE_URL=https://dev-api.proxy.shapeshift.com + VITE_FEATURE_WC_DIRECT_CONNECTION=true VITE_FEATURE_CETUS_SWAP=true VITE_FEATURE_MANTLE=true diff --git a/headers/csps/alchemy.ts b/headers/csps/alchemy.ts deleted file mode 100644 index 1c4c8397fa4..00000000000 --- a/headers/csps/alchemy.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Csp } from '../types' - -export const csp: Csp = { - 'connect-src': ['https://*.g.alchemy.com/v2/'], -} diff --git a/headers/csps/chains/ethereum.ts b/headers/csps/chains/ethereum.ts index 701ee041c2d..8e960034175 100644 --- a/headers/csps/chains/ethereum.ts +++ b/headers/csps/chains/ethereum.ts @@ -11,8 +11,6 @@ export const csp: Csp = { env.VITE_ETHEREUM_NODE_URL, env.VITE_UNCHAINED_ETHEREUM_HTTP_URL, env.VITE_UNCHAINED_ETHEREUM_WS_URL, - 'https://eth.llamarpc.com', - env.VITE_ALCHEMY_POLYGON_URL, ...FALLBACK_RPC_URLS.ethereum, ], } diff --git a/headers/csps/customTokenImport.ts b/headers/csps/customTokenImport.ts index 4b0ee328c39..66dbafe7d80 100644 --- a/headers/csps/customTokenImport.ts +++ b/headers/csps/customTokenImport.ts @@ -5,6 +5,5 @@ export const csp: Csp = { // Common metadata sources 'https://arweave.net/', 'https://*.arweave.net/', - 'https://api.proxy.shapeshift.com', ], } diff --git a/headers/csps/index.ts b/headers/csps/index.ts index 93be4936c67..a1a1aeeb97b 100644 --- a/headers/csps/index.ts +++ b/headers/csps/index.ts @@ -1,6 +1,5 @@ import { csp as across } from './across' import { csp as agenticChat } from './agenticChat' -import { csp as alchemy } from './alchemy' import { csp as trustwallet } from './assetService/trustwallet' import { csp as base } from './base' import { csp as chainflip } from './chainflip' @@ -108,7 +107,6 @@ export const csps = [ base, agenticChat, hypelab, - alchemy, moralis, chainflip, chatwoot, diff --git a/package.json b/package.json index c928567fd82..673b7c62a60 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,6 @@ "@walletconnect/utils": "^2.20.2", "@xstate/react": "5.0.5", "ai": "^6.0.39", - "alchemy-sdk": "^3.4.1", "axios": "^1.13.5", "axios-cache-interceptor": "^1.11.1", "bech32": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7776c0e47c..082a07b85af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -303,9 +303,6 @@ importers: ai: specifier: ^6.0.39 version: 6.0.105(zod@3.25.76) - alchemy-sdk: - specifier: ^3.4.1 - version: 3.6.5(bufferutil@4.1.0)(utf-8-validate@6.0.6) axios: specifier: ^1.13.5 version: 1.13.6(debug@4.4.3) @@ -4227,9 +4224,6 @@ packages: '@ethersproject/units@5.7.0': resolution: {integrity: sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==} - '@ethersproject/units@5.8.0': - resolution: {integrity: sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==} - '@ethersproject/wallet@5.7.0': resolution: {integrity: sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==} @@ -9253,9 +9247,6 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} - alchemy-sdk@3.6.5: - resolution: {integrity: sha512-vikvJvExqPoifnOtnIPoANwS2C46Nv44XsEWJz8kd5hrnZrS320GmhKWGyKSgupd8cvudAWv1+76iSr0pjy8DA==} - algo-msgpack-with-bigint@2.1.1: resolution: {integrity: sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ==} engines: {node: '>= 10'} @@ -10590,10 +10581,6 @@ packages: d3-voronoi@1.1.4: resolution: {integrity: sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==} - d@1.0.2: - resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} - engines: {node: '>=0.12'} - damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -11126,26 +11113,15 @@ packages: es-toolkit@1.44.0: resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} - es5-ext@0.10.64: - resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} - engines: {node: '>=0.10'} - es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} - es6-symbol@3.1.4: - resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} - engines: {node: '>=0.12'} - esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -11371,10 +11347,6 @@ packages: deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true - esniff@2.0.1: - resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} - engines: {node: '>=0.10'} - espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -11556,9 +11528,6 @@ packages: ev-emitter@2.1.2: resolution: {integrity: sha512-jQ5Ql18hdCQ4qS+RCrbLfz1n+Pags27q5TwMKvZyhp5hh2UULUYZUy1keqj6k6SYsdqIYjnmz7xyyEY0V67B8Q==} - event-emitter@0.3.5: - resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} - eventemitter2@5.0.1: resolution: {integrity: sha512-5EM1GHXycJBS6mauYAbVKT1cVs7POKWb2NXD4Vyt8dDqeZa7LaDK1/sjtL+Zb0lzTpSNil4596Dyu97hz37QLg==} @@ -11624,9 +11593,6 @@ packages: exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -13684,9 +13650,6 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} - next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -15656,9 +15619,6 @@ packages: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} - sturdy-websocket@0.2.1: - resolution: {integrity: sha512-NnzSOEKyv4I83qbuKw9ROtJrrT6Z/Xt7I0HiP/e6H6GnpeTDvzwGIGeJ8slai+VwODSHQDooW2CAilJwT9SpRg==} - style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -16103,9 +16063,6 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} - type@2.7.3: - resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} - typecast@0.0.1: resolution: {integrity: sha512-L2f5OCLKsJdCjSyN0d5O6CkNxhiC8EQ2XlXnHpWZVNfF+mj2OTaXhAVnP0/7SY/sxO1DHZpOFMpIuGlFUZEGNA==} @@ -17041,10 +16998,6 @@ packages: websocket-stream@5.5.2: resolution: {integrity: sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==} - websocket@1.0.35: - resolution: {integrity: sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==} - engines: {node: '>=4.0.0'} - whatwg-fetch@2.0.4: resolution: {integrity: sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==} @@ -17289,11 +17242,6 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} - yaeti@0.0.6: - resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} - engines: {node: '>=0.10.32'} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -20376,12 +20324,6 @@ snapshots: '@ethersproject/constants': 5.7.0 '@ethersproject/logger': 5.7.0 - '@ethersproject/units@5.8.0': - dependencies: - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/constants': 5.8.0 - '@ethersproject/logger': 5.8.0 - '@ethersproject/wallet@5.7.0': dependencies: '@ethersproject/abstract-provider': 5.7.0 @@ -34515,30 +34457,6 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy-sdk@3.6.5(bufferutil@4.1.0)(utf-8-validate@6.0.6): - dependencies: - '@ethersproject/abi': 5.8.0 - '@ethersproject/abstract-provider': 5.8.0 - '@ethersproject/bignumber': 5.8.0 - '@ethersproject/bytes': 5.8.0 - '@ethersproject/contracts': 5.8.0 - '@ethersproject/hash': 5.8.0 - '@ethersproject/networks': 5.8.0 - '@ethersproject/providers': 5.8.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - '@ethersproject/units': 5.8.0 - '@ethersproject/wallet': 5.8.0 - '@ethersproject/web': 5.8.0 - '@solana/web3.js': 1.98.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - axios: 1.13.6(debug@4.4.3) - sturdy-websocket: 0.2.1 - websocket: 1.0.35 - transitivePeerDependencies: - - bufferutil - - debug - - encoding - - supports-color - - utf-8-validate - algo-msgpack-with-bigint@2.1.1: {} algosdk@1.24.1: @@ -36143,11 +36061,6 @@ snapshots: d3-voronoi@1.1.4: {} - d@1.0.2: - dependencies: - es5-ext: 0.10.64 - type: 2.7.3 - damerau-levenshtein@1.0.8: {} dargs@7.0.0: {} @@ -36768,33 +36681,15 @@ snapshots: es-toolkit@1.44.0: {} - es5-ext@0.10.64: - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.4 - esniff: 2.0.1 - next-tick: 1.1.0 - es6-error@4.1.1: optional: true - es6-iterator@2.0.3: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - es6-symbol: 3.1.4 - es6-promise@4.2.8: {} es6-promisify@5.0.0: dependencies: es6-promise: 4.2.8 - es6-symbol@3.1.4: - dependencies: - d: 1.0.2 - ext: 1.7.0 - esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -37183,13 +37078,6 @@ snapshots: transitivePeerDependencies: - supports-color - esniff@2.0.1: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - event-emitter: 0.3.5 - type: 2.7.3 - espree@10.4.0: dependencies: acorn: 8.16.0 @@ -37610,11 +37498,6 @@ snapshots: ev-emitter@2.1.2: {} - event-emitter@0.3.5: - dependencies: - d: 1.0.2 - es5-ext: 0.10.64 - eventemitter2@5.0.1: {} eventemitter2@6.4.9: {} @@ -37702,10 +37585,6 @@ snapshots: exsolve@1.0.8: {} - ext@1.7.0: - dependencies: - type: 2.7.3 - extend@3.0.2: {} extension-port-stream@2.1.1: @@ -40349,8 +40228,6 @@ snapshots: netmask@2.0.2: {} - next-tick@1.1.0: {} - nice-try@1.0.5: {} no-case@3.0.4: @@ -42877,8 +42754,6 @@ snapshots: dependencies: '@tokenizer/token': 0.3.0 - sturdy-websocket@0.2.1: {} - style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -43359,8 +43234,6 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 - type@2.7.3: {} - typecast@0.0.1: {} typed-array-buffer@1.0.3: @@ -44733,17 +44606,6 @@ snapshots: - bufferutil - utf-8-validate - websocket@1.0.35: - dependencies: - bufferutil: 4.1.0 - debug: 2.6.9 - es5-ext: 0.10.64 - typedarray-to-buffer: 3.1.5 - utf-8-validate: 5.0.10 - yaeti: 0.0.6 - transitivePeerDependencies: - - supports-color - whatwg-fetch@2.0.4: {} whatwg-fetch@3.6.20: {} @@ -45027,8 +44889,6 @@ snapshots: y18n@5.0.8: {} - yaeti@0.0.6: {} - yallist@3.1.1: {} yallist@4.0.0: {} diff --git a/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx b/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx index e9c273f494e..c51a9b14f81 100644 --- a/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx +++ b/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx @@ -1,4 +1,4 @@ -import { List } from '@chakra-ui/react' +import { Flex, List, Spinner } from '@chakra-ui/react' import type { Asset } from '@shapeshiftoss/types' import { memo, useMemo } from 'react' @@ -20,6 +20,14 @@ export const AssetSearchResults = memo( }, [results.length]) if (isSearching && noResults) { + return ( + + + + ) + } + + if (!isSearching && noResults && searchQuery) { return } diff --git a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx index 9e3594c9f8d..a3b485a893e 100644 --- a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx +++ b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx @@ -9,9 +9,8 @@ import { useUpdateEffect, } from '@chakra-ui/react' import { captureException, setContext } from '@sentry/react' -import { toAssetId } from '@shapeshiftoss/caip' -import type { Asset, KnownChainIds } from '@shapeshiftoss/types' -import { getAssetNamespaceFromChainId, makeAsset } from '@shapeshiftoss/utils' +import type { Asset } from '@shapeshiftoss/types' +import { makeAsset } from '@shapeshiftoss/utils' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import MultiRef from 'react-multi-ref' import { useNavigate } from 'react-router-dom' @@ -23,7 +22,6 @@ import { GlobalFilter } from '@/components/StakingVaults/GlobalFilter' import { useGetCustomTokensQuery } from '@/components/TradeAssetSearch/hooks/useGetCustomTokensQuery' import { useModalRegistration } from '@/context/ModalStackProvider' import { CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS } from '@/lib/customTokenImportSupportedChainIds' -import { isSome } from '@/lib/utils' import { assets as assetsSlice } from '@/state/slices/assetsSlice/assetsSlice' import { selectAssets, selectAssetsBySearchQuery } from '@/state/slices/selectors' import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice' @@ -48,7 +46,6 @@ export const GlobalSearchModal = memo( const dispatch = useAppDispatch() const searchFilter = useMemo(() => ({ searchQuery, limit: 10 }), [searchQuery]) const results = useAppSelector(state => selectAssetsBySearchQuery(state, searchFilter)) - const assetResults = results const resultsCount = results.length const isMac = useMemo(() => /Mac/.test(navigator.userAgent), []) const handleClose = useCallback(() => { @@ -66,50 +63,15 @@ export const GlobalSearchModal = memo( chainIds: CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS, }) - const customAssets = useMemo(() => { - if (!customTokens?.length) return [] + useEffect(() => { + if (!customTokens?.length) return - // Do not move me to a regular useSelector(), as this is reactive on the *whole* assets set and would make this component extremely reactive for no reason const assetsById = selectAssets(store.getState()) - const assets = customTokens - .map(metaData => { - if (!metaData) return null - const { name, symbol, decimals, logo, chainId, contractAddress } = metaData - - if (!name || !symbol || !decimals) return null - - const assetId = toAssetId({ - chainId, - assetNamespace: getAssetNamespaceFromChainId(chainId as KnownChainIds), - assetReference: contractAddress, - }) - - const minimalAsset = { - assetId, - name, - symbol, - precision: decimals, - icon: logo ?? undefined, - } - - return makeAsset(assetsById, minimalAsset) - }) - .filter(isSome) - - return assets - }, [customTokens]) - - useEffect(() => { - customAssets.forEach(asset => { - // Do not move me to a regular useSelector(), as this is reactive on the *whole* assets set and would make this component extremely reactive for no reason - const assetsById = selectAssets(store.getState()) - - if (!assetsById[asset.assetId]) { - dispatch(assetsSlice.actions.upsertAsset(asset)) - } - }) - }, [customAssets, dispatch]) + customTokens + .filter(token => !assetsById[token.assetId]) + .forEach(token => dispatch(assetsSlice.actions.upsertAsset(makeAsset(assetsById, token)))) + }, [customTokens, dispatch]) useEffect(() => { if (!searchQuery) setActiveIndex(0) @@ -225,7 +187,7 @@ export const GlobalSearchModal = memo( { @@ -109,32 +107,15 @@ export const SearchTermAssetList = ({ }, [assetsForChain]) const customAssets: Asset[] = useMemo(() => { - return (customTokens ?? []) - .map(metaData => { - if (!metaData) return null - const { name, symbol, decimals, logo } = metaData - // If we can't get all the information we need to create an Asset, don't allow the custom token - if (!name || !symbol || !decimals) return null - const assetId = toAssetId({ - chainId: metaData.chainId, - assetNamespace: getAssetNamespaceFromChainId(metaData.chainId as KnownChainIds), - assetReference: metaData.contractAddress, - }) - - // Skip if we already have this asset - if (assetIdMap[assetId] !== undefined) return null - - const minimalAsset: MinimalAsset = { - assetId, - name, - symbol, - precision: decimals, - icon: logo ?? undefined, - } - return makeAsset(assetsById, minimalAsset) - }) - .filter(isSome) - }, [assetIdMap, assetsById, customTokens]) + if (!customTokens?.length) return [] + + // Do not move me to a regular useSelector(), as this is reactive on the *whole* assets set and would make this component extremely reactive for no reason + const assetsById = selectAssets(store.getState()) + + return customTokens + .filter(token => !assetsById[token.assetId]) + .map(token => makeAsset(assetsById, token)) + }, [customTokens]) const searchTermAssets = useMemo(() => { const filteredAssets: Asset[] = (() => { diff --git a/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx b/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx index 94434e050c8..dbefed5352b 100644 --- a/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx +++ b/src/components/TradeAssetSearch/hooks/useGetCustomTokensQuery.tsx @@ -1,6 +1,8 @@ import type { ChainId } from '@shapeshiftoss/caip' -import { solanaChainId } from '@shapeshiftoss/caip' -import { isEvmChainId } from '@shapeshiftoss/chain-adapters' +import { toAssetId } from '@shapeshiftoss/caip' +import type { KnownChainIds } from '@shapeshiftoss/types' +import type { MinimalAsset } from '@shapeshiftoss/utils' +import { getAssetNamespaceFromChainId } from '@shapeshiftoss/utils' import type { UseQueryResult } from '@tanstack/react-query' import { skipToken, useQueries } from '@tanstack/react-query' import axios, { isAxiosError } from 'axios' @@ -12,14 +14,11 @@ import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag' import { isSolanaAddress } from '@/lib/utils/isSolanaAddress' import { mergeQueryOutputs } from '@/react-queries/helpers' -type TokenMetadata = { - name?: string - symbol?: string - decimals?: number - logo?: string - chainId: ChainId - contractAddress: string - price: string +type TokenMetadataResponse = { + name: string + symbol: string + decimals: number | null + logo: string | null } type UseGetCustomTokensQueryProps = { @@ -38,42 +37,40 @@ const getCustomTokenQueryKey = (contractAddress: string, chainId: ChainId): Cust export const useGetCustomTokensQuery = ({ contractAddress, chainIds, -}: UseGetCustomTokensQueryProps): UseQueryResult<(TokenMetadata | undefined)[], Error[]> => { +}: UseGetCustomTokensQueryProps): UseQueryResult => { const customTokenImportEnabled = useFeatureFlag('CustomTokenImport') - const getTokenMetadata = useCallback( - async (chainId: ChainId): Promise => { + const getCustomToken = useCallback( + async (chainId: ChainId): Promise => { try { - const { data } = await axios.get<{ - chainId: ChainId - tokenAddress: string - name?: string - symbol?: string - decimals?: number - logo?: string - }>(`${getConfig().VITE_PROXY_API_BASE_URL}/api/v1/tokens/metadata`, { - params: { - chainId, - tokenAddress: contractAddress, + const { data } = await axios.get( + `${getConfig().VITE_PROXY_API_BASE_URL}/api/v1/tokens/metadata`, + { + params: { + chainId, + tokenAddress: contractAddress, + }, }, - timeout: 10_000, + ) + + if (data.decimals === null) return undefined + + const assetId = toAssetId({ + chainId, + assetNamespace: getAssetNamespaceFromChainId(chainId as KnownChainIds), + assetReference: contractAddress, }) return { + assetId, name: data.name, symbol: data.symbol, - decimals: data.decimals, - chainId: data.chainId, - contractAddress: data.tokenAddress, - price: '0', - logo: data.logo, + precision: data.decimals, + icon: data.logo ?? undefined, } - } catch (e) { - if (isAxiosError(e) && (e.response?.status === 404 || e.response?.status === 422)) { - return undefined - } - - throw e + } catch (err) { + if (isAxiosError(err) && (err.response?.status ?? 0) >= 500) throw err + return undefined } }, [contractAddress], @@ -88,20 +85,15 @@ export const useGetCustomTokensQuery = ({ const getQueryFn = useCallback( (chainId: ChainId) => () => { - if (isValidSolanaAddress && chainId === solanaChainId) { - return getTokenMetadata(chainId) - } else if (isValidEvmAddress && isEvmChainId(chainId)) { - return getTokenMetadata(chainId) - } else { - return skipToken - } + if (!isValidEvmAddress && !isValidSolanaAddress) return skipToken + return getCustomToken(chainId) }, - [getTokenMetadata, isValidEvmAddress, isValidSolanaAddress], + [getCustomToken, isValidEvmAddress, isValidSolanaAddress], ) - const isTokenMetadata = ( - result: TokenMetadata | typeof skipToken | undefined, - ): result is TokenMetadata => result !== undefined && result !== skipToken + const isMinimalAsset = ( + result: MinimalAsset | typeof skipToken | undefined, + ): result is MinimalAsset => result !== undefined && result !== skipToken const customTokenQueries = useQueries({ queries: chainIds.map(chainId => ({ @@ -110,7 +102,7 @@ export const useGetCustomTokensQuery = ({ enabled: customTokenImportEnabled, staleTime: Infinity, })), - combine: queries => mergeQueryOutputs(queries, results => results.filter(isTokenMetadata)), + combine: queries => mergeQueryOutputs(queries, results => results.filter(isMinimalAsset)), }) return customTokenQueries diff --git a/src/index.tsx b/src/index.tsx index 566c59eadc7..983145e6e70 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -112,13 +112,7 @@ if (shouldEnableSentry) { [500, 599], // Only server errors, not client errors ], - denyUrls: [ - 'alchemy.com', - 'snapshot.org', - 'coingecko.com', - 'coincap.io', - 'coinmarketcap.com', - ], + denyUrls: ['snapshot.org', 'coingecko.com', 'coincap.io', 'coinmarketcap.com'], }), browserApiErrorsIntegration(), breadcrumbsIntegration(), diff --git a/src/lib/assetSearch/deduplicateAssets.ts b/src/lib/assetSearch/deduplicateAssets.ts index 07c7e9a9efe..019e14197de 100644 --- a/src/lib/assetSearch/deduplicateAssets.ts +++ b/src/lib/assetSearch/deduplicateAssets.ts @@ -26,7 +26,12 @@ export const deduplicateAssets = ( : false for (const asset of assets) { - const familyKey = asset.relatedAssetKey ?? asset.assetId + // If the search term appears in the assetId (contract address search), use assetId as the + // family key so each chain variant is shown independently. Grouping by relatedAssetKey relies + // on isPrimary being correct, which isn't guaranteed (asset generation bugs, or makeAsset + // defaulting isPrimary: true for custom imports). + const matchedByAddress = searchLower && asset.assetId.toLowerCase().includes(searchLower) + const familyKey = matchedByAddress ? asset.assetId : asset.relatedAssetKey ?? asset.assetId const existing = familyToAsset.get(familyKey) const isExact = hasExactSymbolMatch && isExactMatch(searchLower, asset.symbol) diff --git a/src/state/slices/common-selectors.ts b/src/state/slices/common-selectors.ts index aecf987d99f..66fca10c4fe 100644 --- a/src/state/slices/common-selectors.ts +++ b/src/state/slices/common-selectors.ts @@ -595,11 +595,13 @@ export const selectAssetsBySearchQuery = createCachedSelector( const sortedAssets = useAllAssets ? allAssets : primaryAssets - // Filter out spam tokens (low market cap) but keep assets with no market data - const filteredAssets = sortedAssets.filter(asset => { - const marketCap = bnOrZero(marketDataUsd[asset.assetId]?.marketCap) - return marketCap.isZero() || marketCap.gte(MINIMUM_MARKET_CAP_THRESHOLD) - }) + // Filter out spam tokens (low market cap) but keep assets with no market data, unless user is searching by contract address + const filteredAssets = isContractAddressSearch + ? sortedAssets + : sortedAssets.filter(asset => { + const marketCap = bnOrZero(marketDataUsd[asset.assetId]?.marketCap) + return marketCap.isZero() || marketCap.gte(MINIMUM_MARKET_CAP_THRESHOLD) + }) const matchedAssets = searchAssets(searchQuery, filteredAssets) const deduplicated = deduplicateAssets(matchedAssets, searchQuery) diff --git a/src/utils/sentry/httpclient.ts b/src/utils/sentry/httpclient.ts index 4dde3b8378a..394a3f42fbf 100644 --- a/src/utils/sentry/httpclient.ts +++ b/src/utils/sentry/httpclient.ts @@ -45,7 +45,7 @@ interface HttpClientOptions { * This array can contain strings, not regular expressions. * If omitted, no filtering by requests blacklist will be applied. * - * Example: ['snapshot.org', 'alchemy'] + * Example: ['snapshot.org'] */ denyUrls?: string[] } From 974eca49ae6a67e1413f82b85c74dd90586bef07 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:28:02 -0600 Subject: [PATCH 3/6] fix tests --- src/lib/assetSearch/deduplicateAssets.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/assetSearch/deduplicateAssets.test.ts b/src/lib/assetSearch/deduplicateAssets.test.ts index 5e39e5e9b83..492306a69ef 100644 --- a/src/lib/assetSearch/deduplicateAssets.test.ts +++ b/src/lib/assetSearch/deduplicateAssets.test.ts @@ -87,10 +87,10 @@ describe('deduplicateAssets', () => { it('prefers primary AXLUSDC over non-primary when both have exact match', () => { // Create AXLUSDC assets with their own family and a primary - const axlusdcFamily = 'eip155:1/erc20:axlusdc-family' + const axlusdcFamily = 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const axlusdcPrimary = { ...AXLUSDC_OPTIMISM, - assetId: 'eip155:1/erc20:axlusdc-primary' as const, + assetId: 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const, relatedAssetKey: axlusdcFamily, isPrimary: true, } @@ -109,10 +109,10 @@ describe('deduplicateAssets', () => { it('returns primary even when non-primary exact match comes first in array', () => { // Create AXLUSDC assets with primary coming second in array - const axlusdcFamily = 'eip155:1/erc20:axlusdc-family' + const axlusdcFamily = 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' const axlusdcPrimary = { ...AXLUSDC_OPTIMISM, - assetId: 'eip155:1/erc20:axlusdc-primary' as const, + assetId: 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const, relatedAssetKey: axlusdcFamily, isPrimary: true, } @@ -130,13 +130,13 @@ describe('deduplicateAssets', () => { it('shows both AXLUSDC and AXLUSDT groups when searching "axlusd"', () => { // Create separate families for AXLUSDC and AXLUSDT - const axlusdcFamily = 'eip155:1/erc20:axlusdc-family' - const axlusdtFamily = 'eip155:1/erc20:axlusdt-family' + const axlusdcFamily = 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + const axlusdtFamily = 'eip155:1/erc20:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' const assets = [ { ...AXLUSDC_OPTIMISM, relatedAssetKey: axlusdcFamily, isPrimary: false }, { ...AXLUSDC_OPTIMISM, - assetId: 'eip155:1/erc20:axlusdc-primary' as const, + assetId: 'eip155:1/erc20:0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const, relatedAssetKey: axlusdcFamily, isPrimary: true, }, @@ -149,7 +149,7 @@ describe('deduplicateAssets', () => { }, { ...AXLUSDC_ARBITRUM, - assetId: 'eip155:1/erc20:axlusdt-primary' as const, + assetId: 'eip155:1/erc20:0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb' as const, symbol: 'AXLUSDT', name: 'Axelar USDT', relatedAssetKey: axlusdtFamily, From fd9ef04568b778a5a091bc65342c7e7ea91d024a Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:47:02 -0600 Subject: [PATCH 4/6] fix loading state --- .../Layout/Header/GlobalSearch/AssetSearchResults.tsx | 8 ++++---- .../Layout/Header/GlobalSearch/GlobalSearchModal.tsx | 7 +------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx b/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx index c51a9b14f81..605f35e69e7 100644 --- a/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx +++ b/src/components/Layout/Header/GlobalSearch/AssetSearchResults.tsx @@ -9,17 +9,17 @@ import { SearchEmpty } from '@/components/StakingVaults/SearchEmpty' export type AssetSearchResultsProps = { results: Asset[] searchQuery: string - isSearching: boolean + isLoading: boolean onClickResult: (item: Asset) => void } export const AssetSearchResults = memo( - ({ results, searchQuery, isSearching, onClickResult }: AssetSearchResultsProps) => { + ({ results, searchQuery, isLoading, onClickResult }: AssetSearchResultsProps) => { const noResults = useMemo(() => { return !results.length }, [results.length]) - if (isSearching && noResults) { + if (isLoading && noResults) { return ( @@ -27,7 +27,7 @@ export const AssetSearchResults = memo( ) } - if (!isSearching && noResults && searchQuery) { + if (!isLoading && noResults && searchQuery) { return } diff --git a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx index a3b485a893e..bdb90389caa 100644 --- a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx +++ b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx @@ -160,11 +160,6 @@ export const GlobalSearchModal = memo( setActiveIndex(0) }, [searchQuery]) - const isSearching = useMemo( - () => searchQuery.length > 0 || isLoadingCustomTokens, - [searchQuery.length, isLoadingCustomTokens], - ) - return ( @@ -189,7 +184,7 @@ export const GlobalSearchModal = memo( From 6b30b0025825f4597d77ae479272fa8d4d84c806 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:02:33 -0600 Subject: [PATCH 5/6] code rabbit review feedback --- .../Header/GlobalSearch/GlobalSearchModal.tsx | 29 ++++++++++++++----- .../components/SearchTermAssetList.tsx | 1 - src/lib/assetSearch/deduplicateAssets.test.ts | 10 +++++++ src/lib/assetSearch/deduplicateAssets.ts | 14 +++++---- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx index bdb90389caa..5592095426d 100644 --- a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx +++ b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx @@ -45,8 +45,7 @@ export const GlobalSearchModal = memo( const navigate = useNavigate() const dispatch = useAppDispatch() const searchFilter = useMemo(() => ({ searchQuery, limit: 10 }), [searchQuery]) - const results = useAppSelector(state => selectAssetsBySearchQuery(state, searchFilter)) - const resultsCount = results.length + const searchAssets = useAppSelector(state => selectAssetsBySearchQuery(state, searchFilter)) const isMac = useMemo(() => /Mac/.test(navigator.userAgent), []) const handleClose = useCallback(() => { setSearchQuery('') @@ -63,15 +62,31 @@ export const GlobalSearchModal = memo( chainIds: CUSTOM_TOKEN_IMPORT_SUPPORTED_CHAIN_IDS, }) - useEffect(() => { - if (!customTokens?.length) return + const customAssets = useMemo(() => { + if (!customTokens?.length) return [] const assetsById = selectAssets(store.getState()) - customTokens + return customTokens .filter(token => !assetsById[token.assetId]) - .forEach(token => dispatch(assetsSlice.actions.upsertAsset(makeAsset(assetsById, token)))) - }, [customTokens, dispatch]) + .map(token => makeAsset(assetsById, token)) + }, [customTokens]) + + useEffect(() => { + customAssets.forEach(asset => { + dispatch(assetsSlice.actions.upsertAsset(asset)) + }) + }, [customAssets, dispatch]) + + const results = useMemo(() => { + if (!customAssets.length) return searchAssets + const existingIds = new Set(searchAssets.map(a => a.assetId)) + return searchAssets.concat(customAssets.filter(a => !existingIds.has(a.assetId))) + }, [searchAssets, customAssets]) + + console.log({ searchAssets, customAssets, results }) + + const resultsCount = results.length useEffect(() => { if (!searchQuery) setActiveIndex(0) diff --git a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx index 2e5a76987a4..47ffadd1efe 100644 --- a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx +++ b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx @@ -109,7 +109,6 @@ export const SearchTermAssetList = ({ const customAssets: Asset[] = useMemo(() => { if (!customTokens?.length) return [] - // Do not move me to a regular useSelector(), as this is reactive on the *whole* assets set and would make this component extremely reactive for no reason const assetsById = selectAssets(store.getState()) return customTokens diff --git a/src/lib/assetSearch/deduplicateAssets.test.ts b/src/lib/assetSearch/deduplicateAssets.test.ts index 492306a69ef..1e41863d15c 100644 --- a/src/lib/assetSearch/deduplicateAssets.test.ts +++ b/src/lib/assetSearch/deduplicateAssets.test.ts @@ -207,6 +207,16 @@ describe('deduplicateAssets', () => { expect(result[0].isPrimary).toBe(true) }) + it('shows each chain variant independently when searching by contract address', () => { + // AXLUSDC_OPTIMISM and AXLUSDC_ARBITRUM share address 0xeb466342c4d449bc9f53a865d5cb90586f405215 + const assets = [AXLUSDC_OPTIMISM, AXLUSDC_ARBITRUM] + + const result = deduplicateAssets(assets, '0xeb466342c4d449bc9f53a865d5cb90586f405215') + + expect(result).toHaveLength(2) + expect(result.map(a => a.chainId).sort()).toEqual(['eip155:10', 'eip155:42161'].sort()) + }) + describe('name search grouping', () => { it('groups "Axelar Bridged" name search results by family', () => { // AXLUSDC and AXLUSDT are in different families (USDC and USDT families) diff --git a/src/lib/assetSearch/deduplicateAssets.ts b/src/lib/assetSearch/deduplicateAssets.ts index 019e14197de..57fdf3347fb 100644 --- a/src/lib/assetSearch/deduplicateAssets.ts +++ b/src/lib/assetSearch/deduplicateAssets.ts @@ -1,3 +1,6 @@ +import { fromAssetId } from '@shapeshiftoss/caip' + +import { isContractAddress } from '../utils/isContractAddress' import type { SearchableAsset } from './types' import { isExactMatch } from './utils' @@ -26,11 +29,12 @@ export const deduplicateAssets = ( : false for (const asset of assets) { - // If the search term appears in the assetId (contract address search), use assetId as the - // family key so each chain variant is shown independently. Grouping by relatedAssetKey relies - // on isPrimary being correct, which isn't guaranteed (asset generation bugs, or makeAsset - // defaulting isPrimary: true for custom imports). - const matchedByAddress = searchLower && asset.assetId.toLowerCase().includes(searchLower) + // When the search is a valid contract address matching this asset's assetReference, + // use assetId as the family key so each chain variant is shown independently rather than + // collapsed into its family via relatedAssetKey. + const matchedByAddress = + isContractAddress(searchLower) && + fromAssetId(asset.assetId).assetReference.toLowerCase() === searchLower const familyKey = matchedByAddress ? asset.assetId : asset.relatedAssetKey ?? asset.assetId const existing = familyToAsset.get(familyKey) From 52f4cd0184421591887211e7ca598aaf4b514fa0 Mon Sep 17 00:00:00 2001 From: kaladinlight <35275952+kaladinlight@users.noreply.github.com> Date: Mon, 16 Mar 2026 21:24:01 -0600 Subject: [PATCH 6/6] remove debug log --- src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx index 5592095426d..a7279f4b03e 100644 --- a/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx +++ b/src/components/Layout/Header/GlobalSearch/GlobalSearchModal.tsx @@ -84,8 +84,6 @@ export const GlobalSearchModal = memo( return searchAssets.concat(customAssets.filter(a => !existingIds.has(a.assetId))) }, [searchAssets, customAssets]) - console.log({ searchAssets, customAssets, results }) - const resultsCount = results.length useEffect(() => {