From 65045ba7b9d7dfe74247109b585bcba4876f9552 Mon Sep 17 00:00:00 2001 From: Matthias Kretschmann Date: Tue, 28 Jun 2022 14:44:32 +0100 Subject: [PATCH] put back Remove Liquidity components (#1551) * put back Remove Liquidity components * remove Pool Share Lock action --- .../AssetActions/Pool/Actions.module.css | 25 ++ .../organisms/AssetActions/Pool/Actions.tsx | 75 +++++ .../AssetActions/Pool/Add/Output.module.css | 24 ++ .../AssetActions/Pool/Add/index.module.css | 24 ++ .../AssetActions/Pool/Remove.module.css | 70 ++++ .../organisms/AssetActions/Pool/Remove.tsx | 313 ++++++++++++++++++ .../organisms/AssetActions/Pool/index.tsx | 60 +++- .../organisms/AssetActions/Pool/utils.ts | 37 +++ .../AssetActions/Trade/Slippage.module.css | 30 ++ .../organisms/AssetActions/index.tsx | 1 - 10 files changed, 641 insertions(+), 18 deletions(-) create mode 100644 src/components/organisms/AssetActions/Pool/Actions.module.css create mode 100644 src/components/organisms/AssetActions/Pool/Actions.tsx create mode 100644 src/components/organisms/AssetActions/Pool/Add/Output.module.css create mode 100644 src/components/organisms/AssetActions/Pool/Add/index.module.css create mode 100644 src/components/organisms/AssetActions/Pool/Remove.module.css create mode 100644 src/components/organisms/AssetActions/Pool/Remove.tsx create mode 100644 src/components/organisms/AssetActions/Pool/utils.ts create mode 100644 src/components/organisms/AssetActions/Trade/Slippage.module.css diff --git a/src/components/organisms/AssetActions/Pool/Actions.module.css b/src/components/organisms/AssetActions/Pool/Actions.module.css new file mode 100644 index 000000000..d6b521977 --- /dev/null +++ b/src/components/organisms/AssetActions/Pool/Actions.module.css @@ -0,0 +1,25 @@ +.actions { + margin-left: -2rem; + margin-right: -2rem; + padding-left: var(--spacer); + padding-right: var(--spacer); + padding-top: calc(var(--spacer) / 1.5); + border-top: 1px solid var(--border-color); + text-align: center; + display: flex; + justify-content: center; +} + +.success { + margin-top: calc(var(--spacer) / 2); + margin-bottom: calc(var(--spacer) / 2); +} + +.actions button { + margin-left: calc(var(--spacer) / 4); + margin-right: calc(var(--spacer) / 4); +} + +.actions button svg { + fill: currentColor; +} diff --git a/src/components/organisms/AssetActions/Pool/Actions.tsx b/src/components/organisms/AssetActions/Pool/Actions.tsx new file mode 100644 index 000000000..3aa1ff3b2 --- /dev/null +++ b/src/components/organisms/AssetActions/Pool/Actions.tsx @@ -0,0 +1,75 @@ +import React, { ReactElement } from 'react' +import Loader from '../../../atoms/Loader' +import Button from '../../../atoms/Button' +import styles from './Actions.module.css' +import ExplorerLink from '../../../atoms/ExplorerLink' +import SuccessConfetti from '../../../atoms/SuccessConfetti' +import { useOcean } from '../../../../providers/Ocean' +import { useWeb3 } from '../../../../providers/Web3' +import TokenApproval from '../../../molecules/TokenApproval' + +export default function Actions({ + isLoading, + loaderMessage, + successMessage, + txId, + actionName, + amount, + coin, + action, + isDisabled +}: { + isLoading: boolean + loaderMessage: string + successMessage: string + txId: string + actionName: string + amount?: string + coin?: string + action: () => void + isDisabled?: boolean +}): ReactElement { + const { networkId } = useWeb3() + const { ocean } = useOcean() + + const actionButton = ( + + ) + + return ( + <> +
+ {isLoading ? ( + + ) : actionName === 'Supply' || actionName === 'Swap' ? ( + + ) : ( + actionButton + )} +
+ {txId && ( + + View transaction + + } + /> + )} + + ) +} diff --git a/src/components/organisms/AssetActions/Pool/Add/Output.module.css b/src/components/organisms/AssetActions/Pool/Add/Output.module.css new file mode 100644 index 000000000..c21bfc785 --- /dev/null +++ b/src/components/organisms/AssetActions/Pool/Add/Output.module.css @@ -0,0 +1,24 @@ +.output { + display: grid; + gap: var(--spacer); + grid-template-columns: 1fr 1fr; + padding-bottom: calc(var(--spacer) / 2); +} + +.output p { + font-weight: var(--font-weight-bold); + margin-bottom: calc(var(--spacer) / 8); + font-size: var(--font-size-small); +} + +.output div:first-child [class*='token'] { + white-space: normal; +} + +.output div:first-child [class*='token'] > figure { + display: none; +} + +.help { + text-align: center; +} diff --git a/src/components/organisms/AssetActions/Pool/Add/index.module.css b/src/components/organisms/AssetActions/Pool/Add/index.module.css new file mode 100644 index 000000000..3f5b2967c --- /dev/null +++ b/src/components/organisms/AssetActions/Pool/Add/index.module.css @@ -0,0 +1,24 @@ +.addInput { + margin: 0 auto calc(var(--spacer) / 1.5) auto; + background: var(--background-highlight); + padding: var(--spacer) calc(var(--spacer) * 2.5) calc(var(--spacer) * 1.2) + calc(var(--spacer) * 2.5); + border-bottom: 1px solid var(--border-color); + margin-top: -2rem; + margin-left: -2rem; + margin-right: -2rem; + position: relative; +} + +.addInput input { + text-align: center; +} + +.addInput div[class*='field'] { + margin-bottom: 0; +} + +.warning { + margin-left: -3rem; + margin-right: -3rem; +} diff --git a/src/components/organisms/AssetActions/Pool/Remove.module.css b/src/components/organisms/AssetActions/Pool/Remove.module.css new file mode 100644 index 000000000..1d3d229e8 --- /dev/null +++ b/src/components/organisms/AssetActions/Pool/Remove.module.css @@ -0,0 +1,70 @@ +.removeInput { + composes: addInput from './Add/index.module.css'; + padding-left: calc(var(--spacer) * 2); + padding-right: calc(var(--spacer) * 2); + padding-bottom: calc(var(--spacer) / 2); +} + +.range { + margin-top: calc(var(--spacer) / 2); + text-align: center; +} + +.range h3 { + margin-bottom: calc(var(--spacer) / 4); +} + +.range input { + width: 100%; +} + +.range p { + margin-bottom: 0; + margin-left: -2rem; + margin-right: -2rem; +} + +.slider { + position: relative; + z-index: 1; +} + +.maximum { + position: absolute; + right: -2rem; + bottom: 2rem; + font-size: var(--font-size-mini); + min-width: 5rem; + text-align: center; +} + +.toggle { + margin-top: calc(var(--spacer) / 2); + margin-bottom: 0; + font-size: var(--font-size-mini); + margin-bottom: -2rem; +} + +.output { + composes: output from './Add/Output.module.css'; +} + +.output [class*='token'] { + white-space: nowrap; +} + +.output [class*='token'] > figure { + display: inline-block; +} + +.output figure[class*='pool shares'] { + display: none; +} + +.output p { + font-size: var(--font-size-small); +} + +.slippage { + composes: slippage from '../Trade/Slippage.module.css'; +} diff --git a/src/components/organisms/AssetActions/Pool/Remove.tsx b/src/components/organisms/AssetActions/Pool/Remove.tsx new file mode 100644 index 000000000..77c3b3a7f --- /dev/null +++ b/src/components/organisms/AssetActions/Pool/Remove.tsx @@ -0,0 +1,313 @@ +import React, { + ReactElement, + useState, + ChangeEvent, + useEffect, + FormEvent, + useRef +} from 'react' +import styles from './Remove.module.css' +import Header from './Header' +import { toast } from 'react-toastify' +import Actions from './Actions' +import { Logger } from '@oceanprotocol/lib' +import Token from './Token' +import FormHelp from '../../../atoms/Input/Help' +import Button from '../../../atoms/Button' +import { getMaxPercentRemove } from './utils' +import { graphql, useStaticQuery } from 'gatsby' +import debounce from 'lodash.debounce' +import UserLiquidity from '../../../atoms/UserLiquidity' +import InputElement from '../../../atoms/Input/InputElement' +import { useOcean } from '../../../../providers/Ocean' +import { useWeb3 } from '../../../../providers/Web3' +import Decimal from 'decimal.js' +import { useAsset } from '../../../../providers/Asset' + +const contentQuery = graphql` + query PoolRemoveQuery { + content: allFile(filter: { relativePath: { eq: "price.json" } }) { + edges { + node { + childContentJson { + pool { + remove { + title + simple + advanced + output { + titleIn + titleOut + } + action + } + } + } + } + } + } + } +` + +export default function Remove({ + setShowRemove, + refreshInfo, + poolAddress, + poolTokens, + totalPoolTokens, + dtSymbol +}: { + setShowRemove: (show: boolean) => void + refreshInfo: () => void + poolAddress: string + poolTokens: string + totalPoolTokens: string + dtSymbol: string +}): ReactElement { + const data = useStaticQuery(contentQuery) + const content = data.content.edges[0].node.childContentJson.pool.remove + + const slippagePresets = ['5', '10', '15', '25', '50'] + const { accountId } = useWeb3() + const { ocean } = useOcean() + const { isAssetNetwork } = useAsset() + const [amountPercent, setAmountPercent] = useState('0') + const [amountMaxPercent, setAmountMaxPercent] = useState('100') + const [amountPoolShares, setAmountPoolShares] = useState('0') + const [amountOcean, setAmountOcean] = useState('0') + const [amountDatatoken, setAmountDatatoken] = useState('0') + const [isAdvanced, setIsAdvanced] = useState(false) + const [isLoading, setIsLoading] = useState() + const [txId, setTxId] = useState() + const [slippage, setSlippage] = useState('5') + const [minOceanAmount, setMinOceanAmount] = useState('0') + const [minDatatokenAmount, setMinDatatokenAmount] = useState('0') + + Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 }) + + async function handleRemoveLiquidity() { + setIsLoading(true) + try { + const result = + isAdvanced === true + ? await ocean.pool.removePoolLiquidity( + accountId, + poolAddress, + amountPoolShares, + minDatatokenAmount, + minOceanAmount + ) + : await ocean.pool.removeOceanLiquidityWithMinimum( + accountId, + poolAddress, + amountPoolShares, + minOceanAmount + ) + + setTxId(result?.transactionHash) + refreshInfo() + } catch (error) { + Logger.error(error.message) + toast.error(error.message) + } finally { + setIsLoading(false) + } + } + + // Get and set max percentage + useEffect(() => { + if (!ocean || !poolTokens) return + + async function getMax() { + const amountMaxPercent = + isAdvanced === true + ? '100' + : await getMaxPercentRemove(ocean, poolAddress, poolTokens) + setAmountMaxPercent(amountMaxPercent) + } + getMax() + }, [ocean, isAdvanced, poolAddress, poolTokens]) + + const getValues = useRef( + debounce(async (newAmountPoolShares, isAdvanced) => { + if (isAdvanced === true) { + const tokens = await ocean.pool.getTokensRemovedforPoolShares( + poolAddress, + `${newAmountPoolShares}` + ) + setAmountOcean(tokens?.oceanAmount) + setAmountDatatoken(tokens?.dtAmount) + return + } + + const amountOcean = await ocean.pool.getOceanRemovedforPoolShares( + poolAddress, + newAmountPoolShares + ) + setAmountOcean(amountOcean) + }, 150) + ) + // Check and set outputs when amountPoolShares changes + useEffect(() => { + if (!ocean || !poolTokens) return + getValues.current(amountPoolShares, isAdvanced) + }, [ + amountPoolShares, + isAdvanced, + ocean, + poolTokens, + poolAddress, + totalPoolTokens + ]) + + useEffect(() => { + const minOceanAmount = new Decimal(amountOcean) + .mul(new Decimal(100).minus(new Decimal(slippage))) + .dividedBy(100) + .toString() + + const minDatatokenAmount = new Decimal(amountDatatoken) + .mul(new Decimal(100).minus(new Decimal(slippage))) + .dividedBy(100) + .toString() + + setMinOceanAmount(minOceanAmount.slice(0, 18)) + setMinDatatokenAmount(minDatatokenAmount.slice(0, 18)) + }, [slippage, amountOcean, amountDatatoken, isAdvanced]) + + // Set amountPoolShares based on set slider value + function handleAmountPercentChange(e: ChangeEvent) { + setAmountPercent(e.target.value) + if (!poolTokens) return + + const amountPoolShares = new Decimal(e.target.value) + .dividedBy(100) + .mul(new Decimal(poolTokens)) + .toString() + + setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`) + } + + function handleMaxButton(e: ChangeEvent) { + e.preventDefault() + setAmountPercent(amountMaxPercent) + + const amountPoolShares = new Decimal(amountMaxPercent) + .dividedBy(100) + .mul(new Decimal(poolTokens)) + .toString() + + setAmountPoolShares(`${amountPoolShares.slice(0, 18)}`) + } + + function handleAdvancedButton(e: FormEvent) { + e.preventDefault() + setIsAdvanced(!isAdvanced) + + setAmountPoolShares('0') + setAmountPercent('0') + setAmountOcean('0') + setSlippage('5') + setMinOceanAmount('0') + setMinDatatokenAmount('0') + + if (isAdvanced === true) { + setAmountDatatoken('0') + } + } + + function handleSlippageChange(e: ChangeEvent) { + setSlippage(e.target.value) + } + + return ( +
+
setShowRemove(false)} /> + +
+ +
+

{amountPercent}%

+
+ ) => + handleAmountPercentChange(e) + } + /> + +
+ + + {isAdvanced === true ? content.advanced : content.simple} + + +
+ +
+
+

{content.output.titleIn}

+ +
+
+

{content.output.titleOut} minimum

+ {isAdvanced === true ? ( + <> + + + + ) : ( + + )} +
+
+
+ Expected price impact + ) => + handleSlippageChange(e) + } + /> +
+ handleRemoveLiquidity()} + successMessage="Successfully removed liquidity." + isDisabled={!isAssetNetwork} + txId={txId} + /> +
+ ) +} diff --git a/src/components/organisms/AssetActions/Pool/index.tsx b/src/components/organisms/AssetActions/Pool/index.tsx index a2cccdc11..72eeb6404 100644 --- a/src/components/organisms/AssetActions/Pool/index.tsx +++ b/src/components/organisms/AssetActions/Pool/index.tsx @@ -1,8 +1,8 @@ import React, { ReactElement, useEffect, useState } from 'react' import { Logger } from '@oceanprotocol/lib' import styles from './index.module.css' +import stylesActions from './Actions.module.css' import PriceUnit from '../../../atoms/Price/PriceUnit' -import Alert from '../../../atoms/Alert' import Tooltip from '../../../atoms/Tooltip' import ExplorerLink from '../../../atoms/ExplorerLink' import Token from './Token' @@ -19,10 +19,8 @@ import PoolTransactions from '../../../molecules/PoolTransactions' import { fetchData, getQueryContext } from '../../../../utils/subgraph' import { isValidNumber } from './../../../../utils/numberValidations' import Decimal from 'decimal.js' -import { - MigrationStatus, - useMigrationStatus -} from '../../../../providers/Migration' +import Remove from './Remove' +import Button from '../../../atoms/Button' const REFETCH_INTERVAL = 5000 @@ -86,11 +84,10 @@ export default function Pool(): ReactElement { const content = data.content.edges[0].node.childContentJson.pool const { accountId } = useWeb3() - const { status } = useMigrationStatus() const [dtSymbol, setDtSymbol] = useState() const [oceanSymbol, setOceanSymbol] = useState() - const { ddo, owner, price } = useAsset() + const { ddo, owner, price, isInPurgatory, isAssetNetwork } = useAsset() const [poolTokens, setPoolTokens] = useState() const [totalPoolTokens, setTotalPoolTokens] = useState() @@ -116,6 +113,13 @@ export default function Pool(): ReactElement { const [liquidityFetchInterval, setLiquidityFetchInterval] = useState() + const [showRemove, setShowRemove] = useState(false) + const [isRemoveDisabled, setIsRemoveDisabled] = useState(false) + + const [hasAddedLiquidity, setHasAddedLiquidity] = useState(false) + // the purpose of the value is just to trigger the effect + const [refreshPool, setRefreshPool] = useState(false) + async function getPoolLiquidity() { const queryContext = getQueryContext(ddo.chainId) const queryVariables = { @@ -267,6 +271,10 @@ export default function Pool(): ReactElement { init() }, [dataLiquidity, ddo.dataToken, price.datatoken, price.ocean, price?.value]) + useEffect(() => { + setIsRemoveDisabled(isInPurgatory && owner === accountId) + }, [isInPurgatory, owner, accountId]) + useEffect(() => { if (!dataLiquidity) return const poolShare = @@ -280,6 +288,7 @@ export default function Pool(): ReactElement { .toFixed(5) setPoolShare(poolShare) + setHasAddedLiquidity(Number(poolShare) > 0) const totalUserLiquidityInOcean = isValidNumber(userLiquidity?.ocean) && @@ -350,7 +359,23 @@ export default function Pool(): ReactElement { init() }, [accountId, price, ddo, owner, totalPoolTokens]) - return ( + const refreshInfo = async () => { + setRefreshPool(!refreshPool) + + // need some form of replacement or something. + // await refreshPrice() + } + + return showRemove ? ( + + ) : ( <>
={' '} @@ -433,15 +458,16 @@ export default function Pool(): ReactElement { > - + +
+ +
{accountId && ( diff --git a/src/components/organisms/AssetActions/Pool/utils.ts b/src/components/organisms/AssetActions/Pool/utils.ts new file mode 100644 index 000000000..68fe113e2 --- /dev/null +++ b/src/components/organisms/AssetActions/Pool/utils.ts @@ -0,0 +1,37 @@ +import { Ocean } from '@oceanprotocol/lib' + +import { isValidNumber } from './../../../../utils/numberValidations' +import Decimal from 'decimal.js' + +Decimal.set({ toExpNeg: -18, precision: 18, rounding: 1 }) + +export async function getMaxPercentRemove( + ocean: Ocean, + poolAddress: string, + poolTokens: string +): Promise { + const amountMaxOcean = await ocean.pool.getOceanMaxRemoveLiquidity( + poolAddress + ) + + const amountMaxPoolShares = + await ocean.pool.getPoolSharesRequiredToRemoveOcean( + poolAddress, + amountMaxOcean + ) + + let amountMaxPercent = + isValidNumber(amountMaxPoolShares) && isValidNumber(poolTokens) + ? new Decimal(amountMaxPoolShares) + .dividedBy(new Decimal(poolTokens)) + .mul(100) + .floor() + .toString() + : '0' + + if (Number(amountMaxPercent) > 100) { + amountMaxPercent = '100' + } + + return amountMaxPercent +} diff --git a/src/components/organisms/AssetActions/Trade/Slippage.module.css b/src/components/organisms/AssetActions/Trade/Slippage.module.css new file mode 100644 index 000000000..47041a7f2 --- /dev/null +++ b/src/components/organisms/AssetActions/Trade/Slippage.module.css @@ -0,0 +1,30 @@ +.slippage { + 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); +} + +.slippage strong { + font-weight: var(--font-weight-base); + text-align: right; +} + +.title { + font-family: var(--font-family-base); + font-weight: var(--font-weight-base); + font-size: var(--font-size-mini); + text-align: center; + margin-bottom: calc(var(--spacer) / 4); + color: var(--color-secondary); +} + +.slippage select { + width: fit-content; + display: inline-block; +} diff --git a/src/components/organisms/AssetActions/index.tsx b/src/components/organisms/AssetActions/index.tsx index ef7f62cc7..e1eeaa9bb 100644 --- a/src/components/organisms/AssetActions/index.tsx +++ b/src/components/organisms/AssetActions/index.tsx @@ -135,7 +135,6 @@ export default function AssetActions(): ReactElement { return ( <> - {price?.type === 'pool' && }