From e42e3444e61d3b2756d3bd7ce4b8f891c7f2a629 Mon Sep 17 00:00:00 2001 From: w3stside Date: Thu, 2 Dec 2021 07:22:17 +0100 Subject: [PATCH 1/3] export debounce time and make exactout logic --- .../hooks/usePriceImpact/useQuoteAndSwap.ts | 25 +++++++++++++++---- src/custom/state/price/updater.ts | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/custom/hooks/usePriceImpact/useQuoteAndSwap.ts b/src/custom/hooks/usePriceImpact/useQuoteAndSwap.ts index 7a4d48fad..7389c4f45 100644 --- a/src/custom/hooks/usePriceImpact/useQuoteAndSwap.ts +++ b/src/custom/hooks/usePriceImpact/useQuoteAndSwap.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { OrderKind } from '@gnosis.pm/gp-v2-contracts' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { useTradeExactInWithFee } from 'state/swap/extension' +import { useTradeExactInWithFee, useTradeExactOutWithFee } from 'state/swap/extension' import { QuoteInformationObject } from 'state/price/reducer' import { useWalletInfo } from 'hooks/useWalletInfo' @@ -23,12 +23,17 @@ type ExactInSwapParams = { quote: QuoteInformationObject | undefined } +type ExactOutSwapParams = Omit & { + inputCurrency: Currency | undefined +} + type GetQuoteParams = { amountAtoms: string | undefined sellToken?: string | null buyToken?: string | null fromDecimals?: number toDecimals?: number + kind: OrderKind } type FeeQuoteParamsWithError = FeeQuoteParams & { error?: QuoteError } @@ -40,6 +45,7 @@ export function useCalculateQuote(params: GetQuoteParams) { buyToken, fromDecimals = DEFAULT_DECIMALS, toDecimals = DEFAULT_DECIMALS, + kind, } = params const { chainId: preChain } = useActiveWeb3React() const { account } = useWalletInfo() @@ -58,8 +64,7 @@ export function useCalculateQuote(params: GetQuoteParams) { amount, sellToken, buyToken, - // B > A Trade is always a sell - kind: OrderKind.SELL, + kind, fromDecimals, toDecimals, // TODO: check @@ -100,13 +105,13 @@ export function useCalculateQuote(params: GetQuoteParams) { setLocalQuote(quoteError) }) .finally(() => setLoading(false)) - }, [amount, account, preChain, buyToken, sellToken, toDecimals, fromDecimals]) + }, [amount, account, preChain, buyToken, sellToken, toDecimals, fromDecimals, kind]) return { quote, loading, setLoading } } // calculates a new Quote and inverse swap values -export default function useExactInSwap({ quote, outputCurrency, parsedAmount }: ExactInSwapParams) { +export function useExactInSwap({ quote, outputCurrency, parsedAmount }: ExactInSwapParams) { const bestTradeExactIn = useTradeExactInWithFee({ parsedAmount, outputCurrency, @@ -115,3 +120,13 @@ export default function useExactInSwap({ quote, outputCurrency, parsedAmount }: return bestTradeExactIn } + +export function useExactOutSwap({ quote, inputCurrency, parsedAmount }: ExactOutSwapParams) { + const bestTradeExactOut = useTradeExactOutWithFee({ + parsedAmount, + inputCurrency, + quote, + }) + + return bestTradeExactOut +} diff --git a/src/custom/state/price/updater.ts b/src/custom/state/price/updater.ts index 78d89fa95..4a159e4a3 100644 --- a/src/custom/state/price/updater.ts +++ b/src/custom/state/price/updater.ts @@ -19,7 +19,7 @@ import useDebounce from 'hooks/useDebounce' import useIsOnline from 'hooks/useIsOnline' import { QuoteInformationObject } from './reducer' -const DEBOUNCE_TIME = 350 +export const DEBOUNCE_TIME = 350 const REFETCH_CHECK_INTERVAL = 10000 // Every 10s const RENEW_FEE_QUOTES_BEFORE_EXPIRATION_TIME = 30000 // Will renew the quote if there's less than 30 seconds left for the quote to expire const WAITING_TIME_BETWEEN_EQUAL_REQUESTS = 5000 // Prevents from sending the same request to often (max, every 5s) From eb1d5e57e0dc0dc116aaef3cddc2c03710a03e33 Mon Sep 17 00:00:00 2001 From: w3stside Date: Thu, 2 Dec 2021 07:24:36 +0100 Subject: [PATCH 2/3] core inverse swap logic --- .../usePriceImpact/useFallbackPriceImpact.ts | 125 ++++++------------ 1 file changed, 44 insertions(+), 81 deletions(-) diff --git a/src/custom/hooks/usePriceImpact/useFallbackPriceImpact.ts b/src/custom/hooks/usePriceImpact/useFallbackPriceImpact.ts index 84dfab4f6..33c48af45 100644 --- a/src/custom/hooks/usePriceImpact/useFallbackPriceImpact.ts +++ b/src/custom/hooks/usePriceImpact/useFallbackPriceImpact.ts @@ -1,106 +1,74 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import { Percent } from '@uniswap/sdk-core' +import { OrderKind } from '@gnosis.pm/gp-v2-contracts' -import { useSwapState } from 'state/swap/hooks' +import { tryParseAmount, useSwapState } from 'state/swap/hooks' import { Field } from 'state/swap/actions' -import { useGetQuoteAndStatus } from 'state/price/hooks' +import { QuoteInformationObject } from 'state/price/reducer' +import { QuoteError } from 'state/price/actions' +import { DEBOUNCE_TIME } from 'state/price/updater' -import useExactInSwap, { useCalculateQuote } from './useQuoteAndSwap' import { FallbackPriceImpactParams } from './commonTypes' import { calculateFallbackPriceImpact, FeeQuoteParams } from 'utils/price' -import TradeGp from 'state/swap/TradeGp' -import { useActiveWeb3React } from 'hooks/web3' -import { QuoteInformationObject } from 'state/price/reducer' -import { QuoteError } from 'state/price/actions' -type SwapParams = { trade?: TradeGp; sellToken?: string | null; buyToken?: string | null } +import { useCalculateQuote, useExactInSwap, useExactOutSwap } from 'hooks/usePriceImpact/useQuoteAndSwap' +import { useCurrency } from 'hooks/Tokens' +import useDebounce from 'hooks/useDebounce' function _isQuoteValid(quote: QuoteInformationObject | FeeQuoteParams | undefined): quote is QuoteInformationObject { return Boolean(quote && 'lastCheck' in quote) } -function _calculateSwapParams(isExactIn: boolean, { trade, sellToken, buyToken }: SwapParams) { - if (!trade) return undefined - - if (isExactIn) { - return { - outputCurrency: trade.inputAmount.currency, - // Run inverse (B > A) sell trade - sellToken: buyToken, - buyToken: sellToken, - fromDecimals: trade.outputAmount.currency.decimals, - toDecimals: trade.inputAmount.currency.decimals, - } - } else { - // First trade was a buy order - // we need to use the same order but make a sell order - return { - outputCurrency: trade.outputAmount.currency, - // on buy orders we dont inverse it - sellToken, - buyToken, - fromDecimals: trade.inputAmount.currency.decimals, - toDecimals: trade.outputAmount.currency.decimals, - } - } -} - -function _calculateParsedAmount(trade: TradeGp | undefined, isExactIn: boolean, shouldCalculate: boolean) { - if (!shouldCalculate || !trade) return undefined - // First trade was a sell order, we need to make a new sell order using the - // first trade's output amount - const amount = isExactIn ? trade.outputAmount : trade.inputAmount - - return amount -} - export default function useFallbackPriceImpact({ abTrade, fiatPriceImpact }: FallbackPriceImpactParams) { const { - typedValue, + typedValue: unbouncedTypedValue, INPUT: { currencyId: sellToken }, OUTPUT: { currencyId: buyToken }, independentField, } = useSwapState() - const isExactIn = independentField === Field.INPUT - const { chainId } = useActiveWeb3React() - // Should we even calc this? Check if fiatPriceImpact exists + const typedValue = useDebounce(unbouncedTypedValue, DEBOUNCE_TIME) + const shouldCalculate = !Boolean(fiatPriceImpact) - // Calculate the necessary params to get the inverse trade impact - const { parsedAmount, outputCurrency, ...swapQuoteParams } = useMemo( - () => ({ - parsedAmount: _calculateParsedAmount(abTrade, isExactIn, shouldCalculate), - ..._calculateSwapParams(isExactIn, { trade: abTrade, sellToken, buyToken }), - }), - [abTrade, buyToken, sellToken, shouldCalculate, isExactIn] - ) + const inputCurrency = useCurrency(sellToken) + const outputCurrency = useCurrency(buyToken) + + const isExactIn = independentField === Field.INPUT + const parsedAmount = shouldCalculate + ? tryParseAmount(typedValue, (isExactIn ? inputCurrency : outputCurrency) ?? undefined) + : undefined const { quote, loading: loading } = useCalculateQuote({ amountAtoms: parsedAmount?.quotient.toString(), - ...swapQuoteParams, + kind: isExactIn ? OrderKind.BUY : OrderKind.SELL, + sellToken: buyToken, + buyToken: sellToken, + fromDecimals: outputCurrency?.decimals, + toDecimals: inputCurrency?.decimals, + }) + + const baTradeIn = useExactInSwap({ + quote: _isQuoteValid(quote) ? quote : undefined, + parsedAmount: !isExactIn ? parsedAmount : undefined, + outputCurrency: inputCurrency ?? undefined, }) - // we calculate the trade going B > A - // using the output values from the original A > B trade - const baTrade = useExactInSwap({ - // if impact, give undefined and dont compute swap - // the amount traded now is the A > B output amount without fees - // TODO: is this the amount with or without fees? + const baTradeOut = useExactOutSwap({ quote: _isQuoteValid(quote) ? quote : undefined, - parsedAmount, - outputCurrency, + parsedAmount: isExactIn ? parsedAmount : undefined, + inputCurrency: outputCurrency ?? undefined, }) + const baTrade = baTradeIn || baTradeOut + const [impact, setImpact] = useState() const [error, setError] = useState() - // we set price impact to undefined when loading a NEW quote only - const { isGettingNewQuote } = useGetQuoteAndStatus({ token: sellToken, chainId }) - // primitive values to use as dependencies const abIn = abTrade?.inputAmount.quotient.toString() const abOut = abTrade?.outputAmount.quotient.toString() + const baIn = baTrade?.inputAmount.quotient.toString() const baOut = baTrade?.outputAmount.quotient.toString() const quoteError = quote?.error @@ -109,8 +77,13 @@ export default function useFallbackPriceImpact({ abTrade, fiatPriceImpact }: Fal if (quoteError) { setImpact(undefined) setError(quoteError) - } else if (!loading && abIn && abOut && baOut) { - const impact = calculateFallbackPriceImpact(isExactIn ? abIn : abOut, baOut) + } else if (!loading && abIn && abOut && baIn && baOut) { + let impact = undefined + if (isExactIn) { + impact = calculateFallbackPriceImpact(abOut, baIn) + } else { + impact = calculateFallbackPriceImpact(abIn, baOut) + } setImpact(impact) setError(undefined) } else { @@ -118,17 +91,7 @@ export default function useFallbackPriceImpact({ abTrade, fiatPriceImpact }: Fal setImpact(undefined) setError(undefined) } - }, [abIn, abOut, baOut, quoteError, isExactIn, loading]) - - // on changes to typedValue, we hide impact - // quote loading so we hide impact - // prevents lingering calculations and jumping impacts - useEffect(() => { - if (typedValue || isGettingNewQuote) { - setImpact(undefined) - setError(undefined) - } - }, [isGettingNewQuote, typedValue]) + }, [abIn, abOut, baIn, baOut, quoteError, isExactIn, loading, unbouncedTypedValue, sellToken, buyToken]) return { impact, error, loading } } From e8b3831a29e578ff185290e08a3718c07d449fe2 Mon Sep 17 00:00:00 2001 From: w3stside Date: Thu, 2 Dec 2021 11:17:43 +0100 Subject: [PATCH 3/3] aba test cases --- src/custom/utils/calculatePriceImpact.test.ts | 105 +++++++++++------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/src/custom/utils/calculatePriceImpact.test.ts b/src/custom/utils/calculatePriceImpact.test.ts index 08155e54f..4127cd031 100644 --- a/src/custom/utils/calculatePriceImpact.test.ts +++ b/src/custom/utils/calculatePriceImpact.test.ts @@ -1,53 +1,74 @@ import { ChainId, WETH } from '@uniswap/sdk' -import { CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' -import BigNumber from 'bignumber.js' +import { Token } from '@uniswap/sdk-core' import { parseUnits } from 'ethers/lib/utils' -function _calculateAbaPriceImpact(initialValue: string, finalValue: string) { - const initialValueBn = new BigNumber(initialValue) - const finalValueBn = new BigNumber(finalValue) - // TODO: use correct formula - // ((IV - FV) / IV / 2) * 100 - const [numerator, denominator] = initialValueBn.minus(finalValueBn).div(initialValueBn).div('2').toFraction() - - return new Percent(numerator.toString(), denominator.toString()) -} +import { calculateFallbackPriceImpact } from './price' const WETH_MAINNET = new Token(ChainId.MAINNET, WETH[1].address, 18) const DAI_MAINNET = new Token(ChainId.MAINNET, '0x6b175474e89094c44da98b954eedeac495271d0f', 18) -describe('A > B > A Price Impact', () => { - const AB_IN = parseUnits('1', WETH_MAINNET.decimals).toString() - const AB_OUT = parseUnits('1000', DAI_MAINNET.decimals).toString() - - const abIn = CurrencyAmount.fromRawAmount(WETH_MAINNET, AB_IN) - const abOut = CurrencyAmount.fromRawAmount(DAI_MAINNET, AB_OUT) - - describe('[SELL] WETH --> DAI', () => { - it('A > B > A SELL return proper price impact', () => { - // GIVEN a 1 WETH >> 1000 DAI AB Trade - // GIVEN a 1000 DAI >> 0.5 WETH BA Trade - const BA_OUT = parseUnits('0.5', WETH_MAINNET.decimals).toString() - const baOut = CurrencyAmount.fromRawAmount(WETH_MAINNET, BA_OUT) - // THEN we expect price impact to be 25 - // (1 - 0.5) / 1 / 2 * 100 - // BUY order = last param TRUE - const abaImpact = _calculateAbaPriceImpact(abIn.quotient.toString(), baOut.quotient.toString()) - expect(abaImpact.toSignificant(2)).toEqual('25') - }) - }) +const ABA_CASES = [ + { + initialValue: parseUnits('80', DAI_MAINNET.decimals).toString(), + finalValue: parseUnits('56', DAI_MAINNET.decimals).toString(), + expectation: '15', + description: '[SELL] 100 WETH > 80 DAI > 56 WETH', + }, + { + initialValue: parseUnits('0.48', WETH_MAINNET.decimals).toString(), + finalValue: parseUnits('0.61', WETH_MAINNET.decimals).toString(), + expectation: '-14', + description: '[BUY] 700 DAI > 0.48 WETH > 0.61 WETH', + }, + { + initialValue: parseUnits('80', DAI_MAINNET.decimals).toString(), + finalValue: parseUnits('56', DAI_MAINNET.decimals).toString(), + expectation: '15', + description: '[SELL] 100 WETH > 80 DAI > 56 WETH', + }, +] + +// describe('A > B > A Price Impact', () => { +// const AB_IN = parseUnits('1', WETH_MAINNET.decimals).toString() +// const AB_OUT = parseUnits('1000', DAI_MAINNET.decimals).toString() - describe('[BUY] DAI --> WETH', () => { - it('A > B > A BUY returns proper price impact', () => { - // GIVEN a 1000 DAI >> 1 WETH BUY AB Trade - // GIVEN a 1 WETH >> 800 WETH SELL BA Trade - const BA_OUT = parseUnits('800', DAI_MAINNET.decimals).toString() - const baOut = CurrencyAmount.fromRawAmount(DAI_MAINNET, BA_OUT) - // THEN we expect price impact to be 25 - // (1000 - 800) / 1000 / 2 * 100 = 10 - // BUY order = last param FALSE - const abaImpact = _calculateAbaPriceImpact(abOut.quotient.toString(), baOut.quotient.toString()) - expect(abaImpact.toSignificant(2)).toEqual('10') +// const abIn = CurrencyAmount.fromRawAmount(WETH_MAINNET, AB_IN) +// const abOut = CurrencyAmount.fromRawAmount(DAI_MAINNET, AB_OUT) + +// describe('[SELL] WETH --> DAI', () => { +// it('A > B > A SELL return proper price impact', () => { +// // GIVEN a 1 WETH >> 1000 DAI AB Trade +// // GIVEN a 1000 DAI >> 0.5 WETH BA Trade +// const BA_OUT = parseUnits('0.5', WETH_MAINNET.decimals).toString() +// const baOut = CurrencyAmount.fromRawAmount(WETH_MAINNET, BA_OUT) +// // THEN we expect price impact to be 25 +// // (1 - 0.5) / 1 / 2 * 100 +// // BUY order = last param TRUE +// const abaImpact = _calculateAbaPriceImpact(abIn.quotient.toString(), baOut.quotient.toString()) +// expect(abaImpact.toSignificant(2)).toEqual('25') +// }) +// }) + +// describe('[BUY] DAI --> WETH', () => { +// it('A > B > A BUY returns proper price impact', () => { +// // GIVEN a 1000 DAI >> 1 WETH BUY AB Trade +// // GIVEN a 1 WETH >> 800 WETH SELL BA Trade +// const BA_OUT = parseUnits('800', DAI_MAINNET.decimals).toString() +// const baOut = CurrencyAmount.fromRawAmount(DAI_MAINNET, BA_OUT) +// // THEN we expect price impact to be 25 +// // (1000 - 800) / 1000 / 2 * 100 = 10 +// // BUY order = last param FALSE +// const abaImpact = _calculateAbaPriceImpact(abOut.quotient.toString(), baOut.quotient.toString()) +// expect(abaImpact.toSignificant(2)).toEqual('10') +// }) +// }) +// }) + +describe('A > B > A Price Impact', () => { + ABA_CASES.forEach(({ initialValue, finalValue, expectation, description }) => { + it(description, () => { + const abaImpact = calculateFallbackPriceImpact(initialValue.toString(), finalValue.toString()) + expect(abaImpact.toSignificant(2)).toEqual(expectation) }) }) })