mirror of
https://github.com/oceanprotocol/market.git
synced 2024-12-02 05:57:29 +01:00
Merge pull request #102 from oceanprotocol/feature/remove-liquidity
Removing liquidity
This commit is contained in:
commit
7c1ecbfe51
26
content/price.json
Normal file
26
content/price.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"pool": {
|
||||
"tooltips": {
|
||||
"price": "Explain how this price is determined...",
|
||||
"liquidity": "Explain what this represents, advantage of providing liquidity..."
|
||||
},
|
||||
"add": {
|
||||
"title": "Add Liquidity",
|
||||
"output": {
|
||||
"titleIn": "You will receive",
|
||||
"titleOut": "You will earn"
|
||||
},
|
||||
"action": "Supply"
|
||||
},
|
||||
"remove": {
|
||||
"title": "Remove Liquidity",
|
||||
"simple": "Set the amount of your pool shares to spend. You will get the equivalent value in OCEAN, limited to maximum amount for pool protection. If you have Datatokens left in your wallet, you can add them to the pool to increase the maximum amount.",
|
||||
"advanced": "Set the amount of your pool shares to spend. You will get OCEAN and Datatokens equivalent to your pool share, without any limit. You can use these Datatokens in other DeFi tools.",
|
||||
"output": {
|
||||
"titleIn": "You will spend",
|
||||
"titleOut": "You will receive"
|
||||
},
|
||||
"action": "Remove"
|
||||
}
|
||||
}
|
||||
}
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -4029,9 +4029,9 @@
|
||||
"integrity": "sha512-LING+GvW37I0L40rZdPCZ1SvcZurDSGGhT0WOVPNO8oyh2C3bXModDBNE4+gCFa8pTbQBOc4ot1/Zoj9PfT/zA=="
|
||||
},
|
||||
"@oceanprotocol/lib": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.6.1.tgz",
|
||||
"integrity": "sha512-nU+sBTpGdoCaZkzOBkZxwEtjSsxFQ9YDK3C3a8iuxoY60Cd1OfoiG4toBWF3yLgXaXkaiYPufZ6rL4sJKakHKg==",
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.6.2.tgz",
|
||||
"integrity": "sha512-hqZCzJXU+P8lnal+H329UHXfoLHAgjYw+foWxG466KcrQ66TtcdpGIMKMNev+A990P8KzueC8mC62BLGj6//Gg==",
|
||||
"requires": {
|
||||
"@ethereum-navigator/navigator": "^0.5.0",
|
||||
"@oceanprotocol/contracts": "^0.5.5",
|
||||
|
@ -22,7 +22,7 @@
|
||||
"@coingecko/cryptoformat": "^0.4.2",
|
||||
"@loadable/component": "5.13.1",
|
||||
"@oceanprotocol/art": "^3.0.0",
|
||||
"@oceanprotocol/lib": "^0.6.1",
|
||||
"@oceanprotocol/lib": "^0.6.2",
|
||||
"@oceanprotocol/react": "^0.2.0",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@sindresorhus/slugify": "^1.0.0",
|
||||
|
@ -204,6 +204,34 @@
|
||||
border-color: var(--brand-grey);
|
||||
}
|
||||
|
||||
input[type='range'] {
|
||||
background: none;
|
||||
}
|
||||
|
||||
input[type='range']:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-thumb,
|
||||
input[type='range']::-moz-range-thumb {
|
||||
appearance: none;
|
||||
background: var(--brand-gradient);
|
||||
border: 2px solid var(--brand-grey-lighter);
|
||||
width: var(--font-size-large);
|
||||
height: var(--font-size-large);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 9px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
input[type='range']::-webkit-slider-runnable-track,
|
||||
input[type='range']::-moz-range-track {
|
||||
background: var(--brand-grey-lighter);
|
||||
border-radius: var(--border-radius);
|
||||
height: 0.3rem;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Size modifiers */
|
||||
|
||||
.small {
|
||||
|
@ -11,6 +11,30 @@ import PriceUnit from '../../../atoms/Price/PriceUnit'
|
||||
import Actions from './Actions'
|
||||
import Tooltip from '../../../atoms/Tooltip'
|
||||
import { ReactComponent as Caret } from '../../../../images/caret.svg'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
|
||||
const contentQuery = graphql`
|
||||
query PoolAddQuery {
|
||||
content: allFile(filter: { relativePath: { eq: "price.json" } }) {
|
||||
edges {
|
||||
node {
|
||||
childContentJson {
|
||||
pool {
|
||||
add {
|
||||
title
|
||||
output {
|
||||
titleIn
|
||||
titleOut
|
||||
}
|
||||
action
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Add({
|
||||
setShowAdd,
|
||||
@ -29,6 +53,9 @@ export default function Add({
|
||||
dtSymbol: string
|
||||
dtAddress: string
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childContentJson.pool.add
|
||||
|
||||
const { ocean, accountId, balance } = useOcean()
|
||||
const [amount, setAmount] = useState('')
|
||||
const [txId, setTxId] = useState<string>('')
|
||||
@ -93,7 +120,7 @@ export default function Add({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Add Liquidity" backAction={() => setShowAdd(false)} />
|
||||
<Header title={content.title} backAction={() => setShowAdd(false)} />
|
||||
|
||||
<div className={styles.addInput}>
|
||||
<div className={styles.userLiquidity}>
|
||||
@ -138,12 +165,12 @@ export default function Add({
|
||||
|
||||
<div className={styles.output}>
|
||||
<div>
|
||||
<p>You will receive</p>
|
||||
<p>{content.output.titleIn}</p>
|
||||
<Token symbol="pool shares" balance={newPoolTokens} />
|
||||
<Token symbol="% of pool" balance={newPoolShare} />
|
||||
</div>
|
||||
<div>
|
||||
<p>You will earn</p>
|
||||
<p>{content.output.titleOut}</p>
|
||||
<Token symbol="% swap fee" balance={swapFee} />
|
||||
</div>
|
||||
</div>
|
||||
@ -151,7 +178,7 @@ export default function Add({
|
||||
<Actions
|
||||
isLoading={isLoading}
|
||||
loaderMessage="Adding Liquidity..."
|
||||
actionName="Supply"
|
||||
actionName={content.action}
|
||||
action={handleAddLiquidity}
|
||||
txId={txId}
|
||||
/>
|
||||
|
@ -1,11 +1,66 @@
|
||||
.removeInput {
|
||||
composes: addInput from './Add.module.css';
|
||||
}
|
||||
|
||||
.buttonMax {
|
||||
composes: buttonMax from './Add.module.css';
|
||||
padding-left: calc(var(--spacer) * 2);
|
||||
padding-right: calc(var(--spacer) * 2);
|
||||
}
|
||||
|
||||
.userLiquidity {
|
||||
composes: userLiquidity from './Add.module.css';
|
||||
}
|
||||
|
||||
.range {
|
||||
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;
|
||||
}
|
||||
|
||||
.range button {
|
||||
margin-top: calc(var(--spacer) / 4);
|
||||
margin-bottom: 0;
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
right: 2rem;
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.maximum {
|
||||
position: absolute;
|
||||
right: -1.5rem;
|
||||
bottom: 1.5rem;
|
||||
font-size: var(--font-size-small);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.output {
|
||||
composes: output from './Add.module.css';
|
||||
}
|
||||
|
||||
.output [class*='token'] {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.output [class*='token'] > figure {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.output figure[class*='pool shares'] {
|
||||
display: none;
|
||||
}
|
||||
|
@ -1,42 +1,90 @@
|
||||
import React, { ReactElement, useState, ChangeEvent } from 'react'
|
||||
import React, {
|
||||
ReactElement,
|
||||
useState,
|
||||
ChangeEvent,
|
||||
useEffect,
|
||||
FormEvent
|
||||
} from 'react'
|
||||
import styles from './Remove.module.css'
|
||||
import { useOcean } from '@oceanprotocol/react'
|
||||
import Header from './Header'
|
||||
import { toast } from 'react-toastify'
|
||||
import InputElement from '../../../atoms/Input/InputElement'
|
||||
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 PriceUnit from '../../../atoms/Price/PriceUnit'
|
||||
import { Balance } from '.'
|
||||
import { getMaxValuesRemove } from './utils'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
|
||||
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,
|
||||
poolAddress,
|
||||
totalPoolTokens,
|
||||
userLiquidity
|
||||
poolTokens,
|
||||
dtSymbol
|
||||
}: {
|
||||
setShowRemove: (show: boolean) => void
|
||||
poolAddress: string
|
||||
totalPoolTokens: string
|
||||
userLiquidity: Balance
|
||||
poolTokens: string
|
||||
dtSymbol: string
|
||||
}): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childContentJson.pool.remove
|
||||
|
||||
const { ocean, accountId } = useOcean()
|
||||
const [amount, setAmount] = useState('')
|
||||
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<boolean>()
|
||||
const [txId, setTxId] = useState<string>('')
|
||||
const [txId, setTxId] = useState<string>()
|
||||
|
||||
async function handleRemoveLiquidity() {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const result = await ocean.pool.removeOceanLiquidity(
|
||||
accountId,
|
||||
poolAddress,
|
||||
amount,
|
||||
totalPoolTokens
|
||||
)
|
||||
setTxId(result.transactionHash)
|
||||
const result =
|
||||
isAdvanced === true
|
||||
? await ocean.pool.removePoolLiquidity(
|
||||
accountId,
|
||||
poolAddress,
|
||||
amountPoolShares
|
||||
)
|
||||
: await ocean.pool.removeOceanLiquidity(
|
||||
accountId,
|
||||
poolAddress,
|
||||
amountDatatoken,
|
||||
amountPoolShares
|
||||
)
|
||||
|
||||
setTxId(result?.transactionHash)
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
toast.error(error.message)
|
||||
@ -45,55 +93,97 @@ export default function Remove({
|
||||
}
|
||||
}
|
||||
|
||||
function handleAmountChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
setAmount(e.target.value)
|
||||
function handleAmountPercentChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
setAmountPercent(e.target.value)
|
||||
}
|
||||
|
||||
function handleMax() {
|
||||
setAmount(`${userLiquidity.ocean}`)
|
||||
function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) {
|
||||
e.preventDefault()
|
||||
setIsAdvanced(!isAdvanced)
|
||||
}
|
||||
|
||||
// Check and set outputs when percentage changes
|
||||
useEffect(() => {
|
||||
if (!ocean || !poolTokens) return
|
||||
|
||||
async function getValues() {
|
||||
const amountPoolShares =
|
||||
(Number(amountPercent) / 100) * Number(poolTokens)
|
||||
setAmountPoolShares(`${amountPoolShares}`)
|
||||
|
||||
if (isAdvanced === true) {
|
||||
setAmountMaxPercent('100')
|
||||
|
||||
const tokens = await ocean.pool.getTokensRemovedforPoolShares(
|
||||
poolAddress,
|
||||
`${amountPoolShares}`
|
||||
)
|
||||
setAmountOcean(tokens?.oceanAmount)
|
||||
setAmountDatatoken(tokens?.dtAmount)
|
||||
} else {
|
||||
const { amountMaxPercent, amountOcean } = await getMaxValuesRemove(
|
||||
ocean,
|
||||
poolAddress,
|
||||
poolTokens,
|
||||
`${amountPoolShares}`
|
||||
)
|
||||
setAmountMaxPercent(amountMaxPercent)
|
||||
setAmountOcean(amountOcean)
|
||||
}
|
||||
}
|
||||
getValues()
|
||||
}, [amountPercent, isAdvanced, ocean, poolTokens, poolAddress])
|
||||
|
||||
return (
|
||||
<div className={styles.remove}>
|
||||
<Header
|
||||
title="Remove Liquidity"
|
||||
backAction={() => setShowRemove(false)}
|
||||
/>
|
||||
<Header title={content.title} backAction={() => setShowRemove(false)} />
|
||||
|
||||
<form className={styles.removeInput}>
|
||||
<div className={styles.userLiquidity}>
|
||||
<span>Your pool liquidity: </span>
|
||||
<PriceUnit price={`${userLiquidity.ocean}`} symbol="OCEAN" small />
|
||||
</div>
|
||||
<InputElement
|
||||
value={amount}
|
||||
name="ocean"
|
||||
type="number"
|
||||
prefix="OCEAN"
|
||||
placeholder="0"
|
||||
onChange={handleAmountChange}
|
||||
/>
|
||||
<div className={styles.range}>
|
||||
<h3>{amountPercent}%</h3>
|
||||
<div className={styles.slider}>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={amountMaxPercent}
|
||||
step={Number(amountMaxPercent) < 10 ? '1' : '10'}
|
||||
value={amountPercent}
|
||||
onChange={handleAmountPercentChange}
|
||||
/>
|
||||
{isAdvanced === false && (
|
||||
<span
|
||||
className={styles.maximum}
|
||||
>{`${amountMaxPercent}% max.`}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{userLiquidity.ocean > Number(amount) && (
|
||||
<Button
|
||||
className={styles.buttonMax}
|
||||
style="text"
|
||||
size="small"
|
||||
onClick={handleMax}
|
||||
>
|
||||
Use Max
|
||||
<FormHelp>
|
||||
{isAdvanced === true ? content.advanced : content.simple}
|
||||
</FormHelp>
|
||||
<Button style="text" size="small" onClick={handleAdvancedButton}>
|
||||
{isAdvanced === true ? 'Simple' : 'Advanced'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* <Input name="dt" label={dtSymbol} type="number" placeholder="0" /> */}
|
||||
|
||||
<p>You will receive</p>
|
||||
<div className={styles.output}>
|
||||
<div>
|
||||
<p>{content.output.titleIn}</p>
|
||||
<Token symbol="pool shares" balance={amountPoolShares} noIcon />
|
||||
</div>
|
||||
<div>
|
||||
<p>{content.output.titleOut}</p>
|
||||
<Token symbol="OCEAN" balance={amountOcean} />
|
||||
{isAdvanced === true && (
|
||||
<Token symbol={dtSymbol} balance={amountDatatoken} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Actions
|
||||
isLoading={isLoading}
|
||||
loaderMessage="Removing Liquidity..."
|
||||
actionName="Remove"
|
||||
actionName={content.action}
|
||||
action={handleRemoveLiquidity}
|
||||
txId={txId}
|
||||
/>
|
||||
|
@ -13,17 +13,36 @@ import Conversion from '../../../atoms/Price/Conversion'
|
||||
import EtherscanLink from '../../../atoms/EtherscanLink'
|
||||
import Token from './Token'
|
||||
import TokenList from './TokenList'
|
||||
import { graphql, useStaticQuery } from 'gatsby'
|
||||
|
||||
export interface Balance {
|
||||
ocean: number
|
||||
datatoken: number
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: create tooltip copy
|
||||
*/
|
||||
const contentQuery = graphql`
|
||||
query PoolQuery {
|
||||
content: allFile(filter: { relativePath: { eq: "price.json" } }) {
|
||||
edges {
|
||||
node {
|
||||
childContentJson {
|
||||
pool {
|
||||
tooltips {
|
||||
price
|
||||
liquidity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
const data = useStaticQuery(contentQuery)
|
||||
const content = data.content.edges[0].node.childContentJson.pool
|
||||
|
||||
const { ocean, accountId } = useOcean()
|
||||
const { price } = useMetadata(ddo)
|
||||
|
||||
@ -128,8 +147,8 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
<Remove
|
||||
setShowRemove={setShowRemove}
|
||||
poolAddress={price.address}
|
||||
totalPoolTokens={totalPoolTokens}
|
||||
userLiquidity={userLiquidity}
|
||||
poolTokens={poolTokens}
|
||||
dtSymbol={dtSymbol}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@ -137,7 +156,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
<PriceUnit price="1" symbol={dtSymbol} /> ={' '}
|
||||
<PriceUnit price={`${price.value}`} />
|
||||
<Conversion price={`${price.value}`} />
|
||||
<Tooltip content="Explain how this price is determined..." />
|
||||
<Tooltip content={content.tooltips.price} />
|
||||
<div className={styles.dataTokenLinks}>
|
||||
<EtherscanLink
|
||||
network="rinkeby"
|
||||
@ -155,7 +174,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
|
||||
title={
|
||||
<>
|
||||
Your Liquidity
|
||||
<Tooltip content="Explain what this represents, advantage of providing liquidity..." />
|
||||
<Tooltip content={content.tooltips.liquidity} />
|
||||
</>
|
||||
}
|
||||
ocean={`${userLiquidity.ocean}`}
|
||||
|
28
src/components/organisms/AssetActions/Pool/utils.ts
Normal file
28
src/components/organisms/AssetActions/Pool/utils.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Ocean } from '@oceanprotocol/lib'
|
||||
|
||||
export async function getMaxValuesRemove(
|
||||
ocean: Ocean,
|
||||
poolAddress: string,
|
||||
poolTokens: string,
|
||||
amountPoolShares: string
|
||||
): Promise<{ amountMaxPercent: string; amountOcean: string }> {
|
||||
const amountMaxOcean = await ocean.pool.getOceanMaxRemoveLiquidity(
|
||||
poolAddress
|
||||
)
|
||||
|
||||
const amountMaxPoolShares = await ocean.pool.getPoolSharesRequiredToRemoveOcean(
|
||||
poolAddress,
|
||||
amountMaxOcean
|
||||
)
|
||||
|
||||
const amountMaxPercent = `${Math.floor(
|
||||
(Number(amountMaxPoolShares) / Number(poolTokens)) * 100
|
||||
)}`
|
||||
|
||||
const amountOcean = await ocean.pool.getOceanRemovedforPoolShares(
|
||||
poolAddress,
|
||||
amountPoolShares
|
||||
)
|
||||
|
||||
return { amountMaxPercent, amountOcean }
|
||||
}
|
Loading…
Reference in New Issue
Block a user