From e500772d212b13924087b52b16c77208cc549e04 Mon Sep 17 00:00:00 2001 From: Norbi <37236152+KatunaNorbert@users.noreply.github.com> Date: Wed, 18 Aug 2021 12:46:27 +0300 Subject: [PATCH] Add price impact indicator to the trade component (#765) * WIP * calculate price impact by subtracting tokens fiat values * get and use spotPrice for price impact calculation, use Decimal * set impact to 0 if input and output values are undefined * move price impact to a new component * turn price impact value color to red if grater than 5 * add tooltip to price impact and slippage * removed fiat price * change formula * remove console.log * don't block add liquidity button * typos * proper text alignment Co-authored-by: mihaisc Co-authored-by: Matthias Kretschmann --- .../organisms/AssetActions/Pool/index.tsx | 2 +- .../AssetActions/Trade/PriceImpact.module.css | 24 ++++++ .../AssetActions/Trade/PriceImpact.tsx | 65 ++++++++++++++++ .../AssetActions/Trade/Slippage.module.css | 8 +- .../organisms/AssetActions/Trade/Slippage.tsx | 12 +-- .../organisms/AssetActions/Trade/Swap.tsx | 76 ++++++++++++++++--- src/utils/subgraph.ts | 15 ++++ 7 files changed, 183 insertions(+), 19 deletions(-) create mode 100644 src/components/organisms/AssetActions/Trade/PriceImpact.module.css create mode 100644 src/components/organisms/AssetActions/Trade/PriceImpact.tsx diff --git a/src/components/organisms/AssetActions/Pool/index.tsx b/src/components/organisms/AssetActions/Pool/index.tsx index 58ed4f954..8a8c10eb9 100644 --- a/src/components/organisms/AssetActions/Pool/index.tsx +++ b/src/components/organisms/AssetActions/Pool/index.tsx @@ -367,7 +367,7 @@ export default function Pool(): ReactElement { style="primary" size="small" onClick={() => setShowAdd(true)} - disabled={isInPurgatory || !isAssetNetwork} + disabled={isInPurgatory} > Add Liquidity diff --git a/src/components/organisms/AssetActions/Trade/PriceImpact.module.css b/src/components/organisms/AssetActions/Trade/PriceImpact.module.css new file mode 100644 index 000000000..525a20fd4 --- /dev/null +++ b/src/components/organisms/AssetActions/Trade/PriceImpact.module.css @@ -0,0 +1,24 @@ +.priceImpact { + font-size: var(--font-size-small); + border-top: 1px solid var(--border-color); + margin-left: -2rem; + margin-right: -2rem; + padding: calc(var(--spacer) / 4) var(--spacer); + color: var(--color-secondary); + display: grid; + grid-template-columns: 1fr 1fr; + gap: calc(var(--spacer) / 3); +} + +.priceImpact strong { + font-weight: var(--font-weight-base); + text-align: right; +} + +.alert { + color: var(--brand-alert-red); +} + +.number { + font-weight: var(--font-weight-bold); +} diff --git a/src/components/organisms/AssetActions/Trade/PriceImpact.tsx b/src/components/organisms/AssetActions/Trade/PriceImpact.tsx new file mode 100644 index 000000000..97557e301 --- /dev/null +++ b/src/components/organisms/AssetActions/Trade/PriceImpact.tsx @@ -0,0 +1,65 @@ +import React, { ReactElement, useEffect, useState } from 'react' +import Decimal from 'decimal.js' +import Tooltip from '../../../atoms/Tooltip' +import styles from './PriceImpact.module.css' + +export default function PriceImpact({ + totalValue, + tokenAmount, + spotPrice +}: { + /// how much the user actually pays (doesn't matter witch token it is) + totalValue: string + /// how many tokens the user trades (doesn't matter witch token it is) + tokenAmount: string + /// the spot price of the traded token (doesn't matter witch token it is)) + spotPrice: string +}): ReactElement { + const [priceImpact, setPriceImpact] = useState('0') + + async function getPriceImpact( + totalValue: string, + tokenAmount: string, + spotPrice: string + ) { + const dtotalValue = new Decimal(totalValue) + const dTokenAmount = new Decimal(tokenAmount) + const dSpotPrice = new Decimal(spotPrice) + let priceImpact = Decimal.abs( + dtotalValue.div(dTokenAmount.times(dSpotPrice)).minus(1) + ).mul(100) + if (priceImpact.isNaN()) priceImpact = new Decimal(0) + return priceImpact.toDecimalPlaces(2, Decimal.ROUND_DOWN) + } + + useEffect(() => { + if (!totalValue || !tokenAmount || !spotPrice) { + setPriceImpact('0') + return + } + + async function init() { + const newPriceImpact = await getPriceImpact( + totalValue, + tokenAmount, + spotPrice + ) + setPriceImpact(newPriceImpact.toString()) + } + init() + }, [totalValue, tokenAmount, spotPrice]) + + return ( +
+ Price impact +
+ 5 && styles.alert + }`} + >{`${priceImpact}%`} + +
+
+ ) +} diff --git a/src/components/organisms/AssetActions/Trade/Slippage.module.css b/src/components/organisms/AssetActions/Trade/Slippage.module.css index b27e26683..47041a7f2 100644 --- a/src/components/organisms/AssetActions/Trade/Slippage.module.css +++ b/src/components/organisms/AssetActions/Trade/Slippage.module.css @@ -4,13 +4,15 @@ margin-left: -2rem; margin-right: -2rem; padding: calc(var(--spacer) / 4) var(--spacer); - text-align: center; color: var(--color-secondary); + display: grid; + grid-template-columns: 1fr 1fr; + gap: calc(var(--spacer) / 3); } .slippage strong { font-weight: var(--font-weight-base); - color: var(--font-color-heading); + text-align: right; } .title { @@ -25,6 +27,4 @@ .slippage select { width: fit-content; display: inline-block; - margin-left: calc(var(--spacer) / 4); - margin-right: calc(var(--spacer) / 4); } diff --git a/src/components/organisms/AssetActions/Trade/Slippage.tsx b/src/components/organisms/AssetActions/Trade/Slippage.tsx index fcf0571f6..a6ad78952 100644 --- a/src/components/organisms/AssetActions/Trade/Slippage.tsx +++ b/src/components/organisms/AssetActions/Trade/Slippage.tsx @@ -1,7 +1,8 @@ import { FormikContextType, useFormikContext } from 'formik' -import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react' +import React, { ChangeEvent, ReactElement } from 'react' import { FormTradeData, slippagePresets } from '../../../../models/FormTrade' import InputElement from '../../../atoms/Input/InputElement' +import Tooltip from '../../../atoms/Tooltip' import styles from './Slippage.module.css' export default function Slippage(): ReactElement { @@ -14,9 +15,9 @@ export default function Slippage(): ReactElement { } return ( - <> -
- Expected price impact +
+ Slippage Tolerance +
+
- +
) } diff --git a/src/components/organisms/AssetActions/Trade/Swap.tsx b/src/components/organisms/AssetActions/Trade/Swap.tsx index 5193c2bf3..16b05ee89 100644 --- a/src/components/organisms/AssetActions/Trade/Swap.tsx +++ b/src/components/organisms/AssetActions/Trade/Swap.tsx @@ -10,6 +10,7 @@ import Output from './Output' import Slippage from './Slippage' import { FormTradeData, TradeItem } from '../../../../models/FormTrade' import { useOcean } from '../../../../providers/Ocean' +import PriceImpact from './PriceImpact' export default function Swap({ ddo, @@ -47,6 +48,11 @@ export default function Swap({ validateForm }: FormikContextType = useFormikContext() + /// Values used for calculation of price impact + const [spotPrice, setSpotPrice] = useState() + const [totalValue, setTotalValue] = useState() + const [tokenAmount, setTokenAmount] = useState() + /// useEffect(() => { if (!ddo || !balance || !values || !price) return @@ -106,16 +112,63 @@ export default function Swap({ } const handleValueChange = async (name: string, value: number) => { - const newValue = - name === 'ocean' - ? values.type === 'sell' - ? await ocean.pool.getDTNeeded(price.address, value.toString()) - : await ocean.pool.getDTReceived(price.address, value.toString()) - : values.type === 'sell' - ? await ocean.pool.getOceanReceived(price.address, value.toString()) - : await ocean.pool.getOceanNeeded(price.address, value.toString()) + let tokenIn = '' + let tokenOut = '' + let newValue - setFieldValue(name === 'ocean' ? 'datatoken' : 'ocean', newValue) + if (name === 'ocean') { + if (values.type === 'sell') { + newValue = await ocean.pool.getDTNeeded(price.address, value.toString()) + + setTotalValue(newValue) + setTokenAmount(value.toString()) + + tokenIn = ddo.dataToken + tokenOut = ocean.pool.oceanAddress + } else { + newValue = await ocean.pool.getDTReceived( + price.address, + value.toString() + ) + + setTotalValue(value.toString()) + setTokenAmount(newValue) + tokenIn = ocean.pool.oceanAddress + tokenOut = ddo.dataToken + } + } else { + if (values.type === 'sell') { + newValue = await ocean.pool.getOceanReceived( + price.address, + value.toString() + ) + + setTotalValue(value.toString()) + setTokenAmount(newValue) + tokenIn = ddo.dataToken + tokenOut = ocean.pool.oceanAddress + } else { + newValue = await ocean.pool.getOceanNeeded( + price.address, + value.toString() + ) + + setTotalValue(newValue) + setTokenAmount(value.toString()) + tokenIn = ocean.pool.oceanAddress + tokenOut = ddo.dataToken + } + } + + await setFieldValue(name === 'ocean' ? 'datatoken' : 'ocean', newValue) + + const spotPrice = await ocean.pool.getSpotPrice( + price.address, + tokenIn, + tokenOut + ) + + setSpotPrice(spotPrice) validateForm() } @@ -139,6 +192,11 @@ export default function Swap({ +
) diff --git a/src/utils/subgraph.ts b/src/utils/subgraph.ts index a2c6362ab..3c30638f7 100644 --- a/src/utils/subgraph.ts +++ b/src/utils/subgraph.ts @@ -410,6 +410,21 @@ export async function getPrice(asset: DDO): Promise { return bestPrice } +export async function getSpotPrice(asset: DDO): Promise { + const poolVariables = { + datatokenAddress: asset?.dataToken.toLowerCase() + } + const queryContext = getQueryContext(Number(asset.chainId)) + + const poolPriceResponse: OperationResult = await fetchData( + AssetPoolPriceQuerry, + poolVariables, + queryContext + ) + + return poolPriceResponse.data.pools[0].spotPrice +} + export async function getAssetsBestPrices( assets: DDO[] ): Promise {