From a18c8172e9cfeca9e3987979dbb8ee772c5cc823 Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:43:40 +1100 Subject: [PATCH] fix: prevent selecting chain if asset would become unsupported (#8502) * fix: prevent selecting chain if asset would become unsupported * fix: linting --- .../AssetSelection/AssetSelection.tsx | 5 +++++ .../AssetChainDropdown/AssetChainDropdown.tsx | 8 ++++++-- .../TradeAssetSearch/TradeAssetSearchModal.tsx | 6 +++--- .../components/LimitOrderBuyAsset.tsx | 13 ++++++++++--- .../LimitOrder/components/LimitOrderInput.tsx | 14 ++++++++------ .../SharedTradeInput/SharedTradeInputBody.tsx | 13 ++++++++++--- .../components/TradeInput/TradeInput.tsx | 17 +++++++++++++---- .../TradeAssetSearch/TradeAssetSearch.tsx | 14 +++++++------- .../components/SearchTermAssetList.tsx | 6 +++--- 9 files changed, 65 insertions(+), 31 deletions(-) diff --git a/src/components/AssetSelection/AssetSelection.tsx b/src/components/AssetSelection/AssetSelection.tsx index 00ab1a0ae23..a32a7a1b8ed 100644 --- a/src/components/AssetSelection/AssetSelection.tsx +++ b/src/components/AssetSelection/AssetSelection.tsx @@ -17,6 +17,7 @@ type TradeAssetSelectBaseProps = { isLoading?: boolean buttonProps?: ButtonProps onlyConnectedChains: boolean + assetFilterPredicate?: (assetId: AssetId) => boolean chainIdFilterPredicate?: (chainId: ChainId) => boolean } & FlexProps @@ -44,6 +45,7 @@ export const TradeAssetSelect: React.FC = memo(props => { isLoading, onlyConnectedChains, buttonProps, + assetFilterPredicate, chainIdFilterPredicate, flexProps, } = useMemo(() => { @@ -56,6 +58,7 @@ export const TradeAssetSelect: React.FC = memo(props => { isLoading, onlyConnectedChains, buttonProps, + assetFilterPredicate, chainIdFilterPredicate, ...flexProps } = props @@ -68,6 +71,7 @@ export const TradeAssetSelect: React.FC = memo(props => { isLoading, onlyConnectedChains, buttonProps, + assetFilterPredicate, chainIdFilterPredicate, flexProps, } @@ -109,6 +113,7 @@ export const TradeAssetSelect: React.FC = memo(props => { rightIcon={rightIcon} onlyConnectedChains={onlyConnectedChains} chainIdFilterPredicate={chainIdFilterPredicate} + assetFilterPredicate={assetFilterPredicate} /> ) diff --git a/src/components/AssetSelection/components/AssetChainDropdown/AssetChainDropdown.tsx b/src/components/AssetSelection/components/AssetChainDropdown/AssetChainDropdown.tsx index cd1124a354e..25958cd89f9 100644 --- a/src/components/AssetSelection/components/AssetChainDropdown/AssetChainDropdown.tsx +++ b/src/components/AssetSelection/components/AssetChainDropdown/AssetChainDropdown.tsx @@ -31,6 +31,7 @@ type AssetChainDropdownProps = { isError?: boolean onlyConnectedChains: boolean isDisabled?: boolean + assetFilterPredicate?: (assetId: AssetId) => boolean chainIdFilterPredicate?: (chainId: ChainId) => boolean } @@ -48,6 +49,7 @@ export const AssetChainDropdown: React.FC = memo( isLoading, onChangeAsset, onlyConnectedChains, + assetFilterPredicate, chainIdFilterPredicate, }) => { const { @@ -68,11 +70,13 @@ export const AssetChainDropdown: React.FC = memo( const filteredRelatedAssetIds = useMemo(() => { const filteredRelatedAssetIds = relatedAssetIds.filter(assetId => { const { chainId } = fromAssetId(assetId) - return chainIdFilterPredicate?.(chainId) ?? true + const isChainAllowed = chainIdFilterPredicate?.(chainId) ?? true + const isAssetAllowed = assetFilterPredicate?.(assetId) ?? true + return isChainAllowed && isAssetAllowed }) if (!assetIds?.length) return filteredRelatedAssetIds return filteredRelatedAssetIds.filter(relatedAssetId => assetIds.includes(relatedAssetId)) - }, [assetIds, chainIdFilterPredicate, relatedAssetIds]) + }, [assetFilterPredicate, assetIds, chainIdFilterPredicate, relatedAssetIds]) const renderedChains = useMemo(() => { if (!assetId) return null diff --git a/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx b/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx index 295ac0d9203..108660e4ce0 100644 --- a/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx +++ b/src/components/Modals/TradeAssetSearch/TradeAssetSearchModal.tsx @@ -1,4 +1,4 @@ -import type { ChainId } from '@shapeshiftoss/caip' +import type { AssetId, ChainId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' import type { FC } from 'react' import { memo, useCallback } from 'react' @@ -17,14 +17,14 @@ import { useModal } from 'hooks/useModal/useModal' export type TradeAssetSearchModalProps = TradeAssetSearchProps & { title?: string onAssetClick: Required['onAssetClick'] - assetFilterPredicate: (asset: Asset) => boolean + assetFilterPredicate: (assetId: AssetId) => boolean chainIdFilterPredicate: (chainId: ChainId) => boolean } type AssetSearchModalBaseProps = TradeAssetSearchModalProps & { isOpen: boolean close: () => void - assetFilterPredicate: (asset: Asset) => boolean + assetFilterPredicate: (assetId: AssetId) => boolean chainIdFilterPredicate: (chainId: ChainId) => boolean } diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderBuyAsset.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderBuyAsset.tsx index 181da0ff393..7720992d9da 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderBuyAsset.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderBuyAsset.tsx @@ -1,4 +1,4 @@ -import type { AccountId, ChainId } from '@shapeshiftoss/caip' +import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' import React, { memo, useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' @@ -32,7 +32,7 @@ export type LimitOrderBuyAssetProps = { asset: Asset accountId?: AccountId isInputtingFiatSellAmount: boolean - assetFilterPredicate: (asset: Asset) => boolean + assetFilterPredicate: (assetId: AssetId) => boolean chainIdFilterPredicate: (chainId: ChainId) => boolean onAccountIdChange: AccountDropdownProps['onChange'] onSetBuyAsset: (asset: Asset) => void @@ -73,10 +73,17 @@ export const LimitOrderBuyAsset: React.FC = memo( onAssetClick={handleAssetClick} onAssetChange={onSetBuyAsset} onlyConnectedChains={false} + assetFilterPredicate={assetFilterPredicate} chainIdFilterPredicate={chainIdFilterPredicate} /> ), - [asset.assetId, handleAssetClick, onSetBuyAsset, chainIdFilterPredicate], + [ + asset.assetId, + handleAssetClick, + onSetBuyAsset, + assetFilterPredicate, + chainIdFilterPredicate, + ], ) return ( diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx index 9c64a57fdba..5905d237760 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderInput.tsx @@ -1,10 +1,10 @@ import { Button, Divider, Stack, useMediaQuery } from '@chakra-ui/react' import { skipToken } from '@reduxjs/toolkit/query' -import type { ChainId } from '@shapeshiftoss/caip' +import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { fromAccountId, fromAssetId } from '@shapeshiftoss/caip' import { COW_SWAP_VAULT_RELAYER_ADDRESS, getCowNetwork, SwapperName } from '@shapeshiftoss/swapper' import { isNativeEvmAsset } from '@shapeshiftoss/swapper/dist/swappers/utils/helpers/helpers' -import type { Asset, CowSwapError } from '@shapeshiftoss/types' +import type { CowSwapError } from '@shapeshiftoss/types' import { BigNumber, bn, bnOrZero } from '@shapeshiftoss/utils' import type { FormEvent } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -163,15 +163,17 @@ export const LimitOrderInput = ({ }, []) const sellAssetFilterPredicate = useCallback( - (asset: Asset) => { - return chainIdFilterPredicate(asset.chainId) && !isNativeEvmAsset(asset.assetId) + (assetId: AssetId) => { + const { chainId } = fromAssetId(assetId) + return chainIdFilterPredicate(chainId) && !isNativeEvmAsset(assetId) }, [chainIdFilterPredicate], ) const buyAssetFilterPredicate = useCallback( - (asset: Asset) => { - return chainIdFilterPredicate(asset.chainId) + (assetId: AssetId) => { + const { chainId } = fromAssetId(assetId) + return chainIdFilterPredicate(chainId) }, [chainIdFilterPredicate], ) diff --git a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx index 8548c51b8a3..8983609de74 100644 --- a/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx +++ b/src/components/MultiHopTrade/components/SharedTradeInput/SharedTradeInputBody.tsx @@ -7,7 +7,7 @@ import { IconButton, Stack, } from '@chakra-ui/react' -import type { AccountId, ChainId } from '@shapeshiftoss/caip' +import type { AccountId, AssetId, ChainId } from '@shapeshiftoss/caip' import type { Asset } from '@shapeshiftoss/types' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslate } from 'react-polyglot' @@ -36,7 +36,7 @@ type SharedTradeInputBodyProps = { sellAmountUserCurrency: string | undefined sellAsset: Asset sellAccountId: AccountId | undefined - assetFilterPredicate: (asset: Asset) => boolean + assetFilterPredicate: (assetId: AssetId) => boolean chainIdFilterPredicate: (chainId: ChainId) => boolean onSwitchAssets: () => void onChangeIsInputtingFiatSellAmount: (isInputtingFiatSellAmount: boolean) => void @@ -125,10 +125,17 @@ export const SharedTradeInputBody = ({ onAssetClick={handleSellAssetClick} onAssetChange={setSellAsset} onlyConnectedChains={true} + assetFilterPredicate={assetFilterPredicate} chainIdFilterPredicate={chainIdFilterPredicate} /> ), - [handleSellAssetClick, sellAsset.assetId, setSellAsset, chainIdFilterPredicate], + [ + sellAsset.assetId, + handleSellAssetClick, + setSellAsset, + assetFilterPredicate, + chainIdFilterPredicate, + ], ) return ( diff --git a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx index e3eebf76383..cefe5f6cfd1 100644 --- a/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/TradeInput.tsx @@ -1,4 +1,5 @@ -import type { ChainId } from '@shapeshiftoss/caip' +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { fromAssetId } from '@shapeshiftoss/caip' import { isLedger } from '@shapeshiftoss/hdwallet-ledger' import { isArbitrumBridgeTradeQuoteOrRate } from '@shapeshiftoss/swapper/dist/swappers/ArbitrumBridgeSwapper/getTradeQuote/getTradeQuote' import type { ThorTradeQuote } from '@shapeshiftoss/swapper/dist/swappers/ThorchainSwapper/types' @@ -324,8 +325,9 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput const isSolanaSwapperEnabled = useFeatureFlag('SolanaSwapper') const assetFilterPredicate = useCallback( - (asset: Asset) => { - if (asset.chainId === KnownChainIds.SolanaMainnet) return isSolanaSwapperEnabled + (assetId: AssetId) => { + const { chainId } = fromAssetId(assetId) + if (chainId === KnownChainIds.SolanaMainnet) return isSolanaSwapperEnabled return true }, @@ -357,10 +359,17 @@ export const TradeInput = ({ isCompact, tradeInputRef, onChangeTab }: TradeInput onAssetClick={handleBuyAssetClick} onAssetChange={setBuyAsset} onlyConnectedChains={false} + assetFilterPredicate={assetFilterPredicate} chainIdFilterPredicate={chainIdFilterPredicate} /> ), - [buyAsset.assetId, handleBuyAssetClick, setBuyAsset, chainIdFilterPredicate], + [ + buyAsset.assetId, + handleBuyAssetClick, + setBuyAsset, + assetFilterPredicate, + chainIdFilterPredicate, + ], ) const bodyContent = useMemo(() => { diff --git a/src/components/TradeAssetSearch/TradeAssetSearch.tsx b/src/components/TradeAssetSearch/TradeAssetSearch.tsx index 652c0af8f65..fcaa3f57525 100644 --- a/src/components/TradeAssetSearch/TradeAssetSearch.tsx +++ b/src/components/TradeAssetSearch/TradeAssetSearch.tsx @@ -46,7 +46,7 @@ export type TradeAssetSearchProps = { onAssetClick?: (asset: Asset) => void formProps?: BoxProps allowWalletUnsupportedAssets?: boolean - assetFilterPredicate?: (asset: Asset) => boolean + assetFilterPredicate?: (assetId: AssetId) => boolean chainIdFilterPredicate?: (chainId: ChainId) => boolean } export const TradeAssetSearch: FC = ({ @@ -110,9 +110,9 @@ export const TradeAssetSearch: FC = ({ const popularAssets = useMemo(() => { const unfilteredPopularAssets = popularAssetsByChainId?.[activeChainId] ?? [] - const filteredPopularAssets = assetFilterPredicate - ? unfilteredPopularAssets.filter(assetFilterPredicate) - : unfilteredPopularAssets + const filteredPopularAssets = unfilteredPopularAssets.filter( + asset => assetFilterPredicate?.(asset.assetId) ?? true, + ) if (allowWalletUnsupportedAssets || !hasWallet) return filteredPopularAssets // TODO: move `allowWalletUnsupportedAssets` into `assetFilterPredicate` @@ -150,9 +150,9 @@ export const TradeAssetSearch: FC = ({ }, [activeChainId, popularAssets]) const portfolioAssetsSortedByBalanceForChain = useMemo(() => { - const filteredPortfolioAssetsSortedByBalance = assetFilterPredicate - ? portfolioAssetsSortedByBalance.filter(assetFilterPredicate) - : portfolioAssetsSortedByBalance + const filteredPortfolioAssetsSortedByBalance = portfolioAssetsSortedByBalance.filter( + asset => assetFilterPredicate?.(asset.assetId) ?? true, + ) if (activeChainId === 'All') { return filteredPortfolioAssetsSortedByBalance diff --git a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx index 91e34042e40..b17862c45aa 100644 --- a/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx +++ b/src/components/TradeAssetSearch/components/SearchTermAssetList.tsx @@ -1,4 +1,4 @@ -import type { ChainId } from '@shapeshiftoss/caip' +import type { AssetId, ChainId } from '@shapeshiftoss/caip' import { ASSET_NAMESPACE, bscChainId, isNft, toAssetId } from '@shapeshiftoss/caip' import { isEvmChainId } from '@shapeshiftoss/chain-adapters' import type { Asset } from '@shapeshiftoss/types' @@ -25,7 +25,7 @@ export type SearchTermAssetListProps = { activeChainId: ChainId | 'All' searchString: string allowWalletUnsupportedAssets: boolean | undefined - assetFilterPredicate?: (asset: Asset) => boolean + assetFilterPredicate?: (assetId: AssetId) => boolean onAssetClick: (asset: Asset) => void onImportClick: (asset: Asset) => void } @@ -61,7 +61,7 @@ export const SearchTermAssetList = ({ }) const assetsForChain = useMemo(() => { - const _assets = assetFilterPredicate ? assets.filter(assetFilterPredicate) : assets + const _assets = assets.filter(asset => assetFilterPredicate?.(asset.assetId) ?? true) if (activeChainId === 'All') { if (allowWalletUnsupportedAssets) return _assets return _assets.filter(