Add Remove Liquidity (#98)
This commit is contained in:
parent
14b70eebeb
commit
c798045590
@ -56,6 +56,7 @@ class CurrencyInputPanel extends Component {
|
||||
addExchange: PropTypes.func.isRequired,
|
||||
filteredTokens: PropTypes.arrayOf(PropTypes.string),
|
||||
disableUnlock: PropTypes.bool,
|
||||
renderInput: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -249,34 +250,21 @@ class CurrencyInputPanel extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
renderInput() {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
extraText,
|
||||
errorMessage,
|
||||
value,
|
||||
onValueChange,
|
||||
selectedTokenAddress,
|
||||
disableTokenSelect,
|
||||
renderInput,
|
||||
} = this.props;
|
||||
|
||||
if (typeof renderInput === 'function') {
|
||||
return renderInput();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="currency-input-panel">
|
||||
<div className={classnames('currency-input-panel__container', {
|
||||
'currency-input-panel__container--error': errorMessage,
|
||||
})}>
|
||||
<div className="currency-input-panel__label-row">
|
||||
<div className="currency-input-panel__label-container">
|
||||
<span className="currency-input-panel__label">{title}</span>
|
||||
<span className="currency-input-panel__label-description">{description}</span>
|
||||
</div>
|
||||
<span className={classnames('currency-input-panel__extra-text', {
|
||||
'currency-input-panel__extra-text--error': errorMessage,
|
||||
})}>
|
||||
{extraText}
|
||||
</span>
|
||||
</div>
|
||||
<div className="currency-input-panel__input-row">
|
||||
<input
|
||||
type="number"
|
||||
@ -323,6 +311,34 @@ class CurrencyInputPanel extends Component {
|
||||
<span className="currency-input-panel__dropdown-icon" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
extraText,
|
||||
errorMessage,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="currency-input-panel">
|
||||
<div className={classnames('currency-input-panel__container', {
|
||||
'currency-input-panel__container--error': errorMessage,
|
||||
})}>
|
||||
<div className="currency-input-panel__label-row">
|
||||
<div className="currency-input-panel__label-container">
|
||||
<span className="currency-input-panel__label">{title}</span>
|
||||
<span className="currency-input-panel__label-description">{description}</span>
|
||||
</div>
|
||||
<span className={classnames('currency-input-panel__extra-text', {
|
||||
'currency-input-panel__extra-text--error': errorMessage,
|
||||
})}>
|
||||
{extraText}
|
||||
</span>
|
||||
</div>
|
||||
{this.renderInput()}
|
||||
</div>
|
||||
{this.renderModal()}
|
||||
</div>
|
||||
|
@ -44,6 +44,7 @@ const initialState = {
|
||||
const TOKEN_LABEL_FALLBACK = {
|
||||
'0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359': 'DAI',
|
||||
'0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2': 'MKR',
|
||||
'0x9B913956036a3462330B0642B20D3879ce68b450': 'BAT + ETH'
|
||||
};
|
||||
|
||||
// selectors
|
||||
@ -246,6 +247,7 @@ export const sync = () => async (dispatch, getState) => {
|
||||
}
|
||||
|
||||
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress);
|
||||
const contractBytes32 = contracts[tokenAddress] || new web3.eth.Contract(ERC20_WITH_BYTES_ABI, tokenAddress);
|
||||
|
||||
if (!contracts[tokenAddress]) {
|
||||
dispatch({
|
||||
@ -262,7 +264,17 @@ export const sync = () => async (dispatch, getState) => {
|
||||
const tokenBalance = getBalance(address, tokenAddress);
|
||||
const balance = await contract.methods.balanceOf(address).call();
|
||||
const decimals = tokenBalance.decimals || await contract.methods.decimals().call();
|
||||
const symbol = TOKEN_LABEL_FALLBACK[tokenAddress] || tokenBalance.label || await contract.methods.symbol().call();
|
||||
let symbol = tokenBalance.symbol;
|
||||
|
||||
try {
|
||||
symbol = symbol || await contract.methods.symbol().call().catch();
|
||||
} catch (e) {
|
||||
try {
|
||||
symbol = symbol || web3.utils.hexToString(await contractBytes32.methods.symbol().call().catch());
|
||||
} catch (err) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenBalance.value.isEqualTo(BN(balance)) && tokenBalance.label && tokenBalance.decimals) {
|
||||
return;
|
||||
|
@ -54,6 +54,7 @@ class App extends Component {
|
||||
<Route exact path="/swap" component={Swap} />
|
||||
<Route exact path="/send" component={Send} />
|
||||
<Route exact path="/add-liquidity" component={Pool} />
|
||||
<Route exact path="/remove-liquidity" component={Pool} />
|
||||
<Route exact path="/create-exchange" component={Pool} />
|
||||
<Redirect exact from="/" to="/swap" />
|
||||
</AnimatedSwitch>
|
||||
|
294
src/pages/Pool/RemoveLiquidity.js
Normal file
294
src/pages/Pool/RemoveLiquidity.js
Normal file
@ -0,0 +1,294 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classnames from "classnames";
|
||||
import { connect } from 'react-redux';
|
||||
import { BigNumber as BN } from 'bignumber.js';
|
||||
import NavigationTabs from "../../components/NavigationTabs";
|
||||
import ModeSelector from "./ModeSelector";
|
||||
import CurrencyInputPanel from "../../components/CurrencyInputPanel";
|
||||
import { selectors } from '../../ducks/web3connect';
|
||||
import OversizedPanel from "../../components/OversizedPanel";
|
||||
import ArrowPlus from "../../assets/images/plus-blue.svg";
|
||||
import EXCHANGE_ABI from "../../abi/exchange";
|
||||
import promisify from "../../helpers/web3-promisfy";
|
||||
|
||||
class RemoveLiquidity extends Component {
|
||||
static propTypes = {
|
||||
account: PropTypes.string,
|
||||
balances: PropTypes.object,
|
||||
web3: PropTypes.object,
|
||||
exchangeAddresses: PropTypes.shape({
|
||||
fromToken: PropTypes.object.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
tokenAddress: '',
|
||||
value: '',
|
||||
totalSupply: BN(0),
|
||||
};
|
||||
|
||||
reset() {
|
||||
this.setState({
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
|
||||
validate() {
|
||||
const { tokenAddress, value } = this.state;
|
||||
const { account, selectors, exchangeAddresses: { fromToken }, web3 } = this.props;
|
||||
const exchangeAddress = fromToken[tokenAddress];
|
||||
|
||||
if (!web3 || !exchangeAddress || !account || !value) {
|
||||
return {
|
||||
isValid: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { getBalance } = selectors();
|
||||
|
||||
const { value: liquidityBalance, decimals: liquidityDecimals } = getBalance(account, exchangeAddress);
|
||||
|
||||
if (liquidityBalance.isLessThan(BN(value).multipliedBy(10 ** liquidityDecimals))) {
|
||||
return { isValid: false, errorMessage: 'Insufficient balance' };
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
onTokenSelect = async tokenAddress => {
|
||||
const { exchangeAddresses: { fromToken }, web3 } = this.props;
|
||||
const exchangeAddress = fromToken[tokenAddress];
|
||||
this.setState({ tokenAddress });
|
||||
|
||||
if (!web3 || !exchangeAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress);
|
||||
|
||||
const totalSupply = await exchange.methods.totalSupply().call();
|
||||
this.setState({
|
||||
totalSupply: BN(totalSupply),
|
||||
});
|
||||
};
|
||||
|
||||
onInputChange = value => {
|
||||
this.setState({ value });
|
||||
};
|
||||
|
||||
onRemoveLiquidity = async () => {
|
||||
const { tokenAddress, value: input, totalSupply } = this.state;
|
||||
const {
|
||||
exchangeAddresses: { fromToken },
|
||||
web3,
|
||||
selectors,
|
||||
account,
|
||||
} = this.props;
|
||||
const exchangeAddress = fromToken[tokenAddress];
|
||||
const { getBalance } = selectors();
|
||||
if (!web3 || !exchangeAddress) {
|
||||
return;
|
||||
}
|
||||
const exchange = new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress);
|
||||
const SLIPPAGE = .02;
|
||||
const { decimals } = getBalance(account, exchangeAddress);
|
||||
const { value: ethReserve } = getBalance(exchangeAddress);
|
||||
const { value: tokenReserve } = getBalance(exchangeAddress, tokenAddress);
|
||||
const amount = BN(input).multipliedBy(10 ** decimals);
|
||||
|
||||
const ownership = amount.dividedBy(totalSupply);
|
||||
const ethWithdrawn = ethReserve.multipliedBy(ownership);
|
||||
const tokenWithdrawn = tokenReserve.multipliedBy(ownership);
|
||||
const blockNumber = await promisify(web3, 'getBlockNumber');
|
||||
const block = await promisify(web3, 'getBlock', blockNumber);
|
||||
const deadline = block.timestamp + 300;
|
||||
|
||||
exchange.methods.removeLiquidity(
|
||||
amount.toFixed(0),
|
||||
ethWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0),
|
||||
tokenWithdrawn.multipliedBy(1 - SLIPPAGE).toFixed(0),
|
||||
deadline,
|
||||
).send({ from: account }, (err, data) => {
|
||||
if (data) {
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
getBalance = () => {
|
||||
const {
|
||||
exchangeAddresses: { fromToken },
|
||||
account,
|
||||
web3,
|
||||
selectors,
|
||||
} = this.props;
|
||||
|
||||
const { tokenAddress } = this.state;
|
||||
|
||||
if (!web3) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const exchangeAddress = fromToken[tokenAddress];
|
||||
if (!exchangeAddress) {
|
||||
return '';
|
||||
}
|
||||
const { value, decimals } = selectors().getBalance(account, exchangeAddress);
|
||||
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(7)}`;
|
||||
};
|
||||
|
||||
renderOutput() {
|
||||
const {
|
||||
exchangeAddresses: { fromToken },
|
||||
account,
|
||||
web3,
|
||||
selectors,
|
||||
} = this.props;
|
||||
const { getBalance } = selectors();
|
||||
|
||||
const { tokenAddress, totalSupply, value: input } = this.state;
|
||||
|
||||
|
||||
const exchangeAddress = fromToken[tokenAddress];
|
||||
if (!exchangeAddress || !web3 || !input) {
|
||||
return [
|
||||
<CurrencyInputPanel
|
||||
key="remove-liquidity-input"
|
||||
title="Output"
|
||||
description="(estimated)"
|
||||
renderInput={() => (
|
||||
<div className="remove-liquidity__output"></div>
|
||||
)}
|
||||
disableTokenSelect
|
||||
disableUnlock
|
||||
/>,
|
||||
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
|
||||
<div className="pool__summary-panel">
|
||||
<div className="pool__exchange-rate-wrapper">
|
||||
<span className="pool__exchange-rate">Exchange Rate</span>
|
||||
<span> - </span>
|
||||
</div>
|
||||
<div className="pool__exchange-rate-wrapper">
|
||||
<span className="swap__exchange-rate">Current Pool Size</span>
|
||||
<span> - </span>
|
||||
</div>
|
||||
<div className="pool__exchange-rate-wrapper">
|
||||
<span className="swap__exchange-rate">Your Pool Share</span>
|
||||
<span> - </span>
|
||||
</div>
|
||||
</div>
|
||||
</OversizedPanel>
|
||||
];
|
||||
}
|
||||
const { value, decimals } = getBalance(account, exchangeAddress);
|
||||
const { value: ethReserve } = getBalance(exchangeAddress);
|
||||
const { value: tokenReserve, label } = getBalance(exchangeAddress, tokenAddress);
|
||||
|
||||
const ownership = value.dividedBy(totalSupply);
|
||||
const ethPer = ethReserve.dividedBy(totalSupply);
|
||||
const tokenPer = tokenReserve.dividedBy(totalSupply);
|
||||
|
||||
return [
|
||||
<CurrencyInputPanel
|
||||
title="Output"
|
||||
description="(estimated)"
|
||||
key="remove-liquidity-input"
|
||||
renderInput={() => (
|
||||
<div className="remove-liquidity__output">
|
||||
<div className="remove-liquidity__output-text">
|
||||
{`${ethPer.multipliedBy(input).toFixed(3)} ETH`}
|
||||
</div>
|
||||
<div className="remove-liquidity__output-plus"> + </div>
|
||||
<div className="remove-liquidity__output-text">
|
||||
{`${tokenPer.multipliedBy(input).toFixed(3)} ${label}`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
disableTokenSelect
|
||||
disableUnlock
|
||||
/>,
|
||||
<OversizedPanel key="remove-liquidity-input-under" hideBottom>
|
||||
<div className="pool__summary-panel">
|
||||
<div className="pool__exchange-rate-wrapper">
|
||||
<span className="pool__exchange-rate">Exchange Rate</span>
|
||||
<span>{` ${ethReserve.dividedBy(10 ** 18).toFixed(2)} ETH + ${tokenReserve.dividedBy(10 ** decimals).toFixed(2)} ${label}`}</span>
|
||||
</div>
|
||||
<div className="pool__exchange-rate-wrapper">
|
||||
<span className="swap__exchange-rate">Current Pool Size</span>
|
||||
<span>{totalSupply.dividedBy(10 ** decimals).toFixed(4)}</span>
|
||||
</div>
|
||||
<div className="pool__exchange-rate-wrapper">
|
||||
<span className="swap__exchange-rate">Your Pool Share</span>
|
||||
<span>{ownership.multipliedBy(100).toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</OversizedPanel>
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isConnected } = this.props;
|
||||
const { tokenAddress, value } = this.state;
|
||||
const { isValid, errorMessage } = this.validate();
|
||||
|
||||
return (
|
||||
<div
|
||||
key="content"
|
||||
className={classnames('swap__content', {
|
||||
'swap--inactive': !isConnected,
|
||||
})}
|
||||
>
|
||||
<NavigationTabs
|
||||
className={classnames('header__navigation', {
|
||||
'header--inactive': !isConnected,
|
||||
})}
|
||||
/>
|
||||
<ModeSelector title="Remove Liquidity" />
|
||||
<CurrencyInputPanel
|
||||
title="Pool Tokens"
|
||||
extraText={this.getBalance(tokenAddress)}
|
||||
onValueChange={this.onInputChange}
|
||||
value={value}
|
||||
errorMessage={errorMessage}
|
||||
selectedTokenAddress={tokenAddress}
|
||||
onCurrencySelected={this.onTokenSelect}
|
||||
filteredTokens={['ETH']}
|
||||
/>
|
||||
<OversizedPanel>
|
||||
<div className="swap__down-arrow-background">
|
||||
<img className="swap__down-arrow" src={ArrowPlus} />
|
||||
</div>
|
||||
</OversizedPanel>
|
||||
{ this.renderOutput() }
|
||||
<div className="pool__cta-container">
|
||||
<button
|
||||
className={classnames('pool__cta-btn', {
|
||||
'swap--inactive': !isConnected,
|
||||
'pool__cta-btn--inactive': !isValid,
|
||||
})}
|
||||
disabled={!isValid}
|
||||
onClick={this.onRemoveLiquidity}
|
||||
>
|
||||
Remove Liquidity
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
isConnected: Boolean(state.web3connect.account),
|
||||
web3: state.web3connect.web3,
|
||||
balances: state.web3connect.balances,
|
||||
account: state.web3connect.account,
|
||||
exchangeAddresses: state.addresses.exchangeAddresses,
|
||||
}),
|
||||
dispatch => ({
|
||||
selectors: () => dispatch(selectors()),
|
||||
})
|
||||
)(RemoveLiquidity);
|
@ -2,7 +2,8 @@ import React, { Component } from 'react';
|
||||
import Header from '../../components/Header';
|
||||
import AddLiquidity from './AddLiquidity';
|
||||
import CreateExchange from './CreateExchange';
|
||||
import { Switch, Redirect, Route } from 'react-router-dom';
|
||||
import RemoveLiquidity from './RemoveLiquidity';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import "./pool.scss";
|
||||
import MediaQuery from "react-responsive";
|
||||
|
||||
@ -16,7 +17,7 @@ class Pool extends Component {
|
||||
</MediaQuery>
|
||||
<Switch>
|
||||
<Route exact path="/add-liquidity" component={AddLiquidity} />
|
||||
{/*<Route exact path="/remove" component={Send} />*/}
|
||||
<Route exact path="/remove-liquidity" component={RemoveLiquidity} />
|
||||
<Route exact path="/create-exchange" component={CreateExchange} />
|
||||
</Switch>
|
||||
</div>
|
||||
|
@ -150,3 +150,22 @@
|
||||
color: $dove-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-liquidity {
|
||||
&__output {
|
||||
@extend %row-nowrap;
|
||||
min-height: 3.5rem;
|
||||
}
|
||||
|
||||
&__output-text {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 1rem .75rem;
|
||||
}
|
||||
|
||||
&__output-plus {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
@ -666,7 +666,7 @@ class Send extends Component {
|
||||
</OversizedPanel>
|
||||
);
|
||||
}
|
||||
|
||||
console.log(outputLabel)
|
||||
return (
|
||||
<OversizedPanel hideBottom>
|
||||
<div className="swap__exchange-rate-wrapper">
|
||||
|
@ -450,13 +450,6 @@ class Swap extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0),
|
||||
BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(0),
|
||||
inputAmountB.multipliedBy(1.2).toFixed(0),
|
||||
deadline,
|
||||
outputCurrency,
|
||||
)
|
||||
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency])
|
||||
.methods
|
||||
.tokenToTokenSwapOutput(
|
||||
|
Loading…
Reference in New Issue
Block a user