diff --git a/apps/cowswap-frontend/src/common/hooks/useSafeMemo.ts b/apps/cowswap-frontend/src/common/hooks/useSafeMemo.ts index 31f0c824ed..fc9864a4f2 100644 --- a/apps/cowswap-frontend/src/common/hooks/useSafeMemo.ts +++ b/apps/cowswap-frontend/src/common/hooks/useSafeMemo.ts @@ -8,7 +8,7 @@ export function useSafeDeps(deps: unknown[]): unknown[] { if (dep instanceof Token) return dep.address.toLowerCase() if (dep instanceof CurrencyAmount) return dep.toExact() + dep.currency.symbol + dep.currency.chainId if (dep instanceof Percent) return dep.toFixed(6) - if (dep instanceof Price) return dep.toFixed(12) + dep.baseCurrency.symbol + dep.quoteCurrency.symbol + if (dep instanceof Price) return dep.toSignificant(10) + dep.baseCurrency.symbol + dep.quoteCurrency.symbol return dep }) diff --git a/apps/cowswap-frontend/src/legacy/state/orders/utils.ts b/apps/cowswap-frontend/src/legacy/state/orders/utils.ts index b84691ec50..7f80e64776 100644 --- a/apps/cowswap-frontend/src/legacy/state/orders/utils.ts +++ b/apps/cowswap-frontend/src/legacy/state/orders/utils.ts @@ -1,6 +1,6 @@ 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' @@ -12,7 +12,6 @@ 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 { Order, updateOrder, UpdateOrderParams as UpdateOrderParamsAction } from './actions' import { OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE } from './consts' @@ -35,7 +34,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 +100,7 @@ export function classifyOrder( | 'kind' | 'signingScheme' | 'status' - > | null + > | null, ): OrderTransitionStatus { if (!order) { console.debug(`[state::orders::classifyOrder] unknown order`) @@ -133,7 +135,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 +145,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 +177,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, ) } @@ -221,32 +223,75 @@ const EXECUTION_PRICE_FEE_COEFFICIENT = new Percent(5, 100) * @param fillPrice AKA MarketPrice * @param fee Estimated fee in inputToken atoms, as string */ +export function getEstimatedExecutionPrice( + order: undefined, // there's no order when calling this way + fillPrice: Price, + fee: string, + inputAmount: CurrencyAmount, + outputAmount: CurrencyAmount, + kind: OrderKind, + fullAppData: EnrichedOrder['fullAppData'], +): Price | null export function getEstimatedExecutionPrice( order: Order, fillPrice: Price, - fee: string + fee: string, +): Price | null +export function getEstimatedExecutionPrice( + order: Order | undefined, + fillPrice: Price, + fee: string, + inputAmount?: CurrencyAmount, + outputAmount?: CurrencyAmount, + kind?: OrderKind, + fullAppData?: EnrichedOrder['fullAppData'], ): 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 + + if (order) { + inputToken = order.inputToken + outputToken = order.outputToken + 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 + } - // Parent TWAP order, ignore - if (getIsComposableCowParentOrder(order)) { - return null + sellAmount = getRemainderAmountsWithoutSurplus(order).sellAmount + } else { + 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, + }) } + const feeAmount = CurrencyAmount.fromRawAmount(inputToken, fee) + + // Build CurrencyAmount and Price instances + // const feeAmount = CurrencyAmount.fromRawAmount(order.inputToken, feeAmount) + // Take partner fee into account when calculating the limit price + // Check what's left to sell, discounting the surplus, if any - const { sellAmount } = getRemainderAmountsWithoutSurplus(order) - const remainingSellAmount = CurrencyAmount.fromRawAmount(order.inputToken, sellAmount) + 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, '0', '0') } const feeWithMargin = feeAmount.add(feeAmount.multiply(EXECUTION_PRICE_FEE_COEFFICIENT)) @@ -255,7 +300,7 @@ export function getEstimatedExecutionPrice( // 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') + return new Price(inputToken, outputToken, '0', '0') } /** @@ -267,37 +312,11 @@ export function getEstimatedExecutionPrice( * 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 - ) + const feasibleExecutionPrice = new Price(inputToken, outputToken, denominator.quotient, numerator.quotient) // 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 } @@ -378,14 +397,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 +414,7 @@ export function getOrderLimitPriceWithPartnerFee(order: Order | ParsedOrder): Pr order.fullAppData, inputAmount, outputAmount, - isSellOrder(order.kind) + isSellOrder(order.kind), ) return buildPriceFromCurrencyAmounts(inputCurrencyAmount, outputCurrencyAmount) @@ -403,7 +424,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..b197cde0f1 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,45 @@ 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 } from 'common/hooks/useSafeMemo' export function ExecutionPriceUpdater() { const { marketRate, feeAmount } = useAtomValue(limitRateAtom) const { inputCurrencyAmount, outputCurrencyAmount, orderKind } = useLimitOrdersDerivedState() const setExecutionPrice = useSetAtom(executionPriceAtom) + const { fullAppData } = useAppData() || {} + + const inputToken = inputCurrencyAmount?.currency && getWrappedToken(inputCurrencyAmount.currency) + const outputToken = outputCurrencyAmount?.currency && getWrappedToken(outputCurrencyAmount.currency) + + const marketPrice = + marketRate && inputToken && outputToken && FractionUtils.toPrice(marketRate, inputToken, outputToken) + + const fee = feeAmount?.quotient.toString() - const price = calculateExecutionPrice({ - inputCurrencyAmount, - outputCurrencyAmount, - feeAmount, - marketRate, - orderKind, - }) + const price = + marketPrice && + fee && + getEstimatedExecutionPrice( + undefined, + marketPrice, + fee, + inputCurrencyAmount, + outputCurrencyAmount, + orderKind, + fullAppData, + ) - useEffect(() => { - setExecutionPrice(price) + useSafeEffect(() => { + 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 611944dafc..0299017895 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/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