Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: allow enough balance check to return unknown state #3106

Merged
merged 8 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
})
)
}
alfetopito marked this conversation as resolved.
Show resolved Hide resolved

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,
}
anxolin marked this conversation as resolved.
Show resolved Hide resolved

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) &&
anxolin marked this conversation as resolved.
Show resolved Hide resolved
// 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
anxolin marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading