1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-11-15 09:44:53 +01:00

Merge pull request #102 from oceanprotocol/feature/remove-liquidity

Removing liquidity
This commit is contained in:
Matthias Kretschmann 2020-10-14 20:33:22 +02:00 committed by GitHub
commit 7c1ecbfe51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 342 additions and 69 deletions

26
content/price.json Normal file
View 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
View File

@ -4029,9 +4029,9 @@
"integrity": "sha512-LING+GvW37I0L40rZdPCZ1SvcZurDSGGhT0WOVPNO8oyh2C3bXModDBNE4+gCFa8pTbQBOc4ot1/Zoj9PfT/zA==" "integrity": "sha512-LING+GvW37I0L40rZdPCZ1SvcZurDSGGhT0WOVPNO8oyh2C3bXModDBNE4+gCFa8pTbQBOc4ot1/Zoj9PfT/zA=="
}, },
"@oceanprotocol/lib": { "@oceanprotocol/lib": {
"version": "0.6.1", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.6.1.tgz", "resolved": "https://registry.npmjs.org/@oceanprotocol/lib/-/lib-0.6.2.tgz",
"integrity": "sha512-nU+sBTpGdoCaZkzOBkZxwEtjSsxFQ9YDK3C3a8iuxoY60Cd1OfoiG4toBWF3yLgXaXkaiYPufZ6rL4sJKakHKg==", "integrity": "sha512-hqZCzJXU+P8lnal+H329UHXfoLHAgjYw+foWxG466KcrQ66TtcdpGIMKMNev+A990P8KzueC8mC62BLGj6//Gg==",
"requires": { "requires": {
"@ethereum-navigator/navigator": "^0.5.0", "@ethereum-navigator/navigator": "^0.5.0",
"@oceanprotocol/contracts": "^0.5.5", "@oceanprotocol/contracts": "^0.5.5",

View File

@ -22,7 +22,7 @@
"@coingecko/cryptoformat": "^0.4.2", "@coingecko/cryptoformat": "^0.4.2",
"@loadable/component": "5.13.1", "@loadable/component": "5.13.1",
"@oceanprotocol/art": "^3.0.0", "@oceanprotocol/art": "^3.0.0",
"@oceanprotocol/lib": "^0.6.1", "@oceanprotocol/lib": "^0.6.2",
"@oceanprotocol/react": "^0.2.0", "@oceanprotocol/react": "^0.2.0",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^1.0.0", "@sindresorhus/slugify": "^1.0.0",

View File

@ -204,6 +204,34 @@
border-color: var(--brand-grey); 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 */ /* Size modifiers */
.small { .small {

View File

@ -11,6 +11,30 @@ import PriceUnit from '../../../atoms/Price/PriceUnit'
import Actions from './Actions' import Actions from './Actions'
import Tooltip from '../../../atoms/Tooltip' import Tooltip from '../../../atoms/Tooltip'
import { ReactComponent as Caret } from '../../../../images/caret.svg' 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({ export default function Add({
setShowAdd, setShowAdd,
@ -29,6 +53,9 @@ export default function Add({
dtSymbol: string dtSymbol: string
dtAddress: string dtAddress: string
}): ReactElement { }): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childContentJson.pool.add
const { ocean, accountId, balance } = useOcean() const { ocean, accountId, balance } = useOcean()
const [amount, setAmount] = useState('') const [amount, setAmount] = useState('')
const [txId, setTxId] = useState<string>('') const [txId, setTxId] = useState<string>('')
@ -93,7 +120,7 @@ export default function Add({
return ( return (
<> <>
<Header title="Add Liquidity" backAction={() => setShowAdd(false)} /> <Header title={content.title} backAction={() => setShowAdd(false)} />
<div className={styles.addInput}> <div className={styles.addInput}>
<div className={styles.userLiquidity}> <div className={styles.userLiquidity}>
@ -138,12 +165,12 @@ export default function Add({
<div className={styles.output}> <div className={styles.output}>
<div> <div>
<p>You will receive</p> <p>{content.output.titleIn}</p>
<Token symbol="pool shares" balance={newPoolTokens} /> <Token symbol="pool shares" balance={newPoolTokens} />
<Token symbol="% of pool" balance={newPoolShare} /> <Token symbol="% of pool" balance={newPoolShare} />
</div> </div>
<div> <div>
<p>You will earn</p> <p>{content.output.titleOut}</p>
<Token symbol="% swap fee" balance={swapFee} /> <Token symbol="% swap fee" balance={swapFee} />
</div> </div>
</div> </div>
@ -151,7 +178,7 @@ export default function Add({
<Actions <Actions
isLoading={isLoading} isLoading={isLoading}
loaderMessage="Adding Liquidity..." loaderMessage="Adding Liquidity..."
actionName="Supply" actionName={content.action}
action={handleAddLiquidity} action={handleAddLiquidity}
txId={txId} txId={txId}
/> />

View File

@ -1,11 +1,66 @@
.removeInput { .removeInput {
composes: addInput from './Add.module.css'; composes: addInput from './Add.module.css';
} padding-left: calc(var(--spacer) * 2);
padding-right: calc(var(--spacer) * 2);
.buttonMax {
composes: buttonMax from './Add.module.css';
} }
.userLiquidity { .userLiquidity {
composes: userLiquidity from './Add.module.css'; 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;
}

View File

@ -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 styles from './Remove.module.css'
import { useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
import Header from './Header' import Header from './Header'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import InputElement from '../../../atoms/Input/InputElement'
import Actions from './Actions' import Actions from './Actions'
import { Logger } from '@oceanprotocol/lib' import { Logger } from '@oceanprotocol/lib'
import Token from './Token'
import FormHelp from '../../../atoms/Input/Help'
import Button from '../../../atoms/Button' import Button from '../../../atoms/Button'
import PriceUnit from '../../../atoms/Price/PriceUnit' import { getMaxValuesRemove } from './utils'
import { Balance } from '.' 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({ export default function Remove({
setShowRemove, setShowRemove,
poolAddress, poolAddress,
totalPoolTokens, poolTokens,
userLiquidity dtSymbol
}: { }: {
setShowRemove: (show: boolean) => void setShowRemove: (show: boolean) => void
poolAddress: string poolAddress: string
totalPoolTokens: string poolTokens: string
userLiquidity: Balance dtSymbol: string
}): ReactElement { }): ReactElement {
const data = useStaticQuery(contentQuery)
const content = data.content.edges[0].node.childContentJson.pool.remove
const { ocean, accountId } = useOcean() 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 [isLoading, setIsLoading] = useState<boolean>()
const [txId, setTxId] = useState<string>('') const [txId, setTxId] = useState<string>()
async function handleRemoveLiquidity() { async function handleRemoveLiquidity() {
setIsLoading(true) setIsLoading(true)
try { try {
const result = await ocean.pool.removeOceanLiquidity( const result =
isAdvanced === true
? await ocean.pool.removePoolLiquidity(
accountId, accountId,
poolAddress, poolAddress,
amount, amountPoolShares
totalPoolTokens
) )
setTxId(result.transactionHash) : await ocean.pool.removeOceanLiquidity(
accountId,
poolAddress,
amountDatatoken,
amountPoolShares
)
setTxId(result?.transactionHash)
} catch (error) { } catch (error) {
Logger.error(error.message) Logger.error(error.message)
toast.error(error.message) toast.error(error.message)
@ -45,55 +93,97 @@ export default function Remove({
} }
} }
function handleAmountChange(e: ChangeEvent<HTMLInputElement>) { function handleAmountPercentChange(e: ChangeEvent<HTMLInputElement>) {
setAmount(e.target.value) setAmountPercent(e.target.value)
} }
function handleMax() { function handleAdvancedButton(e: FormEvent<HTMLButtonElement>) {
setAmount(`${userLiquidity.ocean}`) 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 ( return (
<div className={styles.remove}> <div className={styles.remove}>
<Header <Header title={content.title} backAction={() => setShowRemove(false)} />
title="Remove Liquidity"
backAction={() => setShowRemove(false)}
/>
<form className={styles.removeInput}> <form className={styles.removeInput}>
<div className={styles.userLiquidity}> <div className={styles.range}>
<span>Your pool liquidity: </span> <h3>{amountPercent}%</h3>
<PriceUnit price={`${userLiquidity.ocean}`} symbol="OCEAN" small /> <div className={styles.slider}>
</div> <input
<InputElement type="range"
value={amount} min="0"
name="ocean" max={amountMaxPercent}
type="number" step={Number(amountMaxPercent) < 10 ? '1' : '10'}
prefix="OCEAN" value={amountPercent}
placeholder="0" onChange={handleAmountPercentChange}
onChange={handleAmountChange}
/> />
{isAdvanced === false && (
{userLiquidity.ocean > Number(amount) && ( <span
<Button className={styles.maximum}
className={styles.buttonMax} >{`${amountMaxPercent}% max.`}</span>
style="text"
size="small"
onClick={handleMax}
>
Use Max
</Button>
)} )}
</div>
<FormHelp>
{isAdvanced === true ? content.advanced : content.simple}
</FormHelp>
<Button style="text" size="small" onClick={handleAdvancedButton}>
{isAdvanced === true ? 'Simple' : 'Advanced'}
</Button>
</div>
</form> </form>
{/* <Input name="dt" label={dtSymbol} type="number" placeholder="0" /> */} <div className={styles.output}>
<div>
<p>You will receive</p> <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 <Actions
isLoading={isLoading} isLoading={isLoading}
loaderMessage="Removing Liquidity..." loaderMessage="Removing Liquidity..."
actionName="Remove" actionName={content.action}
action={handleRemoveLiquidity} action={handleRemoveLiquidity}
txId={txId} txId={txId}
/> />

View File

@ -13,17 +13,36 @@ import Conversion from '../../../atoms/Price/Conversion'
import EtherscanLink from '../../../atoms/EtherscanLink' import EtherscanLink from '../../../atoms/EtherscanLink'
import Token from './Token' import Token from './Token'
import TokenList from './TokenList' import TokenList from './TokenList'
import { graphql, useStaticQuery } from 'gatsby'
export interface Balance { export interface Balance {
ocean: number ocean: number
datatoken: number datatoken: number
} }
/* const contentQuery = graphql`
TODO: create tooltip copy query PoolQuery {
*/ content: allFile(filter: { relativePath: { eq: "price.json" } }) {
edges {
node {
childContentJson {
pool {
tooltips {
price
liquidity
}
}
}
}
}
}
}
`
export default function Pool({ ddo }: { ddo: DDO }): ReactElement { 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 { ocean, accountId } = useOcean()
const { price } = useMetadata(ddo) const { price } = useMetadata(ddo)
@ -128,8 +147,8 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
<Remove <Remove
setShowRemove={setShowRemove} setShowRemove={setShowRemove}
poolAddress={price.address} poolAddress={price.address}
totalPoolTokens={totalPoolTokens} poolTokens={poolTokens}
userLiquidity={userLiquidity} dtSymbol={dtSymbol}
/> />
) : ( ) : (
<> <>
@ -137,7 +156,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
<PriceUnit price="1" symbol={dtSymbol} /> ={' '} <PriceUnit price="1" symbol={dtSymbol} /> ={' '}
<PriceUnit price={`${price.value}`} /> <PriceUnit price={`${price.value}`} />
<Conversion price={`${price.value}`} /> <Conversion price={`${price.value}`} />
<Tooltip content="Explain how this price is determined..." /> <Tooltip content={content.tooltips.price} />
<div className={styles.dataTokenLinks}> <div className={styles.dataTokenLinks}>
<EtherscanLink <EtherscanLink
network="rinkeby" network="rinkeby"
@ -155,7 +174,7 @@ export default function Pool({ ddo }: { ddo: DDO }): ReactElement {
title={ title={
<> <>
Your Liquidity Your Liquidity
<Tooltip content="Explain what this represents, advantage of providing liquidity..." /> <Tooltip content={content.tooltips.liquidity} />
</> </>
} }
ocean={`${userLiquidity.ocean}`} ocean={`${userLiquidity.ocean}`}

View 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 }
}