Skip to content

Commit

Permalink
chore: allow enough balance check to return unknown state (#3106)
Browse files Browse the repository at this point in the history
* chore: allow enough balance check to return unknown state

* chore: allow undefined in the getPriceQuality fn

* chore: refactor repetitive code

* chore: add nit to simplify expresssion

Co-authored-by: Leandro <[email protected]>

* chore: move logic to private function

* chore: don't allow to check quotes with same sellToken and buyToken

* chore: add private function

* fix: add mapping for error in the buttons map

---------

Co-authored-by: Leandro <[email protected]>
  • Loading branch information
anxolin and alfetopito authored Sep 7, 2023
1 parent 6377f58 commit cadc7ad
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 41 deletions.
18 changes: 14 additions & 4 deletions apps/cowswap-frontend/src/api/gnosisProtocol/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
PriceQuality,
TotalSurplus,
OrderQuoteSideKindBuy,
OrderQuoteSideKindSell
OrderQuoteSideKindSell,
} from '@cowprotocol/cow-sdk'

import { orderBookApi } from 'cowSdk'
Expand All @@ -25,8 +25,8 @@ import { toErc20Address, toNativeBuyAddress } from 'legacy/utils/tokens'

import { getAppData } from 'modules/appData'

import { ApiErrorObject } from 'api/gnosisProtocol/errors/OperatorError'
import GpQuoteError, { mapOperatorErrorToQuoteError } from 'api/gnosisProtocol/errors/QuoteError'
import { ApiErrorCodes, ApiErrorObject } from 'api/gnosisProtocol/errors/OperatorError'
import GpQuoteError, { GpQuoteErrorDetails, mapOperatorErrorToQuoteError } from 'api/gnosisProtocol/errors/QuoteError'

import { LegacyFeeQuoteParams as FeeQuoteParams } from './legacy/types'

Expand Down Expand Up @@ -141,6 +141,16 @@ function _mapNewToLegacyParams(params: FeeQuoteParams): OrderQuoteRequest {
export async function getQuote(params: FeeQuoteParams): Promise<OrderQuoteResponse> {
const { chainId } = params
const quoteParams = _mapNewToLegacyParams(params)
const { sellToken, buyToken } = quoteParams

if (sellToken === buyToken) {
return Promise.reject(
mapOperatorErrorToQuoteError({
errorType: ApiErrorCodes.SameBuyAndSellToken,
description: GpQuoteErrorDetails.SameBuyAndSellToken,
})
)
}

return orderBookApi.getQuote(quoteParams, { chainId }).catch((error) => {
if (isOrderbookTypedError(error)) {
Expand Down Expand Up @@ -225,7 +235,7 @@ export async function getProfileData(chainId: ChainId, address: string): Promise
}
}

export function getPriceQuality(props: { fast?: boolean; verifyQuote: boolean }): PriceQuality {
export function getPriceQuality(props: { fast?: boolean; verifyQuote: boolean | undefined }): PriceQuality {
const { fast = false, verifyQuote } = props
if (fast) {
return PriceQuality.FAST
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export enum GpQuoteErrorCodes {
FeeExceedsFrom = 'FeeExceedsFrom',
ZeroPrice = 'ZeroPrice',
TransferEthToContract = 'TransferEthToContract',
SameBuyAndSellToken = 'SameBuyAndSellToken',
UNHANDLED_ERROR = 'UNHANDLED_ERROR',
}

Expand All @@ -25,6 +26,7 @@ export enum GpQuoteErrorDetails {
FeeExceedsFrom = 'Current fee exceeds entered "from" amount.',
ZeroPrice = 'Quoted price is zero. This is likely due to a significant price difference between the two tokens. Please try increasing amounts.',
TransferEthToContract = 'Buying native currencies using smart contract wallets is not currently supported.',
SameBuyAndSellToken = 'You are trying to buy and sell the same token.',
SellAmountDoesNotCoverFee = 'The selling amount for the order is lower than the fee.',
UNHANDLED_ERROR = 'Quote fetch failed. This may be due to a server or network connectivity issue. Please try again later.',
}
Expand Down Expand Up @@ -55,6 +57,13 @@ export function mapOperatorErrorToQuoteError(error?: ApiErrorObject): GpQuoteErr
errorType: GpQuoteErrorCodes.TransferEthToContract,
description: error.description,
}

case ApiErrorCodes.SameBuyAndSellToken:
return {
errorType: GpQuoteErrorCodes.SameBuyAndSellToken,
description: GpQuoteErrorDetails.SameBuyAndSellToken,
}

default:
return { errorType: GpQuoteErrorCodes.UNHANDLED_ERROR, description: GpQuoteErrorDetails.UNHANDLED_ERROR }
}
Expand Down
2 changes: 1 addition & 1 deletion apps/cowswap-frontend/src/common/hooks/useNeedsApproval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ export function useNeedsApproval(amount: Nullish<CurrencyAmount<Currency>>): boo
return false
}

return !isEnoughAmount(amount, allowance)
return isEnoughAmount(amount, allowance) === false
}
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ export function UnfillableOrdersUpdater(): null {

const currencyAmount = CurrencyAmount.fromRawAmount(order.inputToken, order.sellAmount)
const enoughBalance = hasEnoughBalanceAndAllowance({ account, amount: currencyAmount, balances })
const verifiedQuote = verifiedQuotesEnabled && enoughBalance

_getOrderPrice(chainId, order, enoughBalance && verifiedQuotesEnabled, strategy)
_getOrderPrice(chainId, order, verifiedQuote, strategy)
.then((quote) => {
if (quote) {
const [promisedPrice, promisedFee] = quote
Expand Down Expand Up @@ -207,7 +208,12 @@ export function UnfillableOrdersUpdater(): null {
/**
* Thin wrapper around `getBestPrice` that builds the params and returns null on failure
*/
async function _getOrderPrice(chainId: ChainId, order: Order, verifyQuote: boolean, strategy: GpPriceStrategy) {
async function _getOrderPrice(
chainId: ChainId,
order: Order,
verifyQuote: boolean | undefined,
strategy: GpPriceStrategy
) {
let baseToken, quoteToken

const amount = getRemainderAmount(order.kind, order)
Expand Down
2 changes: 1 addition & 1 deletion apps/cowswap-frontend/src/legacy/state/price/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ export default function FeesUpdater(): null {
userAddress: account,
validTo,
isEthFlow,
priceQuality: getPriceQuality({ verifyQuote: enoughBalance && verifiedQuotesEnabled }),
priceQuality: getPriceQuality({ verifyQuote: verifiedQuotesEnabled && enoughBalance }),
}

// Don't refetch if offline.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function OrderRow({
const showCancellationModal = orderActions.getShowCancellationModal(order)

const withWarning =
(!hasEnoughBalance || !hasEnoughAllowance) &&
(hasEnoughBalance === false || hasEnoughAllowance === false) &&
// show the warning only for pending and scheduled orders
(status === OrderStatus.PENDING || status === OrderStatus.SCHEDULED)
const theme = useContext(ThemeContext)
Expand Down Expand Up @@ -353,10 +353,10 @@ export function OrderRow({
bgColor={theme.alert}
content={
<styledEl.WarningContent>
{!hasEnoughBalance && (
{hasEnoughBalance === false && (
<BalanceWarning symbol={inputTokenSymbol} isScheduled={isOrderScheduled} />
)}
{!hasEnoughAllowance && (
{hasEnoughAllowance === false && (
<AllowanceWarning symbol={inputTokenSymbol} isScheduled={isOrderScheduled} />
)}
</styledEl.WarningContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { Currency, CurrencyAmount, Percent } from '@uniswap/sdk-core'
import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core'

import { BalancesAndAllowances } from 'modules/tokens'

Expand All @@ -12,8 +12,8 @@ export interface OrderParams {
sellAmount: CurrencyAmount<Currency>
buyAmount: CurrencyAmount<Currency>
rateInfoParams: RateInfoParams
hasEnoughBalance: boolean
hasEnoughAllowance: boolean
hasEnoughBalance: boolean | undefined
hasEnoughAllowance: boolean | undefined
}

const PERCENTAGE_FOR_PARTIAL_FILLS = new Percent(5, 10000) // 0.05%
Expand All @@ -38,18 +38,12 @@ export function getOrderParams(
const balance = balances[order.inputToken.address]?.value
const allowance = allowances[order.inputToken.address]?.value

let hasEnoughBalance, hasEnoughAllowance

if (order.partiallyFillable) {
// When balance or allowance are undefined (loading state), show as true
// When loaded, check there's at least PERCENTAGE_FOR_PARTIAL_FILLS of balance/allowance to consider it as enough
const amount = sellAmount.multiply(PERCENTAGE_FOR_PARTIAL_FILLS)
hasEnoughBalance = balance === undefined || isEnoughAmount(amount, balance)
hasEnoughAllowance = allowance === undefined || isEnoughAmount(amount, allowance)
} else {
hasEnoughBalance = isEnoughAmount(sellAmount, balance)
hasEnoughAllowance = isEnoughAmount(sellAmount, allowance)
}
const { hasEnoughBalance, hasEnoughAllowance } = _hasEnoughBalanceAndAllowance({
partiallyFillable: order.partiallyFillable,
sellAmount,
balance,
allowance,
})

return {
chainId,
Expand All @@ -60,3 +54,21 @@ export function getOrderParams(
hasEnoughAllowance,
}
}

function _hasEnoughBalanceAndAllowance(params: {
balance: CurrencyAmount<Token> | undefined
partiallyFillable: boolean
sellAmount: CurrencyAmount<Token>
allowance: CurrencyAmount<Token> | undefined
}): {
hasEnoughBalance: boolean | undefined
hasEnoughAllowance: boolean | undefined
} {
const { allowance, balance, partiallyFillable, sellAmount } = params
// Check there's at least PERCENTAGE_FOR_PARTIAL_FILLS of balance/allowance to consider it as enough
const amount = partiallyFillable ? sellAmount.multiply(PERCENTAGE_FOR_PARTIAL_FILLS) : sellAmount
const hasEnoughBalance = isEnoughAmount(amount, balance)
const hasEnoughAllowance = isEnoughAmount(amount, allowance)

return { hasEnoughBalance, hasEnoughAllowance }
}
47 changes: 38 additions & 9 deletions apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface UseEnoughBalanceParams {
* @param params Parameters to check balance and optionally the allowance
* @returns true if the account has enough balance (and allowance if it applies)
*/
export function useEnoughBalanceAndAllowance(params: UseEnoughBalanceParams): boolean {
export function useEnoughBalanceAndAllowance(params: UseEnoughBalanceParams): boolean | undefined {
const { account, amount, checkAllowanceAddress } = params
const isNativeCurrency = amount?.currency.isNative
const token = amount?.currency.wrapped
Expand Down Expand Up @@ -75,22 +75,51 @@ export interface EnoughBalanceParams extends Omit<UseEnoughBalanceParams, 'check
* @param params Parameters to check balance and optionally the allowance
* @returns true if the account has enough balance (and allowance if it applies)
*/
export function hasEnoughBalanceAndAllowance(params: EnoughBalanceParams): boolean {
export function hasEnoughBalanceAndAllowance(params: EnoughBalanceParams): boolean | undefined {
const { account, amount, balances, nativeBalance, allowances } = params

if (!account || !amount) {
return undefined
}

const isNativeCurrency = amount?.currency.isNative
const token = amount?.currency.wrapped
const tokenAddress = getAddress(token)

const balance = tokenAddress ? balances[tokenAddress]?.value : undefined
const allowance = (tokenAddress && allowances && allowances[tokenAddress]?.value) || undefined
const enoughBalance = _enoughBalance(tokenAddress, amount, balances, isNativeCurrency, nativeBalance)
const enoughAllowance = _enoughAllowance(tokenAddress, amount, allowances, isNativeCurrency)

if (!account || !amount) {
return false
if (enoughBalance === undefined || enoughAllowance === undefined) {
return undefined
}

return enoughBalance && enoughAllowance
}

function _enoughBalance(
tokenAddress: string | null,
amount: CurrencyAmount<Currency>,
balances: TokenAmounts,
isNativeCurrency: boolean,
nativeBalance: CurrencyAmount<Currency> | undefined
): boolean | undefined {
const balance = tokenAddress ? balances[tokenAddress]?.value : undefined
const balanceAmount = isNativeCurrency ? nativeBalance : balance || undefined
const enoughBalance = isEnoughAmount(amount, balanceAmount)
const enoughAllowance = !allowances || isNativeCurrency || (allowance && isEnoughAmount(amount, allowance)) || false
return isEnoughAmount(amount, balanceAmount)
}

return enoughBalance && enoughAllowance
function _enoughAllowance(
tokenAddress: string | null,
amount: CurrencyAmount<Currency>,
allowances: TokenAmounts | undefined,
isNativeCurrency: boolean
): boolean | undefined {
if (!tokenAddress || !allowances) {
return undefined
}
if (isNativeCurrency) {
return true
}
const allowance = allowances[tokenAddress]?.value
return allowance && isEnoughAmount(amount, allowance)
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const quoteErrorTexts: Record<GpQuoteErrorCodes, string> = {
[GpQuoteErrorCodes.InsufficientLiquidity]: 'Insufficient liquidity for this trade.',
[GpQuoteErrorCodes.FeeExceedsFrom]: 'Sell amount is too small',
[GpQuoteErrorCodes.ZeroPrice]: 'Invalid price. Try increasing input/output amount.',
[GpQuoteErrorCodes.SameBuyAndSellToken]: 'Tokens must be different',
}

const unsupportedTokenButton = (context: TradeFormButtonContext) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function useQuoteParams(amount: string | null): LegacyFeeQuoteParams | un
toDecimals,
fromDecimals,
isEthFlow: false,
priceQuality: getPriceQuality({ verifyQuote: enoughBalance && verifiedQuotesEnabled }),
priceQuality: getPriceQuality({ verifyQuote: verifiedQuotesEnabled && enoughBalance }),
}

return params
Expand Down
8 changes: 3 additions & 5 deletions apps/cowswap-frontend/src/utils/isEnoughAmount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
export function isEnoughAmount(
sellAmount: CurrencyAmount<Currency>,
targetAmount: CurrencyAmount<Currency> | undefined
): boolean {
if (!targetAmount) return true
): boolean | undefined {
if (!targetAmount) return undefined

if (targetAmount.equalTo(sellAmount)) return true

return sellAmount.lessThan(targetAmount)
return sellAmount.equalTo(targetAmount) || sellAmount.lessThan(targetAmount)
}

0 comments on commit cadc7ad

Please sign in to comment.