1
0
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:
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=="
},
"@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",

View File

@ -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",

View File

@ -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 {

View File

@ -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}
/>

View File

@ -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;
}

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 { 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}
/>

View File

@ -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}`}

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