Skip to content

Commit

Permalink
chore(swap): handle swap balance errors (#2755)
Browse files Browse the repository at this point in the history
Co-authored-by: Juliano Lazzarotto <[email protected]>
  • Loading branch information
banklesss and stackchain authored Oct 11, 2023
1 parent 5a6051f commit 0bf13ce
Show file tree
Hide file tree
Showing 12 changed files with 682 additions and 508 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ storiesOf('Amount Card', module)
console.log('VALUE', value)
}}
value="2223"
hasError
error="Fake Error"
touched
/>
))
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {useSwap} from '@yoroi/swap'
import {Balance} from '@yoroi/types'
import React, {useRef} from 'react'
import {defineMessages, useIntl} from 'react-intl'
Expand All @@ -8,6 +7,7 @@ import {TouchableOpacity} from 'react-native-gesture-handler'
import {Boundary, Icon, Spacer, TokenIcon, TokenIconPlaceholder} from '../../../../components'
import {formatTokenWithText} from '../../../../legacy/format'
import {COLORS} from '../../../../theme'
import {isEmptyString} from '../../../../utils'
import {YoroiWallet} from '../../../../yoroi-wallets/cardano/types'
import {useTokenInfo} from '../../../../yoroi-wallets/hooks'
import {Quantities} from '../../../../yoroi-wallets/utils'
Expand All @@ -18,11 +18,11 @@ type Props = {
amount: Balance.Amount
onChange(value: string): void
value?: string
hasError?: boolean
navigateTo?: () => void
touched?: boolean
inputRef?: React.RefObject<TextInput>
inputEditable?: boolean
error?: string
}

export const AmountCard = ({
Expand All @@ -32,19 +32,16 @@ export const AmountCard = ({
wallet,
amount,
navigateTo,
hasError,
touched,
inputRef,
inputEditable = true,
error,
}: Props) => {
const strings = useStrings()
const {quantity, tokenId} = amount
const amountInputRef = useRef<TextInput>(inputRef?.current ?? null)

const tokenInfo = useTokenInfo({wallet, tokenId})
const {orderData} = useSwap()

const isSell = tokenId === orderData.amounts.sell.tokenId

const noTokenSelected = !touched

Expand All @@ -59,8 +56,8 @@ export const AmountCard = ({
}
return (
<View>
<View style={[styles.container, hasError && styles.borderError]}>
{label != null && <Text style={[styles.label, hasError && styles.labelError]}>{label}</Text>}
<View style={[styles.container, !isEmptyString(error) && styles.borderError]}>
{label != null && <Text style={[styles.label, !isEmptyString(error) && styles.labelError]}>{label}</Text>}

<View style={styles.content}>
<Pressable style={styles.amountWrapper} onPress={focusInput}>
Expand Down Expand Up @@ -113,17 +110,11 @@ export const AmountCard = ({
</View>
</View>

{hasError && (
{!isEmptyString(error) && (
<View>
<Spacer height={4} />

<Text style={styles.errorText}>
{orderData.selectedPoolCalculation === undefined
? strings.noPool
: isSell
? strings.notEnoughBalance
: strings.notEnoughSupply}
</Text>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
</View>
Expand All @@ -139,28 +130,13 @@ const messages = defineMessages({
id: 'swap.swapScreen.currentBalance',
defaultMessage: '!!!Current Balance',
},
notEnoughBalance: {
id: 'swap.swapScreen.notEnoughBalance',
defaultMessage: '!!!Not enough balance',
},
notEnoughSupply: {
id: 'swap.swapScreen.notEnoughSupply',
defaultMessage: '!!!Not enough supply in the pool',
},
noPool: {
id: 'swap.swapScreen.noPool',
defaultMessage: '!!! This pair is not available in any liquidity pool',
},
})

const useStrings = () => {
const intl = useIntl()
return {
selectToken: intl.formatMessage(messages.selectToken),
currentBalance: intl.formatMessage(messages.currentBalance),
notEnoughBalance: intl.formatMessage(messages.notEnoughBalance),
notEnoughSupply: intl.formatMessage(messages.notEnoughSupply),
noPool: intl.formatMessage(messages.noPool),
}
}

Expand Down
17 changes: 17 additions & 0 deletions apps/wallet-mobile/src/features/Swap/common/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ export const useStrings = () => {
generalTxErrorMessage: intl.formatMessage(errorMessages.generalTxError.message),
incorrectPasswordTitle: intl.formatMessage(errorMessages.incorrectPassword.title),
incorrectPasswordMessage: intl.formatMessage(errorMessages.incorrectPassword.message),
notEnoughBalance: intl.formatMessage(messages.notEnoughBalance),
notEnoughSupply: intl.formatMessage(messages.notEnoughSupply),
noPool: intl.formatMessage(messages.noPool),
generalErrorTitle: intl.formatMessage(errorMessages.generalError.title),
generalErrorMessage: (e) => intl.formatMessage(errorMessages.generalError.message, {message: e}),
}
}

Expand Down Expand Up @@ -510,4 +515,16 @@ export const messages = defineMessages({
id: 'components.send.sendscreen.failedTxButton',
defaultMessage: '!!!Try again',
},
notEnoughBalance: {
id: 'swap.swapScreen.notEnoughBalance',
defaultMessage: '!!!Not enough balance',
},
notEnoughSupply: {
id: 'swap.swapScreen.notEnoughSupply',
defaultMessage: '!!!Not enough supply in the pool',
},
noPool: {
id: 'swap.swapScreen.noPool',
defaultMessage: '!!! This pair is not available in any liquidity pool',
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import {makeLimitOrder, makePossibleMarketOrder, useSwap, useSwapCreateOrder, us
import {Swap} from '@yoroi/types'
import BigNumber from 'bignumber.js'
import * as React from 'react'
import {KeyboardAvoidingView, Platform, StyleSheet, View, ViewProps} from 'react-native'
import {Alert, KeyboardAvoidingView, Platform, StyleSheet, View, ViewProps} from 'react-native'
import {ScrollView} from 'react-native-gesture-handler'

import {Button, Spacer} from '../../../../../components'
import {LoadingOverlay} from '../../../../../components/LoadingOverlay'
import {useMetrics} from '../../../../../metrics/metricsManager'
import {useSelectedWallet} from '../../../../../SelectedWallet'
import {COLORS} from '../../../../../theme'
import {useTokenInfo} from '../../../../../yoroi-wallets/hooks'
import {isEmptyString} from '../../../../../utils'
import {NotEnoughMoneyToSendError} from '../../../../../yoroi-wallets/cardano/types'
import {useBalance, useTokenInfo} from '../../../../../yoroi-wallets/hooks'
import {Quantities} from '../../../../../yoroi-wallets/utils'
import {createYoroiEntry} from '../../../common/helpers'
import {useNavigateTo} from '../../../common/navigation'
Expand All @@ -35,6 +37,7 @@ export const CreateOrder = () => {
const wallet = useSelectedWallet()
const {track} = useMetrics()
const {isBuyTouched, isSellTouched, poolDefaulted} = useSwapTouched()
const [sellBackendError, setSellBackendError] = React.useState('')

useSwapPoolsByPair(
{
Expand Down Expand Up @@ -70,7 +73,12 @@ export const CreateOrder = () => {
setShowLimitPriceWarning(false)
},
onError: (error) => {
console.log(error)
if (error instanceof NotEnoughMoneyToSendError) {
setSellBackendError(strings.notEnoughBalance)
return
}

Alert.alert(strings.generalErrorTitle, strings.generalErrorMessage(error.message))
},
})

Expand All @@ -94,16 +102,21 @@ export const CreateOrder = () => {
}
},
onError: (error) => {
console.log(error)
Alert.alert(strings.generalErrorTitle, strings.generalErrorMessage(error))
},
})

const sellError = useSellError([sellBackendError])
const buyError = useBuyError()

const disabled =
!isBuyTouched ||
!isSellTouched ||
Quantities.isZero(orderData.amounts.buy.quantity) ||
Quantities.isZero(orderData.amounts.sell.quantity) ||
(orderData.type === 'limit' && orderData.limitPrice !== undefined && Quantities.isZero(orderData.limitPrice))
(orderData.type === 'limit' && orderData.limitPrice !== undefined && Quantities.isZero(orderData.limitPrice)) ||
!isEmptyString(sellError) ||
!isEmptyString(buyError)

const swap = () => {
if (orderData.selectedPoolCalculation === undefined) return
Expand Down Expand Up @@ -207,15 +220,15 @@ export const CreateOrder = () => {

<TopTokenActions />

<EditSellAmount />
<EditSellAmount error={sellError} />

<Spacer height={16} />

<ShowTokenActions />

<Spacer height={16} />

<EditBuyAmount />
<EditBuyAmount error={buyError} />

<Spacer height={20} />

Expand All @@ -239,6 +252,83 @@ export const CreateOrder = () => {

const Actions = ({style, ...props}: ViewProps) => <View style={[styles.actions, style]} {...props} />

const useSellError = (errors: Array<string> = []): string => {
const noPoolError = useNoPoolError()
const notEnoughBalanceError = useNotEnoughBalanceError()

const allErrors = [noPoolError, notEnoughBalanceError, ...errors]
const sellError = allErrors.find((error) => !isEmptyString(error))

return sellError ?? ''
}

const useBuyError = (errors: Array<string> = []): string => {
const noPoolError = useNoPoolError()
const notEnoughSupplyError = useNotEnoughSupplyError()

const allErrors = [noPoolError, notEnoughSupplyError, ...errors]
const buyError = allErrors.find((error) => !isEmptyString(error))

return buyError ?? ''
}

const useNotEnoughSupplyError = (): string => {
const strings = useStrings()
const {orderData} = useSwap()
const {isBuyTouched, isSellTouched} = useSwapTouched()
const pool = orderData.selectedPoolCalculation?.pool
const {tokenId, quantity} = orderData.amounts.buy
const poolSupply = tokenId === pool?.tokenA.tokenId ? pool?.tokenA.quantity : pool?.tokenB.quantity
const hasSupply = !Quantities.isGreaterThan(quantity, poolSupply ?? Quantities.zero)

const notEnoughSupplyError =
(!Quantities.isZero(quantity) && !hasSupply) || (isSellTouched && isBuyTouched && pool === undefined)
? strings.notEnoughSupply
: ''

return notEnoughSupplyError
}

const useNotEnoughBalanceError = (): string => {
const strings = useStrings()
const {orderData} = useSwap()
const {isBuyTouched} = useSwapTouched()
const wallet = useSelectedWallet()
const {tokenId, quantity} = orderData.amounts.sell
const balance = useBalance({wallet, tokenId})

const hasPrimaryTokenBalance = !Quantities.isGreaterThan(
Quantities.sum([
tokenId === wallet.primaryTokenInfo.id ? orderData.amounts.sell.quantity : Quantities.zero,
orderData.selectedPoolCalculation?.cost.ptTotalFeeNoFEF.quantity ?? Quantities.zero,
]),
balance,
)

const hasSecondaryTokenBalance = !Quantities.isGreaterThan(
tokenId !== wallet.primaryTokenInfo.id ? orderData.amounts.sell.quantity : Quantities.zero,
balance,
)

const notEnoughBalanceError =
!Quantities.isZero(quantity) && !hasPrimaryTokenBalance && !hasSecondaryTokenBalance && isBuyTouched
? strings.notEnoughBalance
: ''

return notEnoughBalanceError
}

const useNoPoolError = () => {
const strings = useStrings()
const {orderData} = useSwap()
const {isBuyTouched, isSellTouched} = useSwapTouched()

const noPoolError =
orderData.selectedPoolCalculation === undefined && isBuyTouched && isSellTouched ? strings.noPool : ''

return noPoolError
}

const styles = StyleSheet.create({
root: {
flex: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import {useNavigateTo} from '../../../../common/navigation'
import {useStrings} from '../../../../common/strings'
import {useSwapTouched} from '../../../../common/SwapFormProvider'

export const EditBuyAmount = () => {
export const EditBuyAmount = ({error = ''}: {error?: string}) => {
const strings = useStrings()
const navigate = useNavigateTo()
const wallet = useSelectedWallet()
const {numberLocale} = useLanguage()
const inputRef = React.useRef<TextInput>(null)

const {orderData, buyQuantityChanged} = useSwap()
const {isBuyTouched, isSellTouched} = useSwapTouched()
const {isBuyTouched} = useSwapTouched()
const pool = orderData.selectedPoolCalculation?.pool
const {tokenId, quantity} = orderData.amounts.buy
const tokenInfo = useTokenInfo({wallet, tokenId})
Expand All @@ -35,11 +35,6 @@ export const EditBuyAmount = () => {
}
}, [isBuyTouched, quantity, tokenInfo.decimals])

const poolSupply = tokenId === pool?.tokenA.tokenId ? pool?.tokenA.quantity : pool?.tokenB.quantity
const hasSupply = !Quantities.isGreaterThan(quantity, poolSupply ?? Quantities.zero)
const showError =
(!Quantities.isZero(quantity) && !hasSupply) || (isSellTouched && isBuyTouched && pool === undefined)

const onChangeQuantity = (text: string) => {
try {
const [input, quantity] = Quantities.parseFromText(text, decimals ?? 0, numberLocale)
Expand All @@ -57,11 +52,11 @@ export const EditBuyAmount = () => {
value={inputValue}
amount={{tokenId, quantity: balance}}
wallet={wallet}
hasError={showError}
navigateTo={navigate.selectBuyToken}
touched={isBuyTouched}
inputRef={inputRef}
inputEditable={pool !== undefined}
error={error}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ const mockWallet = produce(mocks.wallet, (draft) => {
},
]
})
const mockSwapStateNoBalance = produce(mockSwapStateDefault, (draft) => {
draft.orderData.amounts.sell.quantity = '3000000'
})
const mockSwapStateSecodaryToken = produce(mockSwapStateDefault, (draft) => {
draft.orderData.amounts.sell.tokenId = '2a0879034f23ea48ba28dc1c15b056bd63b8cf0cab9733da92add22f.444444'
})
Expand Down Expand Up @@ -89,21 +86,6 @@ storiesOf('Swap Edit Sell Amount', module)
</SelectedWalletProvider>
)
})
.add('without balance error', () => {
return (
<SelectedWalletProvider wallet={mockWallet}>
<SearchProvider>
<SwapProvider swapManager={mockSwapManager} initialState={mockSwapStateNoBalance}>
<SwapFormProvider>
<View style={styles.container}>
<EditSellAmount />
</View>
</SwapFormProvider>
</SwapProvider>
</SearchProvider>
</SelectedWalletProvider>
)
})
.add('secondary token', () => {
return (
<SelectedWalletProvider wallet={mockWallet}>
Expand Down
Loading

0 comments on commit 0bf13ce

Please sign in to comment.