diff --git a/src/config/mainnet/tokens.ts b/src/config/mainnet/tokens.ts index b3a24f500..8db99b653 100644 --- a/src/config/mainnet/tokens.ts +++ b/src/config/mainnet/tokens.ts @@ -235,7 +235,7 @@ export const MAINNET_TOKENS: TokenConfig[] = [ tokenId: { chain: 'Solana', address: 'native' }, }, { - symbol: 'WSOL', + symbol: 'SOL', decimals: 9, tokenId: { chain: 'Solana', diff --git a/src/hooks/useFetchSupportedRoutes.ts b/src/hooks/useFetchSupportedRoutes.ts index ed2a80f75..6c141bf78 100644 --- a/src/hooks/useFetchSupportedRoutes.ts +++ b/src/hooks/useFetchSupportedRoutes.ts @@ -58,7 +58,6 @@ const useFetchSupportedRoutes = ({ try { supported = await route.isRouteSupported( - name, sourceToken, destToken, fromChain, diff --git a/src/hooks/useTokenListWithSearch.ts b/src/hooks/useTokenListWithSearch.ts index 15cf60fc4..327bb1842 100644 --- a/src/hooks/useTokenListWithSearch.ts +++ b/src/hooks/useTokenListWithSearch.ts @@ -12,7 +12,10 @@ import type { Token } from 'config/tokens'; import config from 'config'; import { useTokens } from 'contexts/TokensContext'; import { getTokenSymbol } from 'utils'; -import type { Balances } from 'utils/wallet/types'; +import { + getWrappedNativeToken, + shouldFilterSameChainToken, +} from 'utils/wrappedNativeTokens'; import { unionBy } from 'es-toolkit'; interface UseTokenListWithSearchParams { @@ -22,8 +25,6 @@ interface UseTokenListWithSearchParams { isSource: boolean; isSameChainSwap: boolean; sourceToken?: Token; - balances: Balances; - walletAddress: string; tokenPastingEnabled?: boolean; } @@ -46,8 +47,6 @@ export const useTokenListWithSearch = ({ isSource, isSameChainSwap, sourceToken, - balances, - walletAddress, tokenPastingEnabled = true, }: UseTokenListWithSearchParams): UseTokenListWithSearchReturn => { const [searchedTokens, setSearchedTokens] = useState([]); @@ -60,6 +59,15 @@ export const useTokenListWithSearch = ({ setSearchedTokens((prev) => [...prev, token]); }, []); + // Get wrapped native address for same-chain swaps (only for destination selection) + const wrappedNativeAddr = useMemo(() => { + if (!isSameChainSwap || !chain || isSource) { + return undefined; + } + const wrapped = getWrappedNativeToken(config.network, chain); + return wrapped?.toLowerCase(); + }, [isSameChainSwap, chain, isSource]); + useEffect(() => { if (!chain || !tokenPastingEnabled || !deferredSearch) { setSearchedTokens([]); @@ -139,10 +147,10 @@ export const useTokenListWithSearch = ({ }); } - // For destination token list in same-chain swaps, filter out the source token - if (!isSource && isSameChainSwap && sourceToken) { + // For destination token list in same-chain swaps, filter out invalid options + if (sourceToken && isSameChainSwap && !isSource) { tokens = tokens.filter( - (t) => t.addressString !== sourceToken.addressString, + (t) => !shouldFilterSameChainToken(sourceToken, wrappedNativeAddr, t), ); } @@ -152,9 +160,10 @@ export const useTokenListWithSearch = ({ searchedTokens, deferredSearch, searchLower, - isSource, - isSameChainSwap, sourceToken, + isSameChainSwap, + isSource, + wrappedNativeAddr, ]); const tokenPrices = useMemo( diff --git a/src/routes/operator.ts b/src/routes/operator.ts index 3e9925a69..d16296198 100644 --- a/src/routes/operator.ts +++ b/src/routes/operator.ts @@ -146,10 +146,9 @@ export default class RouteOperator { ): Promise { const supported: Set = new Set(); - await this.forEach(async (name, route) => { + await this.forEach(async (_, route) => { try { const destTokenIds = await route.supportedDestTokens( - name, sourceToken, sourceChain, destChain, diff --git a/src/routes/sdkv2/route.ts b/src/routes/sdkv2/route.ts index 3a0faf243..25c465733 100644 --- a/src/routes/sdkv2/route.ts +++ b/src/routes/sdkv2/route.ts @@ -21,6 +21,10 @@ import config, { getWormholeContextV2 } from 'config'; import { sleep } from 'utils'; import { isFrankensteinToken } from 'utils'; import { isNttToken } from 'utils/ntt'; +import { + getWrappedNativeToken, + shouldFilterSameChainToken, +} from 'utils/wrappedNativeTokens'; type Amount = sdkAmount.Amount; @@ -61,7 +65,6 @@ export class SDKv2Route { } async isRouteSupported( - name: string, sourceToken: Token, destToken: Token, fromChain: Chain, @@ -88,7 +91,6 @@ export class SDKv2Route { try { const supportedDestinationTokens = await this.supportedDestTokens( - name, sourceToken, fromChain, toChain, @@ -108,7 +110,6 @@ export class SDKv2Route { } async supportedDestTokens( - routeName: string, sourceToken: Token | undefined, fromChain?: Chain | undefined, toChain?: Chain | undefined, @@ -148,14 +149,19 @@ export class SDKv2Route { routeSupportedTokenFetcher, ); + // Get wrapped native address for same-chain swaps + const wrappedNativeAddr = isSameChain + ? getWrappedNativeToken(config.network, toContext.chain)?.toLowerCase() + : undefined; + const filteredTokens = destTokens.filter((t) => { const token = config.tokens.get(t); if (token && isFrankensteinToken(token, toContext.chain)) { return false; } - if (isSameChain && token?.address === sourceToken.address) { - return false; + if (isSameChain) { + return !shouldFilterSameChainToken(sourceToken, wrappedNativeAddr, t); } return true; diff --git a/src/utils/sdkv2.ts b/src/utils/sdkv2.ts index 9fab26d82..baa4753c9 100644 --- a/src/utils/sdkv2.ts +++ b/src/utils/sdkv2.ts @@ -21,6 +21,7 @@ import { TBTCBridge, chainToPlatform, } from '@wormhole-foundation/sdk'; +import { getWrappedNativeToken } from './wrappedNativeTokens'; import type { NttRoute } from '@wormhole-foundation/sdk-route-ntt'; import type { CCTPv2ExecutorRoute } from '@wormhole-labs/cctp-executor-route'; import { Connection } from '@solana/web3.js'; @@ -559,10 +560,10 @@ const getTokenBridgeToken = async ( address: token.address, }); - const wrappedNative = await tb.getWrappedNative(); + const wrappedNative = getWrappedNativeToken(config.network, chain); const tokenId = - wrappedNative.toString() === tokenAddress.toString() + wrappedNative && wrappedNative === tokenAddress.toString() ? nativeTokenId(chain) : Wormhole.tokenId(chain, tokenAddress.toString()); diff --git a/src/utils/wrappedNativeTokens.ts b/src/utils/wrappedNativeTokens.ts new file mode 100644 index 000000000..f38556cf0 --- /dev/null +++ b/src/utils/wrappedNativeTokens.ts @@ -0,0 +1,58 @@ +import type { Chain, Network, TokenAddress } from '@wormhole-foundation/sdk'; +import { chainToPlatform, isNative } from '@wormhole-foundation/sdk'; +import { WETH_CONTRACTS } from '@wormhole-foundation/sdk-evm'; + +const WSOL_ADDRESS = 'So11111111111111111111111111111111111111112'; + +export function getWrappedNativeToken( + network: Network, + chain: Chain, +): string | undefined { + const platform = chainToPlatform(chain); + + if (platform === 'Evm') { + return WETH_CONTRACTS[network]?.[chain]; + } + + if (platform === 'Solana') { + return WSOL_ADDRESS; + } + + return undefined; +} + +/** + * Determines if a token should be filtered out in same-chain swaps + * based on native/wrapped token pair restrictions + */ +export function shouldFilterSameChainToken( + sourceToken: { address: TokenAddress } | null, + wrappedNativeAddr: string | undefined, + currentToken: { address: TokenAddress }, +): boolean { + if (!sourceToken) { + return false; + } + + const sourceAddr = sourceToken.address.toString(); + const currentAddr = currentToken.address.toString(); + + if (currentAddr === sourceAddr) { + return true; + } + + // Block native-wrapped token pairs if we have wrapped native address + if (wrappedNativeAddr) { + const srcIsNative = isNative(sourceToken.address); + const srcIsWrapped = sourceAddr.toLowerCase() === wrappedNativeAddr; + + const destIsWrapped = currentAddr.toLowerCase() === wrappedNativeAddr; + const destIsNative = isNative(currentToken.address); + + if ((srcIsNative && destIsWrapped) || (srcIsWrapped && destIsNative)) { + return true; + } + } + + return false; +} diff --git a/src/views/v2/Bridge/AssetPicker/TokenList.tsx b/src/views/v2/Bridge/AssetPicker/TokenList.tsx index 4b3c11e75..bbd342e50 100644 --- a/src/views/v2/Bridge/AssetPicker/TokenList.tsx +++ b/src/views/v2/Bridge/AssetPicker/TokenList.tsx @@ -42,8 +42,6 @@ const TokenList = (props: Props) => { isSource: props.isSource, isSameChainSwap: props.isSameChainSwap, sourceToken: props.sourceToken, - balances: props.balances, - walletAddress: props.wallet.address, tokenPastingEnabled: tokenPastingIsEnabled, }); diff --git a/src/views/v3/Bridge/AssetPicker/TokenList.tsx b/src/views/v3/Bridge/AssetPicker/TokenList.tsx index b3eb84c68..b59a356c2 100644 --- a/src/views/v3/Bridge/AssetPicker/TokenList.tsx +++ b/src/views/v3/Bridge/AssetPicker/TokenList.tsx @@ -42,8 +42,6 @@ const TokenList = (props: Props) => { isSource: props.isSource, isSameChainSwap: props.isSameChainSwap, sourceToken: props.sourceToken, - balances: props.balances, - walletAddress: props.wallet.address, tokenPastingEnabled: tokenPastingIsEnabled, }); @@ -78,15 +76,6 @@ const TokenList = (props: Props) => { tokenListContainer: { padding: '16px 0 0 0 !important', }, - title: { - fontSize: 14, - marginBottom: '8px', - }, - tokenLoaderRow: { - display: 'flex', - justifyContent: 'space-between', - padding: '8px 16px', - }, tokenList: { maxHeight: '360px', [theme.breakpoints.down('sm')]: { @@ -147,15 +136,17 @@ const TokenList = (props: Props) => { const isRestSection = isGroupingEnabled && isWalletConnected && index >= ownedCount; + const headerLabel = (() => { + if (!isGroupingEnabled) return null; + if (index === 0 && ownedCount > 0) return 'Your tokens'; + if (index === ownedCount) return 'All tokens'; + return null; + })(); + return ( - + {headerLabel && } props.onSelectToken(token)} diff --git a/src/views/v3/Bridge/AssetPicker/TokenSectionHeader.tsx b/src/views/v3/Bridge/AssetPicker/TokenSectionHeader.tsx index 918efd3be..9e5918075 100644 --- a/src/views/v3/Bridge/AssetPicker/TokenSectionHeader.tsx +++ b/src/views/v3/Bridge/AssetPicker/TokenSectionHeader.tsx @@ -2,35 +2,9 @@ import { Box, useTheme } from '@mui/material'; import { Typography } from '@mui/material'; import React from 'react'; -const TokenSectionHeader = ({ - index, - ownedCount, - isGroupingEnabled, -}: { - index: number; - ownedCount: number; - isGroupingEnabled: boolean; -}) => { +const TokenSectionHeader = ({ label }: { label: string }) => { const theme = useTheme(); - if (!isGroupingEnabled) { - return null; - } - - let label: string | null = null; - - if (index === 0 && ownedCount > 0) { - label = 'Your tokens'; - } - - if (index === ownedCount) { - label = 'All tokens'; - } - - if (!label) { - return null; - } - return (