Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/cowswap-frontend/src/locales/es-ES.po
Original file line number Diff line number Diff line change
Expand Up @@ -6366,3 +6366,7 @@ msgstr "Ver todas las {totalChains} redes"
#: apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/MobileChainSelector.tsx
msgid "Selected network {activeChainLabel}"
msgstr "Red seleccionada {activeChainLabel}"

#: apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx
msgid "This destination is not supported for this source chain"
msgstr "Este destino no es compatible con esta red de origen"
4 changes: 4 additions & 0 deletions apps/cowswap-frontend/src/locales/ru-RU.po
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Сеть назначения не доступна для выбранной исходной сети"

Original file line number Diff line number Diff line change
Expand Up @@ -6366,3 +6366,7 @@ msgstr "Показать все сети ({totalChains})"
#: apps/cowswap-frontend/src/modules/tokensList/pure/SelectTokenModal/MobileChainSelector.tsx
msgid "Selected network {activeChainLabel}"
msgstr "Выбранная сеть {activeChainLabel}"

#: apps/cowswap-frontend/src/modules/tokensList/pure/ChainsSelector/index.tsx
msgid "This destination is not supported for this source chain"
msgstr "Этот пункт назначения недоступен для выбранной исходной сети"
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ describe('useChainsToSelect state builders', () => {
chainId: SupportedChainId.MAINNET,
currentChainInfo: createChainInfoForTests(SupportedChainId.MAINNET),
bridgeSupportedNetworks: bridgeChains,
areUnsupportedChainsEnabled: true,
isLoading: false,
})

Expand All @@ -98,7 +97,7 @@ describe('useChainsToSelect state builders', () => {
])
})

it('falls back to wallet chain when bridge does not support the source chain', () => {
it('returns all bridge destinations even when source chain is not supported by bridge', () => {
const state = createOutputChainsState({
selectedTargetChainId: SupportedChainId.BASE,
chainId: SupportedChainId.SEPOLIA,
Expand All @@ -107,12 +106,20 @@ describe('useChainsToSelect state builders', () => {
createChainInfoForTests(SupportedChainId.MAINNET),
createChainInfoForTests(SupportedChainId.ARBITRUM_ONE),
],
areUnsupportedChainsEnabled: true,
isLoading: false,
})

// Default to source chain when the selected target isn't available
expect(state.defaultChainId).toBe(SupportedChainId.SEPOLIA)
expect(state.chains?.map((chain) => chain.id)).toEqual([SupportedChainId.SEPOLIA])
// Should show all destinations plus source, but destinations disabled when source not supported
expect(state.chains?.map((chain) => chain.id)).toEqual([
SupportedChainId.MAINNET,
SupportedChainId.ARBITRUM_ONE,
SupportedChainId.SEPOLIA,
])
expect(state.disabledChainIds?.has(SupportedChainId.MAINNET)).toBe(true)
expect(state.disabledChainIds?.has(SupportedChainId.ARBITRUM_ONE)).toBe(true)
expect(state.disabledChainIds?.has(SupportedChainId.SEPOLIA)).toBe(false)
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useMemo } from 'react'

import { CHAIN_INFO } from '@cowprotocol/common-const'
import { useAvailableChains, useFeatureFlags, useIsBridgingEnabled } from '@cowprotocol/common-hooks'
import { useAvailableChains, useIsBridgingEnabled } from '@cowprotocol/common-hooks'
import { ChainInfo, SupportedChainId } from '@cowprotocol/cow-sdk'
import { useWalletInfo } from '@cowprotocol/wallet'

Expand All @@ -27,7 +27,6 @@ export function useChainsToSelect(): ChainsToSelectState | undefined {
const { chainId } = useWalletInfo()
const { field, selectedTargetChainId = chainId, tradeType } = useSelectTokenWidgetState()
const { data: bridgeSupportedNetworks, isLoading } = useBridgeSupportedNetworks()
const { areUnsupportedChainsEnabled } = useFeatureFlags()
const isBridgingEnabled = useIsBridgingEnabled()
const availableChains = useAvailableChains()
const isAdvancedTradeType = tradeType === TradeType.LIMIT_ORDER || tradeType === TradeType.ADVANCED_ORDERS
Expand All @@ -44,7 +43,8 @@ export function useChainsToSelect(): ChainsToSelectState | undefined {
}, [] as ChainInfo[])
}, [availableChains])

return useMemo(() => {
// Compute output chains state for BUY token selection
const outputChainsState = useMemo(() => {
if (!field || !chainId) return undefined

const chainInfo = CHAIN_INFO[chainId]
Expand All @@ -56,20 +56,19 @@ export function useChainsToSelect(): ChainsToSelectState | undefined {
return undefined
}

const currentChainInfo = mapChainInfo(chainId, chainInfo)

if (!isBridgingEnabled) return undefined

if (field === Field.INPUT) {
return createInputChainsState(selectedTargetChainId, supportedChains)
return undefined
}

const currentChainInfo = mapChainInfo(chainId, chainInfo)

return createOutputChainsState({
selectedTargetChainId,
chainId,
currentChainInfo,
bridgeSupportedNetworks,
areUnsupportedChainsEnabled,
isLoading,
})
}, [
Expand All @@ -79,36 +78,47 @@ export function useChainsToSelect(): ChainsToSelectState | undefined {
bridgeSupportedNetworks,
isLoading,
isBridgingEnabled,
areUnsupportedChainsEnabled,
supportedChains,
isAdvancedTradeType,
])
}

function filterDestinationChains(
bridgeSupportedNetworks: ChainInfo[] | undefined,
areUnsupportedChainsEnabled: boolean | undefined,
): ChainInfo[] | undefined {
if (areUnsupportedChainsEnabled) {
// Nothing to filter, we return all bridge supported networks
return bridgeSupportedNetworks
} else {
// If unsupported chains are not enabled, we only return the supported networks
return bridgeSupportedNetworks?.filter((chain) => chain.id in SupportedChainId)
}
return useMemo(() => {
if (!field || !chainId) return undefined

const chainInfo = CHAIN_INFO[chainId]
if (!chainInfo) return undefined

// Limit/TWAP orders don't support chain selection - return undefined for both SELL and BUY
// These trade types rely on wallet/header network switcher instead
if (isAdvancedTradeType) {
return undefined
}

if (!isBridgingEnabled) return undefined

if (field === Field.INPUT) {
return createInputChainsState(selectedTargetChainId, supportedChains)
}

// For BUY token selection, include disabled chains info
if (outputChainsState) {
return outputChainsState
}

return undefined
}, [
field,
selectedTargetChainId,
chainId,
isBridgingEnabled,
supportedChains,
isAdvancedTradeType,
outputChainsState,
])
}

// Represents the “non-bridge” UX (bridging disabled or advanced-trade guardrail) where only the current chain is available.
function createSingleChainState(
defaultChainId: SupportedChainId | number,
chain: ChainInfo,
isLoading = false,
): ChainsToSelectState {
return {
defaultChainId,
chains: [chain],
isLoading,
}
function filterDestinationChains(bridgeSupportedNetworks: ChainInfo[] | undefined): ChainInfo[] | undefined {
// Show only chains the app supports.
return bridgeSupportedNetworks?.filter((chain) => chain.id in SupportedChainId)
}

// Sell-side selector intentionally limits chains to the wallet-supported list; bridge destinations never appear here.
Expand All @@ -128,7 +138,6 @@ interface CreateOutputChainsOptions {
chainId: SupportedChainId
currentChainInfo: ChainInfo
bridgeSupportedNetworks: ChainInfo[] | undefined
areUnsupportedChainsEnabled: boolean | undefined
isLoading: boolean
}

Expand All @@ -137,24 +146,31 @@ export function createOutputChainsState({
chainId,
currentChainInfo,
bridgeSupportedNetworks,
areUnsupportedChainsEnabled,
isLoading,
}: CreateOutputChainsOptions): ChainsToSelectState {
const destinationChains = filterDestinationChains(bridgeSupportedNetworks, areUnsupportedChainsEnabled) ?? []
const orderedDestinationChains = sortChainsByDisplayOrder(destinationChains)
const isSourceChainSupportedByBridge = Boolean(
bridgeSupportedNetworks?.some((bridgeChain) => bridgeChain.id === chainId),
)

if (!isSourceChainSupportedByBridge) {
// Source chain is unsupported by the bridge provider; fall back to non-bridge behavior.
return createSingleChainState(chainId, currentChainInfo)
}

const destinationChains = filterDestinationChains(bridgeSupportedNetworks) ?? []
const isSourceChainSupportedByBridge = Boolean(destinationChains.some((chain) => chain.id === chainId))

// Always include the current chain for same-chain swaps (no bridging required)
const chainSet = new Set(destinationChains.map((chain) => chain.id))
const chainsWithCurrent = chainSet.has(chainId) ? destinationChains : [currentChainInfo, ...destinationChains]

const orderedDestinationChains = sortChainsByDisplayOrder(chainsWithCurrent)
const hasSelectedTarget = orderedDestinationChains.some((chain) => chain.id === selectedTargetChainId)
const fallbackChainId =
orderedDestinationChains.find((chain) => chain.id === chainId)?.id ?? orderedDestinationChains[0]?.id
const resolvedDefaultChainId = hasSelectedTarget ? selectedTargetChainId : fallbackChainId
const disabledChainIds = isSourceChainSupportedByBridge
? undefined
: new Set<number>(destinationChains.map((c) => c.id))

// Always return bridgeSupportedNetworks (filtered by feature flag) for BUY,
// even if the source lacks bridge support, so all destinations show (disabled when unsupported).
// Current chain is always included for same-chain swaps.
return {
defaultChainId: selectedTargetChainId,
// Bridge supports this chain, so expose the provider-supplied destinations.
defaultChainId: resolvedDefaultChainId,
chains: orderedDestinationChains,
isLoading,
disabledChainIds,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function ChainPanel({
chains={filteredChains}
defaultChainId={chainsState?.defaultChainId}
onSelectChain={onSelectChain}
disabledChainIds={chainsState?.disabledChainIds}
/>
{showUnavailableState && <styledEl.EmptyState>{t`No networks available for this trade.`}</styledEl.EmptyState>}
{showSearchEmptyState && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { useTheme } from '@cowprotocol/common-hooks'
import { ChainInfo, SupportedChainId } from '@cowprotocol/cow-sdk'
import { getChainAccentColors } from '@cowprotocol/ui'

import { msg } from '@lingui/core/macro'
import { useLingui } from '@lingui/react/macro'
import SVG from 'react-inlinesvg'

import * as styledEl from './styled'
Expand All @@ -19,17 +21,30 @@ export interface ChainsSelectorProps {
onSelectChain: (chainId: ChainInfo) => void
defaultChainId?: ChainInfo['id']
isLoading: boolean
disabledChainIds?: Set<number>
}

export function ChainsSelector({ chains, onSelectChain, defaultChainId, isLoading }: ChainsSelectorProps): ReactNode {
export function ChainsSelector({
chains,
onSelectChain,
defaultChainId,
isLoading,
disabledChainIds,
}: ChainsSelectorProps): ReactNode {
const { darkMode } = useTheme()

if (isLoading) {
return <ChainsLoadingList />
}

return (
<ChainsList chains={chains} defaultChainId={defaultChainId} onSelectChain={onSelectChain} isDarkMode={darkMode} />
<ChainsList
chains={chains}
defaultChainId={defaultChainId}
onSelectChain={onSelectChain}
isDarkMode={darkMode}
disabledChainIds={disabledChainIds}
/>
)
}

Expand Down Expand Up @@ -59,22 +74,36 @@ interface ChainsListProps {
defaultChainId?: ChainInfo['id']
onSelectChain(chain: ChainInfo): void
isDarkMode: boolean
disabledChainIds?: Set<number>
}

function ChainsList({ chains, defaultChainId, onSelectChain, isDarkMode }: ChainsListProps): ReactNode {
function ChainsList({
chains,
defaultChainId,
onSelectChain,
isDarkMode,
disabledChainIds,
}: ChainsListProps): ReactNode {
return (
<styledEl.List>
<ChainsButtonsList
chains={chains}
defaultChainId={defaultChainId}
onSelectChain={onSelectChain}
isDarkMode={isDarkMode}
disabledChainIds={disabledChainIds}
/>
</styledEl.List>
)
}

function ChainsButtonsList({ chains, defaultChainId, onSelectChain, isDarkMode }: ChainsListProps): ReactNode {
function ChainsButtonsList({
chains,
defaultChainId,
onSelectChain,
isDarkMode,
disabledChainIds,
}: ChainsListProps): ReactNode {
return (
<>
{chains.map((chain) => (
Expand All @@ -84,6 +113,7 @@ function ChainsButtonsList({ chains, defaultChainId, onSelectChain, isDarkMode }
isActive={defaultChainId === chain.id}
onSelectChain={onSelectChain}
isDarkMode={isDarkMode}
isDisabled={disabledChainIds?.has(chain.id) ?? false}
/>
))}
</>
Expand All @@ -108,24 +138,36 @@ interface ChainButtonProps {
isActive: boolean
isDarkMode: boolean
onSelectChain(chain: ChainInfo): void
isDisabled: boolean
}

function ChainButton({ chain, isActive, isDarkMode, onSelectChain }: ChainButtonProps): ReactNode {
function ChainButton({ chain, isActive, isDarkMode, onSelectChain, isDisabled }: ChainButtonProps): ReactNode {
const { i18n } = useLingui()
const logoSrc = isDarkMode ? chain.logo.dark : chain.logo.light
const accent = getChainAccent(chain.id)
const disabledTooltip = i18n._(msg`This destination is not supported for this source chain`)

const handleClick = (): void => {
if (!isDisabled) {
onSelectChain(chain)
}
}

return (
<styledEl.ChainButton
onClick={() => onSelectChain(chain)}
onClick={handleClick}
active$={isActive}
accent$={accent}
aria-pressed={isActive}
aria-disabled={isDisabled}
disabled$={isDisabled}
title={isDisabled ? disabledTooltip : undefined}
>
<styledEl.ChainInfo>
<styledEl.ChainLogo>
<img src={logoSrc} alt={chain.label} loading="lazy" />
</styledEl.ChainLogo>
<styledEl.ChainText>{chain.label}</styledEl.ChainText>
<styledEl.ChainText disabled$={isDisabled}>{chain.label}</styledEl.ChainText>
</styledEl.ChainInfo>
{isActive && (
<styledEl.ActiveIcon aria-hidden="true" accent$={accent} color$={chain.color}>
Expand Down
Loading