Add Remove Liquidity (#98)

This commit is contained in:
Chi Kei Chan 2018-10-27 19:32:11 -07:00 committed by GitHub
parent 14b70eebeb
commit c798045590
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 397 additions and 61 deletions

@ -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,16 +250,76 @@ class CurrencyInputPanel extends Component {
);
}
renderInput() {
const {
errorMessage,
value,
onValueChange,
selectedTokenAddress,
disableTokenSelect,
renderInput,
} = this.props;
if (typeof renderInput === 'function') {
return renderInput();
}
return (
<div className="currency-input-panel__input-row">
<input
type="number"
min="0"
className={classnames('currency-input-panel__input',{
'currency-input-panel__input--error': errorMessage,
})}
placeholder="0.0"
onChange={e => onValueChange(e.target.value)}
onKeyPress={e => {
const charCode = e.which ? e.which : e.keyCode;
// Prevent 'minus' character
if (charCode === 45) {
e.preventDefault();
e.stopPropagation();
}
}}
value={value}
/>
{ this.renderUnlockButton() }
<button
className={classnames("currency-input-panel__currency-select", {
'currency-input-panel__currency-select--selected': selectedTokenAddress,
'currency-input-panel__currency-select--disabled': disableTokenSelect,
})}
onClick={() => {
if (!disableTokenSelect) {
this.setState({ isShowingModal: true });
}
}}
>
{
selectedTokenAddress
? (
<TokenLogo
className="currency-input-panel__selected-token-logo"
address={selectedTokenAddress}
/>
)
: null
}
{ TOKEN_ADDRESS_TO_LABEL[selectedTokenAddress] || 'Select a token' }
<span className="currency-input-panel__dropdown-icon" />
</button>
</div>
);
}
render() {
const {
title,
description,
extraText,
errorMessage,
value,
onValueChange,
selectedTokenAddress,
disableTokenSelect,
} = this.props;
return (
@ -277,52 +338,7 @@ class CurrencyInputPanel extends Component {
{extraText}
</span>
</div>
<div className="currency-input-panel__input-row">
<input
type="number"
min="0"
className={classnames('currency-input-panel__input',{
'currency-input-panel__input--error': errorMessage,
})}
placeholder="0.0"
onChange={e => onValueChange(e.target.value)}
onKeyPress={e => {
const charCode = e.which ? e.which : e.keyCode;
// Prevent 'minus' character
if (charCode === 45) {
e.preventDefault();
e.stopPropagation();
}
}}
value={value}
/>
{ this.renderUnlockButton() }
<button
className={classnames("currency-input-panel__currency-select", {
'currency-input-panel__currency-select--selected': selectedTokenAddress,
'currency-input-panel__currency-select--disabled': disableTokenSelect,
})}
onClick={() => {
if (!disableTokenSelect) {
this.setState({ isShowingModal: true });
}
}}
>
{
selectedTokenAddress
? (
<TokenLogo
className="currency-input-panel__selected-token-logo"
address={selectedTokenAddress}
/>
)
: null
}
{ TOKEN_ADDRESS_TO_LABEL[selectedTokenAddress] || 'Select a token' }
<span className="currency-input-panel__dropdown-icon" />
</button>
</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>

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