diff --git a/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts index 7c914abf89..3e39b7a788 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/SpotPricesUpdater.ts @@ -105,8 +105,11 @@ export function SpotPricesUpdater(): null { }) } catch (e) { console.error( - `Failed to update spot prices for ${inputCurrency.address} and ${outputCurrency.address}`, - { inputPrice, outputPrice }, + `[SpotPricesUpdater] Failed to calculate spot price for ${inputCurrency.address} and ${outputCurrency.address}`, + inputPrice.price.numerator.toString(), + inputPrice.price.denominator.toString(), + outputPrice.price.numerator.toString(), + outputPrice.price.denominator.toString(), e, ) } diff --git a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts index 7667007a5e..ba2ee287d4 100644 --- a/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts +++ b/apps/cowswap-frontend/src/common/updaters/orders/UnfillableOrdersUpdater.ts @@ -181,6 +181,7 @@ export function UnfillableOrdersUpdater(): null { async function _getOrderPrice(chainId: ChainId, order: Order, strategy: PriceStrategy) { let baseToken, quoteToken + // TODO: consider a fixed amount in case of partial fills const amount = getRemainderAmount(order.kind, order) // Don't quote if there's nothing left to match in this order diff --git a/apps/cowswap-frontend/src/legacy/state/orders/utils.ts b/apps/cowswap-frontend/src/legacy/state/orders/utils.ts index b84691ec50..1e3aec063b 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/utils.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/utils.ts @@ -1,10 +1,11 @@ import type { LatestAppDataDocVersion } from '@cowprotocol/app-data' import { ONE_HUNDRED_PERCENT, PENDING_ORDERS_BUFFER, ZERO_FRACTION } from '@cowprotocol/common-const' -import { bpsToPercent, buildPriceFromCurrencyAmounts, isSellOrder } from '@cowprotocol/common-utils' +import { bpsToPercent, buildPriceFromCurrencyAmounts, getWrappedToken, isSellOrder } from '@cowprotocol/common-utils' import { EnrichedOrder, OrderKind, OrderStatus } from '@cowprotocol/cow-sdk' import { UiOrderType } from '@cowprotocol/types' import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core' +import BigNumber from 'bignumber.js' import JSBI from 'jsbi' import { decodeAppData } from 'modules/appData/utils/decodeAppData' @@ -12,7 +13,7 @@ import { decodeAppData } from 'modules/appData/utils/decodeAppData' import { getIsComposableCowParentOrder } from 'utils/orderUtils/getIsComposableCowParentOrder' import { getOrderSurplus } from 'utils/orderUtils/getOrderSurplus' import { getUiOrderType } from 'utils/orderUtils/getUiOrderType' -import type { ParsedOrder } from 'utils/orderUtils/parseOrder' +import { ParsedOrder } from 'utils/orderUtils/parseOrder' import { Order, updateOrder, UpdateOrderParams as UpdateOrderParamsAction } from './actions' import { OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE } from './consts' @@ -35,7 +36,10 @@ export type OrderTransitionStatus = * or all buyAmount has been bought, for buy orders */ export function isOrderFulfilled( - order: Pick + order: Pick< + EnrichedOrder, + 'buyAmount' | 'sellAmount' | 'executedBuyAmount' | 'executedSellAmountBeforeFees' | 'kind' + >, ): boolean { const { buyAmount, sellAmount, executedBuyAmount, executedSellAmountBeforeFees, kind } = order @@ -98,7 +102,7 @@ export function classifyOrder( | 'kind' | 'signingScheme' | 'status' - > | null + > | null, ): OrderTransitionStatus { if (!order) { console.debug(`[state::orders::classifyOrder] unknown order`) @@ -133,7 +137,7 @@ export function classifyOrder( export function isOrderUnfillable( order: Order, orderPrice: Price, - executionPrice: Price + executionPrice: Price, ): boolean { // Calculate the percentage of the current price in regards to the order price const percentageDifference = ONE_HUNDRED_PERCENT.subtract(executionPrice.divide(orderPrice)) @@ -143,7 +147,7 @@ export function isOrderUnfillable( orderPrice.toSignificant(10), executionPrice.toSignificant(10), `${percentageDifference.toFixed(4)}%`, - percentageDifference.greaterThan(OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE) + percentageDifference.greaterThan(OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE), ) // Example. Consider the pair X-Y: @@ -175,7 +179,7 @@ export function getOrderMarketPrice(order: Order, quotedAmount: string, feeAmoun order.outputToken, // For sell orders, the market price has the fee subtracted from the sell amount JSBI.subtract(JSBI.BigInt(remainingAmount), JSBI.BigInt(feeAmount)), - quotedAmount + quotedAmount, ) } @@ -187,7 +191,42 @@ export function getOrderMarketPrice(order: Order, quotedAmount: string, feeAmoun * 5% to level out the fee amount changes */ const EXECUTION_PRICE_FEE_COEFFICIENT = new Percent(5, 100) +const FEE_AMOUNT_MULTIPLIER = 1_000 +/** + * Calculates the estimated execution price based on order params, before the order is placed + * + * @param order undefined // there's no order when calling this way + * @param fillPrice The current market price + * @param fee The estimated fee in inputToken atoms, as string + * @param inputAmount The input amount + * @param outputAmount The output amount + * @param kind The order kind + * @param fullAppData The full app data (for calculating the partner fee) + * @param isPartiallyFillable Whether the order is partially fillable + */ +export function getEstimatedExecutionPrice( + order: undefined, + fillPrice: Price, + fee: string, + inputAmount: CurrencyAmount, + outputAmount: CurrencyAmount, + kind: OrderKind, + fullAppData: EnrichedOrder['fullAppData'], + isPartiallyFillable: boolean, +): Price | null +/** + * Calculates the estimated execution price for an order + * + * @param order Tokens and amounts information, plus whether partially fillable + * @param fillPrice AKA MarketPrice + * @param fee Estimated fee in inputToken atoms, as string + */ +export function getEstimatedExecutionPrice( + order: Order | ParsedOrder, + fillPrice: Price, + fee: string, +): Price | null /** * Get estimated execution price * @@ -199,7 +238,7 @@ const EXECUTION_PRICE_FEE_COEFFICIENT = new Percent(5, 100) * * `Kind` Fill Type (FoC, Partial) * * And the Market conditions: - * * `FP` Fill Price (Volume sensitive) (aka Market Price) + * * `FP` Fill Price (No longer volume sensitive) (aka Market Price, Spot price) * * `BOP` Best Offer Price (Non-volume sensitive) (aka Spot Price) * * `F` Fee to execute the order (in sell tokens) * @@ -215,90 +254,149 @@ const EXECUTION_PRICE_FEE_COEFFICIENT = new Percent(5, 100) * * IF (Kind = Partial) * EEP = MAX(FEP, FBOP) - * - * - * @param order Tokens and amounts information, plus whether partially fillable - * @param fillPrice AKA MarketPrice - * @param fee Estimated fee in inputToken atoms, as string */ export function getEstimatedExecutionPrice( - order: Order, + order: Order | ParsedOrder | undefined, fillPrice: Price, - fee: string + fee: string, + inputAmount?: CurrencyAmount, + outputAmount?: CurrencyAmount, + kind?: OrderKind, + fullAppData?: EnrichedOrder['fullAppData'], + isPartiallyFillable?: boolean, ): Price | null { - // Build CurrencyAmount and Price instances - const feeAmount = CurrencyAmount.fromRawAmount(order.inputToken, fee) - // Take partner fee into account when calculating the limit price - const limitPrice = getOrderLimitPriceWithPartnerFee(order) + let inputToken: Token + let outputToken: Token + let limitPrice: Price + let sellAmount: string + let partiallyFillable = isPartiallyFillable + + if (order) { + inputToken = order.inputToken + outputToken = order.outputToken + // Take partner fee into account when calculating the limit price + limitPrice = getOrderLimitPriceWithPartnerFee(order) + + if (getUiOrderType(order) === UiOrderType.SWAP) { + return limitPrice + } - if (getUiOrderType(order) === UiOrderType.SWAP) { - return limitPrice + // Parent TWAP order, ignore + if (getIsComposableCowParentOrder(order)) { + return null + } + + // Check what's left to sell, discounting the surplus, if any + sellAmount = getRemainderAmountsWithoutSurplus(order).sellAmount + partiallyFillable = order.partiallyFillable + } else { + if (!inputAmount || !outputAmount || !kind || inputAmount.equalTo(0) || outputAmount.equalTo(0)) { + return null + } + // Always the full amount + sellAmount = inputAmount.quotient.toString() + + inputToken = getWrappedToken(inputAmount.currency) + outputToken = getWrappedToken(outputAmount.currency) + + limitPrice = getOrderLimitPriceWithPartnerFee({ + inputToken, + outputToken, + sellAmount, + buyAmount: outputAmount.quotient.toString(), + kind: kind as OrderKind, + fullAppData, + }) } - // Parent TWAP order, ignore - if (getIsComposableCowParentOrder(order)) { + if (!limitPrice) { return null } - // Check what's left to sell, discounting the surplus, if any - const { sellAmount } = getRemainderAmountsWithoutSurplus(order) - const remainingSellAmount = CurrencyAmount.fromRawAmount(order.inputToken, sellAmount) + const feeAmount = CurrencyAmount.fromRawAmount(inputToken, fee) + + const remainingSellAmount = CurrencyAmount.fromRawAmount(inputToken, sellAmount) // When fee > amount, return 0 price if (!remainingSellAmount.greaterThan(ZERO_FRACTION)) { - return new Price(order.inputToken, order.outputToken, '0', '0') + return new Price(inputToken, outputToken, '1', '0') } const feeWithMargin = feeAmount.add(feeAmount.multiply(EXECUTION_PRICE_FEE_COEFFICIENT)) - const numerator = remainingSellAmount.multiply(limitPrice) - const denominator = remainingSellAmount.subtract(feeWithMargin) - // Just in case when the denominator is <= 0 after subtraction the fee - if (!denominator.greaterThan(ZERO_FRACTION)) { - return new Price(order.inputToken, order.outputToken, '0', '0') + let feasibleExecutionPrice: Price | undefined = undefined + + if (partiallyFillable) { + // If the order is partially fillable, we need to extrapolate the price based on the fee amount + // So the estimated price remains fixed regardless of the order size + feasibleExecutionPrice = extrapolatePriceBasedOnFeeAmount( + feeAmount, + remainingSellAmount, + limitPrice, + inputToken, + outputToken, + order?.id.slice(0, 6), + ) + feasibleExecutionPrice && + console.log(`getEstimatedExecutionPrice: partial fill`, { + limitPrice: limitPrice.toSignificant(10), + feasibleExecutionPrice: feasibleExecutionPrice.toSignificant(10), + fillPrice: fillPrice.toSignificant(10), + feeAmount: feeAmount.toSignificant(10), + sellAmount: remainingSellAmount.toSignificant(10), + order: order?.id.slice(0, 6) || 'form', + }) } - /** - * Example: - * Order: 100 WETH -> 182000 USDC - * Fee: 0.002 WETH - * Limit price: 182000 / 100 = 1820 USDC per 1 WETH - * - * Fee with margin: 0.002 + 5% = 0.0021 WETH - * Executes at: 182000 / (100 - 0.0021) = 1820.038 USDC per 1 WETH - */ - const feasibleExecutionPrice = new Price( - order.inputToken, - order.outputToken, - denominator.quotient, - numerator.quotient - ) + // Regular case, when the order is fill or kill OR the fill amount is smaller than the threshold set + if (feasibleExecutionPrice === undefined) { + const numerator = remainingSellAmount.multiply(limitPrice) + const denominator = remainingSellAmount.subtract(feeWithMargin) + + // Just in case when the denominator is <= 0 after subtracting the fee + if (!denominator.greaterThan(ZERO_FRACTION)) { + // numerator and denominator are inverted!!! + // https://github.com/Uniswap/sdk-core/blob/9997e88/src/entities/fractions/price.ts#L26 + return new Price(inputToken, outputToken, '1', '0') + } + + /** + * Example: + * Order: 100 WETH -> 182000 USDC + * Fee: 0.002 WETH + * Limit price: 182000 / 100 = 1820 USDC per 1 WETH + * + * Fee with margin: 0.002 + 5% = 0.0021 WETH + * Executes at: 182000 / (100 - 0.0021) = 1820.038 USDC per 1 WETH + */ + feasibleExecutionPrice = new Price(inputToken, outputToken, denominator.quotient, numerator.quotient) + + console.log(`getEstimatedExecutionPrice: fill or kill`, { + limitPrice: limitPrice.toSignificant(10), + feasibleExecutionPrice: feasibleExecutionPrice.toSignificant(10), + fillPrice: fillPrice.toSignificant(10), + feeAmount: feeAmount.toSignificant(10), + sellAmount: remainingSellAmount.toSignificant(10), + order: order?.id.slice(0, 6) || 'form', + }) + } + + // // Make the fill price a bit worse to account for the fee + const newFillPrice = + extrapolatePriceBasedOnFeeAmount(feeAmount, remainingSellAmount, fillPrice, inputToken, outputToken, order?.id) || + fillPrice + if (newFillPrice.greaterThan(feasibleExecutionPrice) && !newFillPrice.equalTo(fillPrice)) { + console.log(`getEstimatedExecutionPrice: new fill price`, { + limitPrice: limitPrice.toSignificant(10), + feasibleExecutionPrice: feasibleExecutionPrice.toSignificant(10), + fillPrice: fillPrice.toSignificant(10), + newFillPrice: newFillPrice.toSignificant(10), + order: order?.id.slice(0, 6) || 'form', + }) + } // Pick the MAX between FEP and FP - const estimatedExecutionPrice = fillPrice.greaterThan(feasibleExecutionPrice) ? fillPrice : feasibleExecutionPrice - - // TODO: remove debug statement - console.debug(`getEstimatedExecutionPrice`, { - 'Amount (A)': - remainingSellAmount.toFixed(remainingSellAmount.currency.decimals) + ' ' + remainingSellAmount.currency.symbol, - 'Fee (F)': feeAmount.toFixed(feeAmount.currency.decimals) + ' ' + feeAmount.currency.symbol, - 'Limit Price (LP)': `${limitPrice.toFixed(8)} ${limitPrice.quoteCurrency.symbol} per ${ - limitPrice.baseCurrency.symbol - } (${limitPrice.numerator.toString()}/${limitPrice.denominator.toString()})`, - 'Feasible Execution Price (FEP)': `${feasibleExecutionPrice.toFixed(18)} ${ - feasibleExecutionPrice.quoteCurrency.symbol - } per ${feasibleExecutionPrice.baseCurrency.symbol}`, - 'Fill Price (FP)': `${fillPrice.toFixed(8)} ${fillPrice.quoteCurrency.symbol} per ${ - fillPrice.baseCurrency.symbol - } (${fillPrice.numerator.toString()}/${fillPrice.denominator.toString()})`, - 'Est.Execution Price (EEP)': `${estimatedExecutionPrice.toFixed(8)} ${ - estimatedExecutionPrice.quoteCurrency.symbol - } per ${estimatedExecutionPrice.baseCurrency.symbol}`, - id: order.id.slice(0, 8), - class: order.class, - }) - - return estimatedExecutionPrice + return newFillPrice.greaterThan(feasibleExecutionPrice) ? newFillPrice : feasibleExecutionPrice } /** @@ -316,11 +414,21 @@ export function getEstimatedExecutionPrice( * In both cases, the sell and buy remainders can be returned in full * @param order */ -export function getRemainderAmountsWithoutSurplus(order: Order): { buyAmount: string; sellAmount: string } { +export function getRemainderAmountsWithoutSurplus(order: Order | ParsedOrder): { + buyAmount: string + sellAmount: string +} { const sellRemainder = getRemainderAmount(OrderKind.SELL, order) const buyRemainder = getRemainderAmount(OrderKind.BUY, order) - const { amount: surplusAmountBigNumber } = getOrderSurplus(order) + let surplusAmountBigNumber: BigNumber + if ('executionData' in order) { + // ParsedOrder + surplusAmountBigNumber = order.executionData.surplusAmount + } else { + // Order + surplusAmountBigNumber = getOrderSurplus(order).amount + } if (surplusAmountBigNumber.isZero()) { return { sellAmount: sellRemainder, buyAmount: buyRemainder } @@ -348,22 +456,68 @@ export function getRemainderAmountsWithoutSurplus(order: Order): { buyAmount: st * @param kind The kind of remainder * @param order The order object */ -export function getRemainderAmount(kind: OrderKind, order: Order): string { - const { sellAmountBeforeFee, buyAmount, apiAdditionalInfo } = order +export function getRemainderAmount(kind: OrderKind, order: Order | ParsedOrder): string { + const buyAmount = order.buyAmount.toString() + let sellAmount: string + let executedSellAmount: string | undefined + let executedBuyAmount: string | undefined + + if ('executionData' in order) { + // ParsedOrder + sellAmount = order.sellAmount + executedSellAmount = order.executionData.executedSellAmount.toString() + executedBuyAmount = order.executionData.executedBuyAmount.toString() + } else { + // Order + sellAmount = order.sellAmountBeforeFee.toString() + executedSellAmount = order.apiAdditionalInfo?.executedSellAmountBeforeFees + executedBuyAmount = order.apiAdditionalInfo?.executedBuyAmount + } - const fullAmount = isSellOrder(kind) ? sellAmountBeforeFee.toString() : buyAmount.toString() + const fullAmount = isSellOrder(kind) ? sellAmount : buyAmount - if (!apiAdditionalInfo) { + if (!executedSellAmount || !executedBuyAmount || executedSellAmount === '0' || executedBuyAmount === '0') { return fullAmount } - const { executedSellAmountBeforeFees, executedBuyAmount } = apiAdditionalInfo - - const executedAmount = JSBI.BigInt((isSellOrder(kind) ? executedSellAmountBeforeFees : executedBuyAmount) || 0) + const executedAmount = JSBI.BigInt((isSellOrder(kind) ? executedSellAmount : executedBuyAmount) || 0) return JSBI.subtract(JSBI.BigInt(fullAmount), executedAmount).toString() } +function extrapolatePriceBasedOnFeeAmount( + feeAmount: CurrencyAmount, + remainingSellAmount: CurrencyAmount, + limitPrice: Price, + inputToken: Token, + outputToken: Token, + id: string | undefined, +): Price | undefined { + // Use FEE_AMOUNT_MULTIPLIER times fee amount as the new sell amount + const newSellAmount = feeAmount.multiply(JSBI.BigInt(FEE_AMOUNT_MULTIPLIER)) + // Only use this method if the new sell amount is smaller than the remaining sell amount + if (remainingSellAmount.greaterThan(newSellAmount)) { + // Quote the buy amount using the existing limit price + const buyAmount = limitPrice.quote(newSellAmount) + const feasibleExecutionPrice = new Price( + inputToken, + outputToken, + newSellAmount.subtract(feeAmount).quotient, // TODO: should we use the fee with margin here? + buyAmount.quotient, + ) + console.log(`getEstimatedExecutionPrice: extrapolatePriceBasedOnFeeAmount`, { + limitPrice: limitPrice.toSignificant(10), + feasibleExecutionPrice: feasibleExecutionPrice.toSignificant(10), + feeAmount: feeAmount.toSignificant(10), + sellAmount: remainingSellAmount.toSignificant(10), + newSellAmount: newSellAmount.toSignificant(10), + order: id || 'form', + }) + return feasibleExecutionPrice + } + return undefined +} + export function partialOrderUpdate({ chainId, order, isSafeWallet }: UpdateOrderParams, dispatch: AppDispatch): void { const params: UpdateOrderParamsAction = { chainId, @@ -378,14 +532,16 @@ export function partialOrderUpdate({ chainId, order, isSafeWallet }: UpdateOrder } export function getOrderVolumeFee( - fullAppData: EnrichedOrder['fullAppData'] + fullAppData: EnrichedOrder['fullAppData'], ): LatestAppDataDocVersion['metadata']['partnerFee'] | undefined { const appData = decodeAppData(fullAppData) as LatestAppDataDocVersion return appData?.metadata?.partnerFee } -export function getOrderLimitPriceWithPartnerFee(order: Order | ParsedOrder): Price { +type LimitPriceOrder = Pick + +export function getOrderLimitPriceWithPartnerFee(order: LimitPriceOrder): Price { const inputAmount = CurrencyAmount.fromRawAmount(order.inputToken, order.sellAmount.toString()) const outputAmount = CurrencyAmount.fromRawAmount(order.outputToken, order.buyAmount.toString()) @@ -393,7 +549,7 @@ export function getOrderLimitPriceWithPartnerFee(order: Order | ParsedOrder): Pr order.fullAppData, inputAmount, outputAmount, - isSellOrder(order.kind) + isSellOrder(order.kind), ) return buildPriceFromCurrencyAmounts(inputCurrencyAmount, outputCurrencyAmount) @@ -403,7 +559,7 @@ function getOrderAmountsWithPartnerFee( fullAppData: EnrichedOrder['fullAppData'], sellAmount: CurrencyAmount, buyAmount: CurrencyAmount, - isSellOrder: boolean + isSellOrder: boolean, ): { inputCurrencyAmount: CurrencyAmount; outputCurrencyAmount: CurrencyAmount } { const volumeFee = getOrderVolumeFee(fullAppData) diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/ExecutionPriceUpdater/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/updaters/ExecutionPriceUpdater/index.tsx index 96f43f9322..7386f4308c 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/updaters/ExecutionPriceUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/ExecutionPriceUpdater/index.tsx @@ -1,27 +1,70 @@ import { useAtomValue, useSetAtom } from 'jotai' -import { useEffect } from 'react' +import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils' + +import { getEstimatedExecutionPrice } from 'legacy/state/orders/utils' + +import { useAppData } from 'modules/appData' import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState' import { executionPriceAtom } from 'modules/limitOrders/state/executionPriceAtom' import { limitRateAtom } from 'modules/limitOrders/state/limitRateAtom' -import { calculateExecutionPrice } from 'utils/orderUtils/calculateExecutionPrice' +import { useSafeEffect, useSafeMemo } from 'common/hooks/useSafeMemo' + +import { limitOrdersSettingsAtom } from '../../state/limitOrdersSettingsAtom' export function ExecutionPriceUpdater() { const { marketRate, feeAmount } = useAtomValue(limitRateAtom) const { inputCurrencyAmount, outputCurrencyAmount, orderKind } = useLimitOrdersDerivedState() + const { partialFillsEnabled } = useAtomValue(limitOrdersSettingsAtom) const setExecutionPrice = useSetAtom(executionPriceAtom) + const { fullAppData } = useAppData() || {} + + const inputToken = inputCurrencyAmount?.currency && getWrappedToken(inputCurrencyAmount.currency) + const outputToken = outputCurrencyAmount?.currency && getWrappedToken(outputCurrencyAmount.currency) + + const marketPrice = useSafeMemo(() => { + try { + if (marketRate && !marketRate.equalTo('0') && inputToken && outputToken) { + return FractionUtils.toPrice(marketRate, inputToken, outputToken) + } + } catch (e) { + console.error( + `[ExecutionPriceUpdater] Failed to parse the market price for ${inputToken?.address} and ${outputToken?.address}`, + marketRate?.numerator.toString(), + marketRate?.denominator.toString(), + e, + ) + } + return null + }, [marketRate, inputToken, outputToken]) + + const fee = feeAmount?.quotient.toString() + + const price = + marketPrice && + fee && + inputCurrencyAmount && + outputCurrencyAmount && + getEstimatedExecutionPrice( + undefined, + marketPrice, + fee, + inputCurrencyAmount, + outputCurrencyAmount, + orderKind, + fullAppData, + partialFillsEnabled, + ) - const price = calculateExecutionPrice({ - inputCurrencyAmount, - outputCurrencyAmount, - feeAmount, - marketRate, - orderKind, - }) + useSafeEffect(() => { + // Reset execution price when input or output token changes + setExecutionPrice(null) + }, [inputToken, outputToken, setExecutionPrice]) - useEffect(() => { - setExecutionPrice(price) + useSafeEffect(() => { + // Set execution price when price is calculated and it's valid + price && price.greaterThan(0) && setExecutionPrice(price) }, [price, setExecutionPrice]) return null diff --git a/apps/cowswap-frontend/src/modules/limitOrders/updaters/QuoteObserverUpdater/index.tsx b/apps/cowswap-frontend/src/modules/limitOrders/updaters/QuoteObserverUpdater/index.tsx index bc38cce60d..4a6d336e9c 100644 --- a/apps/cowswap-frontend/src/modules/limitOrders/updaters/QuoteObserverUpdater/index.tsx +++ b/apps/cowswap-frontend/src/modules/limitOrders/updaters/QuoteObserverUpdater/index.tsx @@ -1,15 +1,19 @@ import { useSetAtom } from 'jotai' -import { useEffect, useMemo } from 'react' +import { useMemo } from 'react' import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils' -import { Fraction, Token } from '@uniswap/sdk-core' +import { CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core' import { Nullish } from 'types' import { updateLimitRateAtom } from 'modules/limitOrders/state/limitRateAtom' import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState' +import { useTradeQuote } from 'modules/tradeQuote' import { useUsdPrice } from 'modules/usdAmount/hooks/useUsdPrice' +import { useSafeEffect } from 'common/hooks/useSafeMemo' + + export function QuoteObserverUpdater() { const state = useDerivedTradeState() @@ -23,10 +27,21 @@ export function QuoteObserverUpdater() { const { price, isLoading } = useSpotPrice(inputToken, outputToken) - useEffect(() => { + // Update market rate based on spot prices + useSafeEffect(() => { updateLimitRateState({ marketRate: price, isLoadingMarketRate: isLoading }) }, [price, isLoading, updateLimitRateState]) + const { response } = useTradeQuote() + const { quote } = response || {} + const { feeAmount: feeAmountRaw } = quote || {} + const feeAmount = inputCurrency && feeAmountRaw ? CurrencyAmount.fromRawAmount(inputCurrency, feeAmountRaw) : null + + // Update fee amount based on quote response + useSafeEffect(() => { + updateLimitRateState({ feeAmount }) + }, [feeAmount, updateLimitRateState]) + return null } diff --git a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx index 9cd1b51b12..6d482a50da 100644 --- a/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx +++ b/apps/cowswap-frontend/src/modules/ordersTable/pure/OrdersTableContainer/OrderRow/index.tsx @@ -13,8 +13,10 @@ import { Currency, CurrencyAmount, Percent, Price } from '@uniswap/sdk-core' import { Check, Clock, X, Zap } from 'react-feather' import SVG from 'react-inlinesvg' +import { Nullish } from 'types' import { OrderStatus } from 'legacy/state/orders/actions' +import { getEstimatedExecutionPrice } from 'legacy/state/orders/utils' import { PendingOrderPrices } from 'modules/orders/state/pendingOrdersPricesAtom' import { getIsEthFlowOrder } from 'modules/swap/containers/EthFlowStepper' @@ -113,7 +115,10 @@ export function OrderRow({ const { creationTime, expirationTime, status } = order const { filledPercentDisplay, executedPrice } = order.executionData const { inputCurrencyAmount, outputCurrencyAmount } = rateInfoParams - const { estimatedExecutionPrice, feeAmount } = prices || {} + const { feeAmount } = prices || {} + const estimatedExecutionPrice = useSafeMemo(() => { + return spotPrice && feeAmount && getEstimatedExecutionPrice(order, spotPrice, feeAmount.quotient.toString()) + }, [spotPrice, feeAmount, order]) const isSafeWallet = useIsSafeWallet() const showCancellationModal = useMemo(() => { @@ -157,7 +162,7 @@ export function OrderRow({ const executedPriceInverted = isInverted ? executedPrice?.invert() : executedPrice const spotPriceInverted = isInverted ? spotPrice?.invert() : spotPrice - const priceDiffs = usePricesDifference(prices, spotPrice, isInverted) + const priceDiffs = usePricesDifference(estimatedExecutionPrice, spotPrice, isInverted) const feeDifference = useFeeAmountDifference(rateInfoParams, prices) const isExecutedPriceZero = executedPriceInverted !== undefined && executedPriceInverted?.equalTo(ZERO_FRACTION) @@ -335,12 +340,12 @@ export function OrderRow({ return '-' } - if (prices && estimatedExecutionPrice) { + if (estimatedExecutionPrice && !estimatedExecutionPrice.equalTo(ZERO_FRACTION)) { return ( {!isUnfillable && - priceDiffs?.percentage && - Math.abs(Number(priceDiffs.percentage.toFixed(4))) <= PENDING_EXECUTION_THRESHOLD_PERCENTAGE ? ( + priceDiffs?.percentage && + Math.abs(Number(priceDiffs.percentage.toFixed(4))) <= PENDING_EXECUTION_THRESHOLD_PERCENTAGE ? ( >, spotPrice: OrderRowProps['spotPrice'], isInverted: boolean, ): PriceDifference { - const { estimatedExecutionPrice } = prices || {} return useSafeMemo( () => diff --git a/apps/cowswap-frontend/src/utils/orderUtils/calculateExecutionPrice.ts b/apps/cowswap-frontend/src/utils/orderUtils/calculateExecutionPrice.ts index 77fa94c15c..25adc3aab8 100644 --- a/apps/cowswap-frontend/src/utils/orderUtils/calculateExecutionPrice.ts +++ b/apps/cowswap-frontend/src/utils/orderUtils/calculateExecutionPrice.ts @@ -20,7 +20,7 @@ export interface ExecutionPriceParams { */ export function convertAmountToCurrency( amount: CurrencyAmount, - targetCurrency: Currency + targetCurrency: Currency, ): CurrencyAmount { const { numerator, denominator } = amount @@ -34,12 +34,12 @@ export function convertAmountToCurrency( const decimalsDiff = Math.abs(inputDecimals - outputDecimals) const decimalsDiffAmount = rawToTokenAmount(1, decimalsDiff) - const fixedNumenator = + const fixedNumerator = inputDecimals < outputDecimals ? JSBI.multiply(numerator, decimalsDiffAmount) : JSBI.divide(numerator, decimalsDiffAmount) - return CurrencyAmount.fromFractionalAmount(targetCurrency, fixedNumenator, denominator) + return CurrencyAmount.fromFractionalAmount(targetCurrency, fixedNumerator, denominator) } export function calculateExecutionPrice(params: ExecutionPriceParams): Price | null { @@ -62,7 +62,7 @@ export function calculateExecutionPrice(params: ExecutionPriceParams): Price { expect(JSBI.toNumber(simplified.numerator)).toBe(1) expect(JSBI.toNumber(simplified.denominator)).toBe(3) }) + it('should avoid division by 0', () => { + const fraction = new Fraction(JSBI.BigInt(0), JSBI.BigInt(0)) + const simplified = FractionUtils.simplify(fraction) + expect(JSBI.toNumber(simplified.numerator)).toBe(0) + expect(JSBI.toNumber(simplified.denominator)).toBe(1) + }) }) }) diff --git a/libs/common-utils/src/fractionUtils.ts b/libs/common-utils/src/fractionUtils.ts index fbae358369..ad9d3fb84b 100644 --- a/libs/common-utils/src/fractionUtils.ts +++ b/libs/common-utils/src/fractionUtils.ts @@ -193,6 +193,11 @@ function reduce(fraction: Fraction): Fraction { let numerator = fraction.numerator let denominator = fraction.denominator let rest: JSBI + + if (JSBI.equal(denominator, ZERO)) { + return new Fraction(JSBI.BigInt(0), JSBI.BigInt(1)) + } + while (JSBI.notEqual(denominator, ZERO)) { rest = JSBI.remainder(numerator, denominator) numerator = denominator