diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx index 1c6d2d215..fc36299c7 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/ChainTokenSelectorModal.tsx @@ -31,7 +31,6 @@ import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { BigNumber } from "ethers"; import { Text, TokenImage } from "components"; import { useHotkeys } from "react-hotkeys-hook"; -import { getBridgeableSvmTokenFilterPredicate } from "./getBridgeableSvmTokenFilterPredicate"; import { isTokenUnreachable } from "./isTokenUnreachable"; import { useTrackChainSelected } from "./useTrackChainSelected"; import { useTrackTokenSelected } from "./useTrackTokenSelected"; @@ -148,27 +147,25 @@ export function ChainTokenSelectorModal({ }); // Filter by search first - const filteredTokens = enrichedTokens - .filter((t) => { - // First filter by selected chain - if (selectedChain !== null && t.chainId !== selectedChain) { - return false; - } + const filteredTokens = enrichedTokens.filter((t) => { + // First filter by selected chain + if (selectedChain !== null && t.chainId !== selectedChain) { + return false; + } - if (tokenSearch === "") { - return true; - } + if (tokenSearch === "") { + return true; + } - const keywords = [ - t.symbol.toLowerCase().replaceAll(" ", ""), - t.name.toLowerCase().replaceAll(" ", ""), - t.address.toLowerCase().replaceAll(" ", ""), - ]; - return keywords.some((keyword) => - keyword.includes(tokenSearch.toLowerCase().replaceAll(" ", "")) - ); - }) - .filter(getBridgeableSvmTokenFilterPredicate(isOriginToken, otherToken)); + const keywords = [ + t.symbol.toLowerCase().replaceAll(" ", ""), + t.name.toLowerCase().replaceAll(" ", ""), + t.address.toLowerCase().replaceAll(" ", ""), + ]; + return keywords.some((keyword) => + keyword.includes(tokenSearch.toLowerCase().replaceAll(" ", "")) + ); + }); // Sort function that prioritizes tokens with balance, then by balance amount, then alphabetically const sortTokens = (tokens: EnrichedTokenWithReachability[]) => { diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/getBridgeableSvmTokenFilterPredicate.test.ts b/src/views/SwapAndBridge/components/ChainTokenSelector/getBridgeableSvmTokenFilterPredicate.test.ts deleted file mode 100644 index c00882f17..000000000 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/getBridgeableSvmTokenFilterPredicate.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getBridgeableSvmTokenFilterPredicate } from "./getBridgeableSvmTokenFilterPredicate"; -import { EnrichedToken } from "./ChainTokenSelectorModal"; -import { solana } from "../../../../constants/chains/configs"; - -describe("getBridgeableSvmTokenFilterPredicate", () => { - it("should filter out solana tokens that are not bridgeable", () => { - const otherToken = { - chainId: solana.chainId, - } as EnrichedToken; - const predicate = getBridgeableSvmTokenFilterPredicate(false, otherToken); - const tokens = [ - { symbol: "XYZ" }, - { symbol: "USDC" }, - { symbol: "USDzC" }, - { symbol: "USDH" }, - { symbol: "USDH-SPOT" }, - ] as EnrichedToken[]; - expect(tokens.filter(predicate)).toEqual([ - { symbol: "USDC" }, - { symbol: "USDzC" }, - { symbol: "USDH" }, - { symbol: "USDH-SPOT" }, - ]); - }); -}); diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/getBridgeableSvmTokenFilterPredicate.tsx b/src/views/SwapAndBridge/components/ChainTokenSelector/getBridgeableSvmTokenFilterPredicate.tsx deleted file mode 100644 index 85f3b40d1..000000000 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/getBridgeableSvmTokenFilterPredicate.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { solana } from "../../../../constants/chains/configs"; -import { interchangeableTokensMap } from "../../../../constants/tokens"; -import { EnrichedToken } from "./ChainTokenSelectorModal"; - -/** - * Since we, temporarily, do not support destination swaps when bridging - * from SVM, we filter out all tokens from the token selector that are - * not bridgeable tokens. - */ -export const getBridgeableSvmTokenFilterPredicate = - (isOriginToken: boolean, otherToken: EnrichedToken | null | undefined) => - (token: EnrichedToken) => { - if (isOriginToken || otherToken?.chainId !== solana.chainId) return true; - const bridgeableSvmTokenSymbols = [ - "USDC", - "USDH", - "USDH-SPOT", - "USDC-SPOT", - "USDT-SPOT", - ]; - return ( - bridgeableSvmTokenSymbols.includes(token.symbol) || - bridgeableSvmTokenSymbols.some((symbol) => - interchangeableTokensMap[token.symbol]?.includes(symbol) - ) - ); - }; diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.test.ts b/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.test.ts index 6811ff69f..cc0b94d0b 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.test.ts +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.test.ts @@ -195,36 +195,92 @@ describe("isTokenUnreachable", () => { }); }); - it("should restrict ANY output tokens to Solana", () => { - const inputToken = { - chainId: CHAIN_IDs.MAINNET, - symbol: "USDC", - } as EnrichedToken; + describe("bridgeable SVM token restrictions", () => { + describe("when Solana is origin chain", () => { + it("should mark non-bridgeable destination tokens as unreachable", () => { + const originToken = { + chainId: CHAIN_IDs.SOLANA, + symbol: "USDC", + } as EnrichedToken; + + const destinationToken = { + chainId: CHAIN_IDs.MAINNET, + symbol: "ETH", // not bridgeable + } as EnrichedToken; + + // Selecting destination token (isOriginToken = false) + const isUnreachable = isTokenUnreachable( + destinationToken, + false, + originToken + ); + + expect(isUnreachable).toBe(true); + }); - const outputToken = { - chainId: CHAIN_IDs.SOLANA, - symbol: "USDT", // not bridgeable - } as EnrichedToken; + it("should NOT mark bridgeable destination tokens as unreachable", () => { + const originToken = { + chainId: CHAIN_IDs.SOLANA, + symbol: "USDC", + } as EnrichedToken; - const isUnreachable = isTokenUnreachable(inputToken, true, outputToken); + const destinationToken = { + chainId: CHAIN_IDs.MAINNET, + symbol: "USDC", // bridgeable + } as EnrichedToken; - expect(isUnreachable).toBe(true); - }); + const isUnreachable = isTokenUnreachable( + destinationToken, + false, + originToken + ); - it("should NOT restrict BRIDGEABLE output tokens to Solana", () => { - const inputToken = { - chainId: CHAIN_IDs.MAINNET, - symbol: "USDC", - } as EnrichedToken; + expect(isUnreachable).toBe(false); + }); + }); - const outputToken = { - chainId: CHAIN_IDs.SOLANA, - symbol: "USDC", // not bridgeable - } as EnrichedToken; + describe("when Solana is destination chain", () => { + it("should mark non-bridgeable destination tokens as unreachable", () => { + const originToken = { + chainId: CHAIN_IDs.MAINNET, + symbol: "USDC", + } as EnrichedToken; + + const destinationToken = { + chainId: CHAIN_IDs.SOLANA, + symbol: "ETH", // not bridgeable + } as EnrichedToken; + + // Selecting destination token (isOriginToken = false) + const isUnreachable = isTokenUnreachable( + destinationToken, + false, + originToken + ); + + expect(isUnreachable).toBe(true); + }); - const isUnreachable = isTokenUnreachable(inputToken, true, outputToken); + it("should NOT mark bridgeable destination tokens as unreachable", () => { + const originToken = { + chainId: CHAIN_IDs.MAINNET, + symbol: "USDC", + } as EnrichedToken; - expect(isUnreachable).toBe(false); + const destinationToken = { + chainId: CHAIN_IDs.SOLANA, + symbol: "USDC", // bridgeable + } as EnrichedToken; + + const isUnreachable = isTokenUnreachable( + destinationToken, + false, + originToken + ); + + expect(isUnreachable).toBe(false); + }); + }); }); describe("matchesGlob", () => { diff --git a/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.ts b/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.ts index c2aa8c0df..4d2db0058 100644 --- a/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.ts +++ b/src/views/SwapAndBridge/components/ChainTokenSelector/isTokenUnreachable.ts @@ -1,4 +1,9 @@ -import { CHAIN_IDs, INDIRECT_CHAINS } from "../../../../utils/constants"; +import { + CHAIN_IDs, + INDIRECT_CHAINS, + interchangeableTokensMap, +} from "../../../../utils/constants"; +import { solana } from "../../../../constants/chains/configs"; import { EnrichedToken } from "./ChainTokenSelectorModal"; export type RouteParams = { @@ -38,14 +43,6 @@ const RESTRICTED_ROUTES: RestrictedRoute[] = [ toChainId: [CHAIN_IDs.HYPERCORE], toSymbol: ["USDC-SPOT"], }, - - // only allow bridegable output to SOlana - { - fromChainId: "*", - fromSymbol: ["*"], - toChainId: [CHAIN_IDs.SOLANA], - toSymbol: ["!USDC"], - }, ]; // simple glob tester. supports only: ["*" , "!"] @@ -112,6 +109,45 @@ function getRestrictedOriginChainsUnreachable( ); } +/** + * Checks if a token should be marked unreachable when Solana is involved. + * When Solana is either origin or destination chain, only bridgeable tokens + * should be allowed as output tokens. + */ +function isNonBridgeableSvmTokenUnreachable( + token: EnrichedToken, + isOriginToken: boolean, + otherToken: EnrichedToken | null | undefined +): boolean { + // Only apply this check when selecting destination tokens (output tokens) + if (isOriginToken) return false; + + // Check if Solana is either origin or destination chain + const isSolanaOrigin = otherToken?.chainId === CHAIN_IDs.SOLANA; + const isSolanaDestination = token.chainId === CHAIN_IDs.SOLANA; + + // If Solana is not involved, don't mark as unreachable + if (!isSolanaOrigin && !isSolanaDestination) return false; + + // If Solana is involved, check if token is bridgeable + const bridgeableSvmTokenSymbols = [ + "USDC", + "USDH", + "USDH-SPOT", + "USDC-SPOT", + "USDT-SPOT", + ]; + + const isBridgeable = + bridgeableSvmTokenSymbols.includes(token.symbol) || + bridgeableSvmTokenSymbols.some((symbol) => + interchangeableTokensMap[token.symbol]?.includes(symbol) + ); + + // Mark as unreachable if not bridgeable + return !isBridgeable; +} + /** * Determines if a token is unreachable based on various criteria. * @@ -144,6 +180,15 @@ export function isTokenUnreachable( }) : false; + // Check if token should be unreachable due to Solana bridgeable token restrictions + const isNonBridgeableSvm = isNonBridgeableSvmTokenUnreachable( + token, + isOriginToken, + otherToken + ); + // Combine all unreachability checks - return isSameChain || isRestrictedOrigin || isRestrictedRoute; + return ( + isSameChain || isRestrictedOrigin || isRestrictedRoute || isNonBridgeableSvm + ); }