From 9efe13af427d680245cbd8fcaf975562e52ab5a0 Mon Sep 17 00:00:00 2001 From: Kyle Leighton Date: Wed, 10 Sep 2025 11:56:42 -0400 Subject: [PATCH 1/4] fix: update SOL/WSOL display logic and block native-wrapped same-chain swaps --- src/config/mainnet/tokens.ts | 2 +- src/routes/sdkv2/route.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) 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/routes/sdkv2/route.ts b/src/routes/sdkv2/route.ts index 3a0faf243..5bf524846 100644 --- a/src/routes/sdkv2/route.ts +++ b/src/routes/sdkv2/route.ts @@ -11,6 +11,7 @@ import { routes, chainToPlatform, isSameToken, + isNative, TransferState, } from '@wormhole-foundation/sdk'; import type { Token } from 'config/tokens'; @@ -148,6 +149,17 @@ export class SDKv2Route { routeSupportedTokenFetcher, ); + // Pre-compute wrapped native address when filtering same-chain swaps + let wrappedNativeAddr: string | undefined = undefined; + if (isSameChain) { + try { + const tb = await toContext.context.getTokenBridge(); + wrappedNativeAddr = tb.getWrappedNative().toString().toLowerCase(); + } catch { + console.log('Unable to fetch wrapped native token'); + } + } + const filteredTokens = destTokens.filter((t) => { const token = config.tokens.get(t); if (token && isFrankensteinToken(token, toContext.chain)) { @@ -158,6 +170,24 @@ export class SDKv2Route { return false; } + // Block same-chain swaps between native gas token and its wrapped native token + if (isSameChain && wrappedNativeAddr) { + const srcIsNative = sourceToken.isNativeGasToken; + const srcIsWrappedNative = + sourceToken.address.toString().toLowerCase() === wrappedNativeAddr; + + const destIsWrappedNative = + t.address.toString().toLowerCase() === wrappedNativeAddr; + const destIsNative = isNative(t.address); + + if ( + (srcIsNative && destIsWrappedNative) || + (srcIsWrappedNative && destIsNative) + ) { + return false; + } + } + return true; }); From 04df172e2c06460c46e343617b7883810dc6ee6f Mon Sep 17 00:00:00 2001 From: Kyle Leighton Date: Wed, 10 Sep 2025 15:00:44 -0400 Subject: [PATCH 2/4] chore: clean up section header logic --- src/views/v3/Bridge/AssetPicker/TokenList.tsx | 23 ++++++--------- .../Bridge/AssetPicker/TokenSectionHeader.tsx | 28 +------------------ 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/src/views/v3/Bridge/AssetPicker/TokenList.tsx b/src/views/v3/Bridge/AssetPicker/TokenList.tsx index b3eb84c68..aa0f7cf7f 100644 --- a/src/views/v3/Bridge/AssetPicker/TokenList.tsx +++ b/src/views/v3/Bridge/AssetPicker/TokenList.tsx @@ -78,15 +78,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 +138,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 ( Date: Thu, 11 Sep 2025 09:26:12 -0400 Subject: [PATCH 3/4] chore: minor name and function cleanup --- src/hooks/useFetchSupportedRoutes.ts | 1 - src/routes/operator.ts | 3 +-- src/routes/sdkv2/route.ts | 15 +++++++-------- 3 files changed, 8 insertions(+), 11 deletions(-) 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/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 5bf524846..d0b269072 100644 --- a/src/routes/sdkv2/route.ts +++ b/src/routes/sdkv2/route.ts @@ -62,7 +62,6 @@ export class SDKv2Route { } async isRouteSupported( - name: string, sourceToken: Token, destToken: Token, fromChain: Chain, @@ -89,7 +88,6 @@ export class SDKv2Route { try { const supportedDestinationTokens = await this.supportedDestTokens( - name, sourceToken, fromChain, toChain, @@ -109,7 +107,6 @@ export class SDKv2Route { } async supportedDestTokens( - routeName: string, sourceToken: Token | undefined, fromChain?: Chain | undefined, toChain?: Chain | undefined, @@ -154,9 +151,11 @@ export class SDKv2Route { if (isSameChain) { try { const tb = await toContext.context.getTokenBridge(); - wrappedNativeAddr = tb.getWrappedNative().toString().toLowerCase(); + wrappedNativeAddr = (await tb.getWrappedNative()) + .toString() + .toLowerCase(); } catch { - console.log('Unable to fetch wrapped native token'); + // No-op } } @@ -172,8 +171,8 @@ export class SDKv2Route { // Block same-chain swaps between native gas token and its wrapped native token if (isSameChain && wrappedNativeAddr) { - const srcIsNative = sourceToken.isNativeGasToken; - const srcIsWrappedNative = + const srcIsNative = isNative(sourceToken.address); + const srcIsWrapped = sourceToken.address.toString().toLowerCase() === wrappedNativeAddr; const destIsWrappedNative = @@ -182,7 +181,7 @@ export class SDKv2Route { if ( (srcIsNative && destIsWrappedNative) || - (srcIsWrappedNative && destIsNative) + (srcIsWrapped && destIsNative) ) { return false; } From b22ad8f59dc8bd084ad9f037c48a1782dc57aeec Mon Sep 17 00:00:00 2001 From: Kyle Leighton Date: Thu, 11 Sep 2025 10:45:22 -0400 Subject: [PATCH 4/4] chore: prevent native and wrapped token swaps on same chain --- src/hooks/useTokenListWithSearch.ts | 29 ++++++---- src/routes/sdkv2/route.ts | 43 ++++---------- src/utils/sdkv2.ts | 5 +- src/utils/wrappedNativeTokens.ts | 58 +++++++++++++++++++ src/views/v2/Bridge/AssetPicker/TokenList.tsx | 2 - src/views/v3/Bridge/AssetPicker/TokenList.tsx | 2 - 6 files changed, 90 insertions(+), 49 deletions(-) create mode 100644 src/utils/wrappedNativeTokens.ts 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/sdkv2/route.ts b/src/routes/sdkv2/route.ts index d0b269072..25c465733 100644 --- a/src/routes/sdkv2/route.ts +++ b/src/routes/sdkv2/route.ts @@ -11,7 +11,6 @@ import { routes, chainToPlatform, isSameToken, - isNative, TransferState, } from '@wormhole-foundation/sdk'; import type { Token } from 'config/tokens'; @@ -22,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; @@ -146,18 +149,10 @@ export class SDKv2Route { routeSupportedTokenFetcher, ); - // Pre-compute wrapped native address when filtering same-chain swaps - let wrappedNativeAddr: string | undefined = undefined; - if (isSameChain) { - try { - const tb = await toContext.context.getTokenBridge(); - wrappedNativeAddr = (await tb.getWrappedNative()) - .toString() - .toLowerCase(); - } catch { - // No-op - } - } + // 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); @@ -165,26 +160,8 @@ export class SDKv2Route { return false; } - if (isSameChain && token?.address === sourceToken.address) { - return false; - } - - // Block same-chain swaps between native gas token and its wrapped native token - if (isSameChain && wrappedNativeAddr) { - const srcIsNative = isNative(sourceToken.address); - const srcIsWrapped = - sourceToken.address.toString().toLowerCase() === wrappedNativeAddr; - - const destIsWrappedNative = - t.address.toString().toLowerCase() === wrappedNativeAddr; - const destIsNative = isNative(t.address); - - if ( - (srcIsNative && destIsWrappedNative) || - (srcIsWrapped && destIsNative) - ) { - 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 aa0f7cf7f..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, });