Skip to content

Commit

Permalink
feat: expose monadic quote errors to the user (#5055)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xApotheosis authored Aug 9, 2023
1 parent 4c9787a commit 1822ee1
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 50 deletions.
2 changes: 2 additions & 0 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,7 @@
"insufficientFundsForProtocolFee": "Insufficient %{symbol} on %{chainName} for fees",
"invalidTradePair": "Invalid trade pair: %{sellAssetName} and %{buyAssetName}",
"invalidTradePairBtnText": "Invalid Trade Pair",
"unsupportedTradePair": "Unsupported Trade Pair",
"noLiquidityError": "Not enough liquidity available",
"overMaxSlippage": "This trade would result in over %{slippagePercentage}% slippage. Please trade a lower amount for a better price",
"balanceToLow": "Balance too low. The minimum trade amount for this pair is %{minLimit}",
Expand All @@ -745,6 +746,7 @@
"assetNotSupportedByWallet": "%{assetSymbol} not supported by wallet",
"noReceiveAddress": "No receive address for %{assetSymbol}",
"tradingNotActive": "Trades temporarily halted for %{assetSymbol}",
"tradingNotActiveNoAssetSymbol": "Trades temporarily halted for this route",
"signing": {
"failed": "An error occurred signing the transaction",
"required": "A signed transaction is required"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,14 @@ export const TradeInput = memo(() => {
const isSellAmountEntered = bnOrZero(sellAmountCryptoPrecision).gt(0)

const shouldDisablePreviewButton = useMemo(() => {
return quoteHasError || manualReceiveAddressIsValidating || isLoading || !isSellAmountEntered
}, [isLoading, isSellAmountEntered, manualReceiveAddressIsValidating, quoteHasError])
return (
quoteHasError ||
manualReceiveAddressIsValidating ||
isLoading ||
!isSellAmountEntered ||
!activeQuote
)
}, [activeQuote, isLoading, isSellAmountEntered, manualReceiveAddressIsValidating, quoteHasError])

const rightRegion = useMemo(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { useCallback, useMemo } from 'react'
import { FaGasPump } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'
import { Amount } from 'components/Amount/Amount'
import { quoteStatusTranslation } from 'components/MultiHopTrade/components/TradeInput/components/TradeQuotes/getQouteErrorTranslation'
import { useIsTradingActive } from 'components/MultiHopTrade/hooks/useIsTradingActive'
import { RawText } from 'components/Text'
import { bnOrZero } from 'lib/bignumber/bignumber'
import { SwapErrorType } from 'lib/swapper/api'
import type { ApiQuote } from 'state/apis/swappers'
import {
selectBuyAsset,
Expand Down Expand Up @@ -80,6 +82,8 @@ export const TradeQuoteLoaded: React.FC<TradeQuoteLoadedProps> = ({
const hoverColor = useColorModeValue('blackAlpha.300', 'whiteAlpha.300')
const focusColor = useColorModeValue('blackAlpha.400', 'whiteAlpha.400')

const { quote, error } = quoteData

const { isTradingActive } = useIsTradingActive()

const buyAsset = useAppSelector(selectBuyAsset)
Expand All @@ -89,20 +93,20 @@ export const TradeQuoteLoaded: React.FC<TradeQuoteLoadedProps> = ({

// NOTE: don't pull this from the slice - we're not displaying the active quote here
const networkFeeUserCurrencyPrecision = useMemo(
() => (quoteData.quote ? getTotalNetworkFeeUserCurrencyPrecision(quoteData.quote) : undefined),
[quoteData.quote],
() => (quote ? getTotalNetworkFeeUserCurrencyPrecision(quote) : undefined),
[quote],
)

// NOTE: don't pull this from the slice - we're not displaying the active quote here
const totalReceiveAmountCryptoPrecision = useMemo(
() =>
quoteData.quote
quote
? getNetReceiveAmountCryptoPrecision({
quote: quoteData.quote,
quote,
swapperName: quoteData.swapperName,
})
: '0',
[quoteData.quote, quoteData.swapperName],
[quote, quoteData.swapperName],
)

const handleQuoteSelection = useCallback(() => {
Expand All @@ -126,23 +130,32 @@ export const TradeQuoteLoaded: React.FC<TradeQuoteLoadedProps> = ({
bnOrZero(totalReceiveAmountCryptoPrecision).isGreaterThan(0)

const tag: JSX.Element = useMemo(() => {
switch (true) {
case !hasAmountWithPositiveReceive && isAmountEntered:
return (
<Tag size='sm' colorScheme='red'>
{translate('trade.rates.tags.negativeRatio')}
</Tag>
)
case isBest:
return (
<Tag size='sm' colorScheme='green'>
{translate('common.best')}
</Tag>
)
default:
return <Tag size='sm'>{translate('common.alternative')}</Tag>
if (quote)
switch (true) {
case !hasAmountWithPositiveReceive && isAmountEntered:
return (
<Tag size='sm' colorScheme='red'>
{translate('trade.rates.tags.negativeRatio')}
</Tag>
)
case isBest:
return (
<Tag size='sm' colorScheme='green'>
{translate('common.best')}
</Tag>
)
default:
return <Tag size='sm'>{translate('common.alternative')}</Tag>
}
else {
// Add helper to get user-friendly error message from code
return (
<Tag size='sm' colorScheme='red'>
{translate(quoteStatusTranslation(error))}
</Tag>
)
}
}, [hasAmountWithPositiveReceive, isAmountEntered, translate, isBest])
}, [quote, hasAmountWithPositiveReceive, isAmountEntered, translate, isBest, error])

const activeSwapperColor = (() => {
if (!isTradingActive) return redColor
Expand All @@ -160,23 +173,31 @@ export const TradeQuoteLoaded: React.FC<TradeQuoteLoadedProps> = ({
[activeSwapperColor, focusColor, isActive],
)

return totalReceiveAmountCryptoPrecision ? (
const isDisabled = !!error

// TODO: work out for which error codes we want to show a swapper with a human-readable error vs hiding it
const showSwapperError =
error?.code === SwapErrorType.TRADING_HALTED || error?.code === SwapErrorType.UNSUPPORTED_PAIR
const showSwapper = !!quote || showSwapperError

return showSwapper ? (
<Flex
borderWidth={1}
cursor='pointer'
cursor={isDisabled ? 'not-allowed' : 'pointer'}
borderColor={isActive ? activeSwapperColor : borderColor}
_hover={hoverProps}
_active={activeProps}
_hover={isDisabled ? undefined : hoverProps}
_active={isDisabled ? undefined : activeProps}
borderRadius='xl'
flexDir='column'
gap={2}
width='full'
px={4}
py={2}
fontSize='sm'
onClick={handleQuoteSelection}
onClick={isDisabled ? undefined : handleQuoteSelection}
transitionProperty='common'
transitionDuration='normal'
opacity={isDisabled ? 0.4 : 1}
>
<Flex justifyContent='space-between' alignItems='center'>
<Flex gap={2}>
Expand All @@ -187,30 +208,34 @@ export const TradeQuoteLoaded: React.FC<TradeQuoteLoadedProps> = ({
</Tag>
)}
</Flex>
<Flex gap={2} alignItems='center'>
<RawText color='gray.500'>
<FaGasPump />
</RawText>
{
// We cannot infer gas fees in specific scenarios, so if the fee is undefined we must render is as such
!networkFeeUserCurrencyPrecision ? (
translate('trade.unknownGas')
) : (
<Amount.Fiat value={networkFeeUserCurrencyPrecision} />
)
}
</Flex>
{quote && (
<Flex gap={2} alignItems='center'>
<RawText color='gray.500'>
<FaGasPump />
</RawText>
{
// We cannot infer gas fees in specific scenarios, so if the fee is undefined we must render is as such
!networkFeeUserCurrencyPrecision ? (
translate('trade.unknownGas')
) : (
<Amount.Fiat value={networkFeeUserCurrencyPrecision} />
)
}
</Flex>
)}
</Flex>
<Flex justifyContent='space-between' alignItems='center'>
<Flex gap={2} alignItems='center'>
<SwapperIcon swapperName={quoteData.swapperName} />
<RawText>{quoteData.swapperName}</RawText>
</Flex>
<Amount.Crypto
value={hasAmountWithPositiveReceive ? totalReceiveAmountCryptoPrecision : '0'}
symbol={buyAsset?.symbol ?? ''}
color={isBest ? greenColor : 'inherit'}
/>
{quote && (
<Amount.Crypto
value={hasAmountWithPositiveReceive ? totalReceiveAmountCryptoPrecision : '0'}
symbol={buyAsset?.symbol ?? ''}
color={isBest ? greenColor : 'inherit'}
/>
)}
</Flex>
</Flex>
) : null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ export const TradeQuotes: React.FC<TradeQuotesProps> = memo(({ isOpen, sortedQuo
const quotes = useMemo(
() =>
sortedQuotes.map((quoteData, i) => {
const { quote, swapperName } = quoteData

// TODO(woodenfurniture): we may want to display per-swapper errors here
if (!quote) return null
const { swapperName } = quoteData

// TODO(woodenfurniture): use quote ID when we want to support multiple quotes per swapper
const isActive = activeSwapperName === swapperName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { InterpolationOptions } from 'node-polyglot'
import type { SwapErrorRight } from 'lib/swapper/api'
import { SwapErrorType } from 'lib/swapper/api'

export const quoteStatusTranslation = (
swapError: SwapErrorRight | undefined,
): string | [string, InterpolationOptions] => {
const code = swapError?.code

switch (code) {
case SwapErrorType.TRADING_HALTED:
return 'trade.errors.tradingNotActiveNoAssetSymbol'
case SwapErrorType.UNSUPPORTED_PAIR:
return 'trade.errors.unsupportedTradePair'
default:
return 'trade.errors.quoteError'
}
}
1 change: 1 addition & 0 deletions src/lib/swapper/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export enum SwapErrorType {
MISSING_INPUT = 'MISSING_INPUT',
// Catch-all for happy responses, but entity not found according to our criteria
NOT_FOUND = 'NOT_FOUND',
TRADING_HALTED = 'TRADING_HALTED',
}

export type TradeQuote2 = TradeQuote & {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ export const getQuote = async ({
details: { sellAssetId: sellAsset.assetId, buyAssetId },
}),
)
} else if (isError && /trading is halted/.test(data.error)) {
return Err(
makeSwapErrorRight({
message: `[getTradeRate]: Trading is halted, cannot process swap`,
code: SwapErrorType.TRADING_HALTED,
details: { sellAssetId: sellAsset.assetId, buyAssetId },
}),
)
} else if (isError) {
return Err(
makeSwapErrorRight({
Expand Down

0 comments on commit 1822ee1

Please sign in to comment.