From 4d6a4e741e5e2db0db22ef7febc32c671f671ec0 Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:03:47 +1100 Subject: [PATCH] feat: add warnings to open orders when order cannot be filled (#8359) * feat: warning icon for open limit orders that cant execute * chore: cleanup and comments --- src/assets/translations/en/main.json | 8 +- .../LimitOrder/components/LimitOrderCard.tsx | 97 +++++++++++++++++-- .../LimitOrder/components/LimitOrderList.tsx | 6 +- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json index 4a69324ab33..aff1a21683d 100644 --- a/src/assets/translations/en/main.json +++ b/src/assets/translations/en/main.json @@ -1079,7 +1079,13 @@ }, "orders": "Orders", "viewOrders": "View Orders", - "loadingOrderList": "Loading your limit orders..." + "loadingOrderList": "Loading your limit orders...", + "orderCard": { + "warning": { + "insufficientBalance": "Your wallet currently has insufficient %{symbol} balance on %{chainName} to execute this order. The order is still open and will become executable when you top up your %{symbol} balance on %{chainName}.", + "insufficientAllowance": "This order requires a additional allowance of %{symbol} on %{chainName} to be approved for CoW Swap to execute this order. The order is still open and will become executable when you complete the allowance approval." + } + } }, "modals": { "assetSearch": { diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderCard.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderCard.tsx index 783f7ad168c..6ddf844a9b2 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderCard.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderCard.tsx @@ -1,18 +1,26 @@ -import { Box, Button, Center, Flex, Progress, Tag, Tooltip } from '@chakra-ui/react' -import type { AssetId } from '@shapeshiftoss/caip' +import { WarningTwoIcon } from '@chakra-ui/icons' +import { Box, Button, Center, Flex, Progress, Tag, TagLabel, Tooltip } from '@chakra-ui/react' +import type { AccountId, AssetId } from '@shapeshiftoss/caip' +import { fromAccountId } from '@shapeshiftoss/caip' +import { COW_SWAP_VAULT_RELAYER_ADDRESS } from '@shapeshiftoss/swapper' import { OrderStatus } from '@shapeshiftoss/types' -import { bn, fromBaseUnit } from '@shapeshiftoss/utils' +import { bn, bnOrZero, fromBaseUnit } from '@shapeshiftoss/utils' import { formatDistanceToNow } from 'date-fns' import type { FC } from 'react' import { useCallback, useMemo } from 'react' import { useTranslate } from 'react-polyglot' +import { useAllowance } from 'react-queries/hooks/useAllowance' import { Amount } from 'components/Amount/Amount' import { AssetIconWithBadge } from 'components/AssetIconWithBadge' import { SwapBoldIcon } from 'components/Icons/SwapBold' import { RawText, Text } from 'components/Text' import { useLocaleFormatter } from 'hooks/useLocaleFormatter/useLocaleFormatter' -import { selectAssetById } from 'state/slices/selectors' -import { useAppSelector } from 'state/store' +import { assertGetChainAdapter } from 'lib/utils' +import { + selectAssetById, + selectPortfolioCryptoBalanceBaseUnitByFilter, +} from 'state/slices/selectors' +import { useSelectorWithArgs } from 'state/store' export type LimitOrderCardProps = { uid: string @@ -23,6 +31,7 @@ export type LimitOrderCardProps = { validTo?: number filledDecimalPercentage: number status: OrderStatus + accountId: AccountId onCancelClick?: (uid: string) => void } @@ -39,6 +48,7 @@ export const LimitOrderCard: FC = ({ validTo, filledDecimalPercentage, status, + accountId, onCancelClick, }) => { const translate = useTranslate() @@ -46,8 +56,44 @@ export const LimitOrderCard: FC = ({ number: { toCrypto }, } = useLocaleFormatter() - const buyAsset = useAppSelector(state => selectAssetById(state, buyAssetId)) - const sellAsset = useAppSelector(state => selectAssetById(state, sellAssetId)) + const buyAsset = useSelectorWithArgs(selectAssetById, buyAssetId) + const sellAsset = useSelectorWithArgs(selectAssetById, sellAssetId) + + const filter = useMemo(() => { + return { + accountId, + assetId: sellAssetId, + } + }, [accountId, sellAssetId]) + + const sellAssetBalanceCryptoBaseUnit = useSelectorWithArgs( + selectPortfolioCryptoBalanceBaseUnitByFilter, + filter, + ) + + const hasSufficientBalance = useMemo(() => { + return bnOrZero(sellAssetBalanceCryptoBaseUnit).gte(sellAmountCryptoBaseUnit) + }, [sellAmountCryptoBaseUnit, sellAssetBalanceCryptoBaseUnit]) + + const from = useMemo(() => { + return fromAccountId(accountId).account + }, [accountId]) + + const { data: allowanceOnChainCryptoBaseUnit } = useAllowance({ + assetId: sellAssetId, + spender: COW_SWAP_VAULT_RELAYER_ADDRESS, + from, + // Don't fetch allowance if there is insufficient balance, because we wont display the allowance + // warning in this case. + isDisabled: !hasSufficientBalance || status !== OrderStatus.OPEN, + }) + + const hasSufficientAllowance = useMemo(() => { + // If the request failed, default to true since this is just a helper and not safety critical. + if (!allowanceOnChainCryptoBaseUnit) return true + + return bn(sellAmountCryptoBaseUnit).lte(allowanceOnChainCryptoBaseUnit) + }, [allowanceOnChainCryptoBaseUnit, sellAmountCryptoBaseUnit]) const handleCancel = useCallback(() => { onCancelClick?.(uid) @@ -118,6 +164,30 @@ export const LimitOrderCard: FC = ({ [buyAmountCryptoPrecision, toCrypto, buyAsset], ) + const warningText = useMemo(() => { + if (status !== OrderStatus.OPEN) return + + const translationProps = { + symbol: sellAsset?.symbol ?? '', + chainName: assertGetChainAdapter(sellAsset?.chainId ?? '')?.getDisplayName() ?? '', + } + + if (!hasSufficientBalance) { + return translate('limitOrder.orderCard.warning.insufficientBalance', translationProps) + } + + if (!hasSufficientAllowance) { + return translate('limitOrder.orderCard.warning.insufficientAllowance', translationProps) + } + }, [ + hasSufficientAllowance, + hasSufficientBalance, + sellAsset?.chainId, + sellAsset?.symbol, + status, + translate, + ]) + if (!buyAsset || !sellAsset) return null return ( @@ -172,9 +242,16 @@ export const LimitOrderCard: FC = ({ {/* Right group - status tag */} - - {translate(`limitOrder.status.${status}`)} - + + + {translate(`limitOrder.status.${status}`)} + + {Boolean(warningText) && ( + + + + )} + {/* Price row */} diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx index 6ce0e572c67..0d87c9d9d05 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx @@ -131,10 +131,11 @@ export const LimitOrderList: FC = ({ cardProps, onBack }) = )} {openLimitOrders !== undefined && openLimitOrders.length > 0 && - openLimitOrders.map(({ sellAssetId, buyAssetId, order }) => ( + openLimitOrders.map(({ accountId, sellAssetId, buyAssetId, order }) => ( = ({ cardProps, onBack }) = {historicalLimitOrders !== undefined && historicalLimitOrders.length > 0 ? ( - historicalLimitOrders.map(({ sellAssetId, buyAssetId, order }) => ( + historicalLimitOrders.map(({ accountId, sellAssetId, buyAssetId, order }) => (