Implement Add Liquidity (#77)

* CSS fixes

* Add Liquidity UI and Validation

* Finish Add Liquidity
This commit is contained in:
Chi Kei Chan 2018-10-23 15:19:49 -07:00 committed by GitHub
parent a4e0d11cef
commit 509ddaeaa0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 242 additions and 54 deletions

@ -2,25 +2,26 @@
.currency-input-panel { .currency-input-panel {
@extend %col-nowrap; @extend %col-nowrap;
box-shadow: 0 4px 8px 0 rgba($royal-blue, 0.1);
position: relative;
border-radius: 1.25rem;
z-index: 200;
&__container { &__container {
position: relative;
z-index: 200;
border-radius: 1.25rem; border-radius: 1.25rem;
border: 0.5px solid $mercury-gray; box-shadow: 0 0 0 .5px $mercury-gray;
background-color: $white; background-color: $white;
box-shadow: 0px 4px 4px 2px rgba($royal-blue, 0.05);
&--error { &--error {
border: 0.5px solid $salmon-red; box-shadow: 0 0 0 .5px $salmon-red;
} }
&:focus-within { &:focus-within {
border-color: $royal-blue; box-shadow: 0 0 .5px .5px $malibu-blue;
} }
&--error:focus-within { &--error:focus-within {
border-color: $salmon-red; box-shadow: 0 0 .5px .5px $salmon-red;
} }
} }
@ -93,6 +94,12 @@
background-image: url(../../assets/images/dropdown.svg); background-image: url(../../assets/images/dropdown.svg);
} }
} }
&--disabled {
.currency-input-panel__dropdown-icon {
opacity: 0;
}
}
} }
&__sub-currency-select { &__sub-currency-select {

@ -45,10 +45,13 @@ class CurrencyInputPanel extends Component {
selectedTokens: PropTypes.array.isRequired, selectedTokens: PropTypes.array.isRequired,
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
selectedTokenAddress: PropTypes.string, selectedTokenAddress: PropTypes.string,
disableTokenSelect: PropTypes.bool,
filteredTokens: PropTypes.arrayOf(PropTypes.string),
}; };
static defaultProps = { static defaultProps = {
selectedTokens: [], selectedTokens: [],
filteredTokens: [],
onCurrencySelected() {}, onCurrencySelected() {},
onValueChange() {}, onValueChange() {},
selectedTokenAddress: '', selectedTokenAddress: '',
@ -64,19 +67,20 @@ class CurrencyInputPanel extends Component {
}; };
createTokenList = () => { createTokenList = () => {
const { filteredTokens } = this.props;
let tokens = this.props.tokenAddresses.addresses; let tokens = this.props.tokenAddresses.addresses;
let tokenList = [ { value: 'ETH', label: 'ETH', address: 'ETH' } ]; let tokenList = [ { value: 'ETH', label: 'ETH', address: 'ETH' } ];
for (let i = 0; i < tokens.length; i++) { for (let i = 0; i < tokens.length; i++) {
let entry = { value: '', label: '' }; let entry = { value: '', label: '' };
entry.value = tokens[i][0]; entry.value = tokens[i][0];
entry.label = tokens[i][0]; entry.label = tokens[i][0];
entry.address = tokens[i][1]; entry.address = tokens[i][1];
tokenList.push(entry); tokenList.push(entry);
TOKEN_ADDRESS_TO_LABEL[tokens[i][1]] = tokens[i][0]; TOKEN_ADDRESS_TO_LABEL[tokens[i][1]] = tokens[i][0];
} }
return tokenList; return tokenList.filter(({ address }) => !filteredTokens.includes(address));
}; };
onTokenSelect = (address) => { onTokenSelect = (address) => {
@ -122,7 +126,12 @@ class CurrencyInputPanel extends Component {
renderTokenList() { renderTokenList() {
const tokens = this.createTokenList(); const tokens = this.createTokenList();
const { searchQuery } = this.state; const { searchQuery } = this.state;
const { selectedTokens } = this.props; const { selectedTokens, disableTokenSelect } = this.props;
if (disableTokenSelect) {
return;
}
let results; let results;
if (!searchQuery) { if (!searchQuery) {
@ -196,6 +205,7 @@ class CurrencyInputPanel extends Component {
value, value,
onValueChange, onValueChange,
selectedTokenAddress, selectedTokenAddress,
disableTokenSelect,
} = this.props; } = this.props;
return ( return (
@ -232,8 +242,13 @@ class CurrencyInputPanel extends Component {
<button <button
className={classnames("currency-input-panel__currency-select", { className={classnames("currency-input-panel__currency-select", {
'currency-input-panel__currency-select--selected': selectedTokenAddress, 'currency-input-panel__currency-select--selected': selectedTokenAddress,
'currency-input-panel__currency-select--disabled': disableTokenSelect,
})} })}
onClick={() => this.setState({ isShowingModal: true })} onClick={() => {
if (!disableTokenSelect) {
this.setState({ isShowingModal: true });
}
}}
> >
{ {
selectedTokenAddress selectedTokenAddress

@ -6,14 +6,14 @@
height: 3rem; height: 3rem;
background-color: $concrete-gray; background-color: $concrete-gray;
border-radius: 3rem; border-radius: 3rem;
border: 1px solid $mercury-gray; box-shadow: 0 0 0 .5px darken($concrete-gray, 5);
.tab:first-child { .tab:first-child {
margin-left: -1px; //margin-left: -1px;
} }
.tab:last-child { .tab:last-child {
margin-right: -1px; //margin-right: -1px;
} }
} }
@ -24,7 +24,6 @@
height: 3rem; height: 3rem;
flex: 1 0 auto; flex: 1 0 auto;
border-radius: 3rem; border-radius: 3rem;
border: 1px solid transparent;
transition: 300ms ease-in-out; transition: 300ms ease-in-out;
cursor: pointer; cursor: pointer;
@ -36,7 +35,7 @@
&--selected { &--selected {
background-color: $white; background-color: $white;
border-radius: 3rem; border-radius: 3rem;
border: 1px solid $mercury-gray; box-shadow: 0 0 .5px .5px $mercury-gray;
font-weight: 500; font-weight: 500;
span { span {

@ -17,6 +17,7 @@ export const ADD_CONTRACT = 'web3connect/addContract';
const initialState = { const initialState = {
web3: null, web3: null,
initialized: false,
account: '', account: '',
balances: { balances: {
ethereum: {}, ethereum: {},
@ -39,7 +40,7 @@ export const selectors = () => (dispatch, getState) => {
return { return {
getBalance: address => { getBalance: address => {
const balance = state.balances.ethereum[address]; const balance = state.balances.ethereum[address];
console.log({balance})
if (!balance) { if (!balance) {
dispatch(watchBalance({ balanceOf: address })); dispatch(watchBalance({ balanceOf: address }));
return Balance(0, 'ETH'); return Balance(0, 'ETH');
@ -98,7 +99,6 @@ export const initialize = () => (dispatch, getState) => {
if (typeof window.web3 !== 'undefined') { if (typeof window.web3 !== 'undefined') {
const web3 = new Web3(window.web3.currentProvider); const web3 = new Web3(window.web3.currentProvider);
await window.ethereum.enable();
dispatch({ dispatch({
type: INITIALIZE, type: INITIALIZE,
payload: web3, payload: web3,
@ -108,28 +108,38 @@ export const initialize = () => (dispatch, getState) => {
}) })
}; };
export const watchBalance = ({ balanceOf, tokenAddress }) => { export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState) => {
if (!balanceOf) { if (!balanceOf) {
return { type: '' }; return;
} }
const { web3connect } = getState();
const { watched } = web3connect;
if (!tokenAddress) { if (!tokenAddress) {
return { if (watched.balances.ethereum.includes(balanceOf)) {
return;
}
dispatch({
type: WATCH_ETH_BALANCE, type: WATCH_ETH_BALANCE,
payload: balanceOf, payload: balanceOf,
}; });
} else if (tokenAddress) { } else if (tokenAddress) {
return { if (watched.balances[tokenAddress] && watched.balances[tokenAddress].includes(balanceOf)) {
return;
}
dispatch({
type: WATCH_TOKEN_BALANCE, type: WATCH_TOKEN_BALANCE,
payload: { payload: {
tokenAddress, tokenAddress,
balanceOf, balanceOf,
}, },
}; });
} }
} };
export const sync = () => async (dispatch, getState) => { export const sync = () => async (dispatch, getState) => {
const { getBalance, getTokenBalance } = dispatch(selectors());
const web3 = await dispatch(initialize()); const web3 = await dispatch(initialize());
const { const {
account, account,
@ -142,12 +152,17 @@ export const sync = () => async (dispatch, getState) => {
if (account !== accounts[0]) { if (account !== accounts[0]) {
dispatch({ type: UPDATE_ACCOUNT, payload: accounts[0] }); dispatch({ type: UPDATE_ACCOUNT, payload: accounts[0] });
dispatch(watchBalance({ balanceOf: accounts[0] })); dispatch(watchBalance({ balanceOf: accounts[0] }));
// dispatch(watchBalance({ balanceOf: accounts[0], tokenAddress: '0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B' }));
} }
// Sync Ethereum Balances // Sync Ethereum Balances
watched.balances.ethereum.forEach(async address => { watched.balances.ethereum.forEach(async address => {
const balance = await web3.eth.getBalance(address); const balance = await web3.eth.getBalance(address);
const { value } = getBalance(address);
if (value.isEqualTo(BN(balance))) {
return;
}
dispatch({ dispatch({
type: UPDATE_ETH_BALANCE, type: UPDATE_ETH_BALANCE,
payload: { payload: {
@ -178,9 +193,16 @@ export const sync = () => async (dispatch, getState) => {
const watchlist = watched.balances[tokenAddress] || []; const watchlist = watched.balances[tokenAddress] || [];
watchlist.forEach(async address => { watchlist.forEach(async address => {
const tokenBalance = getTokenBalance(tokenAddress, address);
const balance = await contract.methods.balanceOf(address).call(); const balance = await contract.methods.balanceOf(address).call();
const decimals = await contract.methods.decimals().call(); const decimals = tokenBalance.decimals || await contract.methods.decimals().call();
const symbol = await contract.methods.symbol().call(); const symbol = tokenBalance.label || await contract.methods.symbol().call();
if (tokenBalance.value.isEqualTo(BN(balance))) {
console.log('block');
return;
}
dispatch({ dispatch({
type: UPDATE_TOKEN_BALANCE, type: UPDATE_TOKEN_BALANCE,
payload: { payload: {
@ -200,7 +222,6 @@ export const startWatching = () => async (dispatch, getState) => {
: 5000; : 5000;
dispatch(sync()); dispatch(sync());
setTimeout(() => dispatch(startWatching()), timeout); setTimeout(() => dispatch(startWatching()), timeout);
}; };

@ -11,7 +11,7 @@ window.addEventListener('load', function() {
<DrizzleProvider options={{ <DrizzleProvider options={{
contracts: [], contracts: [],
events: [], events: [],
polls: { accounts: 3000, blocks: 3000 }, polls: { accounts: 60000, blocks: 60000 },
}} store={store}> }} store={store}>
<App /> <App />
</DrizzleProvider> </DrizzleProvider>

@ -8,7 +8,9 @@ import { selectors, sync } from '../../ducks/web3connect';
import ArrowDown from '../../assets/images/arrow-down-blue.svg'; import ArrowDown from '../../assets/images/arrow-down-blue.svg';
import ModeSelector from './ModeSelector'; import ModeSelector from './ModeSelector';
import {BigNumber as BN} from 'bignumber.js'; import {BigNumber as BN} from 'bignumber.js';
import EXCHANGE_ABI from '../../abi/exchange';
import "./pool.scss"; import "./pool.scss";
import promisify from "../../helpers/web3-promisfy";
const INPUT = 0; const INPUT = 0;
const OUTPUT = 1; const OUTPUT = 1;
@ -18,6 +20,7 @@ class AddLiquidity extends Component {
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired,
account: PropTypes.string.isRequired, account: PropTypes.string.isRequired,
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
balances: PropTypes.object.isRequired,
exchangeAddresses: PropTypes.shape({ exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired, fromToken: PropTypes.object.isRequired,
}).isRequired, }).isRequired,
@ -26,11 +29,27 @@ class AddLiquidity extends Component {
state = { state = {
inputValue: '', inputValue: '',
outputValue: '', outputValue: '',
inputCurrency: '', inputCurrency: 'ETH',
outputCurrency: '', outputCurrency: '',
lastEditedField: '', lastEditedField: '',
}; };
shouldComponentUpdate(nextProps, nextState) {
const { isConnected, account, exchangeAddresses, balances, web3 } = this.props;
const { inputValue, outputValue, inputCurrency, outputCurrency, lastEditedField } = this.state;
return isConnected !== nextProps.isConnected ||
account !== nextProps.account ||
exchangeAddresses !== nextProps.exchangeAddresses ||
web3 !== nextProps.web3 ||
balances !== nextProps.balances ||
inputValue !== nextState.inputValue ||
outputValue !== nextState.outputValue ||
inputCurrency !== nextState.inputCurrency ||
outputCurrency !== nextState.outputCurrency ||
lastEditedField !== nextState.lastEditedField;
}
getBalance(currency) { getBalance(currency) {
const { selectors, account } = this.props; const { selectors, account } = this.props;
@ -47,6 +66,34 @@ class AddLiquidity extends Component {
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`; return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
} }
onAddLiquidity = async () => {
const { account, web3, exchangeAddresses: { fromToken }, selectors } = this.props;
const { inputValue, outputValue, outputCurrency } = this.state;
const exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]);
const ethAmount = BN(inputValue).multipliedBy(10 ** 18);
const { decimals } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]);
const tokenAmount = BN(outputValue).multipliedBy(10 ** decimals);
const { value: ethReserve } = selectors().getBalance(fromToken[outputCurrency]);
const totalLiquidity = await exchange.methods.totalSupply().call();
const liquidityMinted = BN(totalLiquidity).multipliedBy(ethAmount.dividedBy(ethReserve));
const blockNumber = await promisify(web3, 'getBlockNumber');
const block = await promisify(web3, 'getBlock', blockNumber);
const deadline = block.timestamp + 300;
const MAX_LIQUIDITY_SLIPPAGE = 0.025;
const minLiquidity = liquidityMinted.multipliedBy(1 - MAX_LIQUIDITY_SLIPPAGE);
const maxTokens = tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE);
try {
const tx = await exchange.methods.addLiquidity(minLiquidity.toFixed(0), maxTokens.toFixed(0), deadline).send({
from: account,
value: ethAmount.toFixed(0)
});
} catch (err) {
console.error(err);
}
};
onInputChange = value => { onInputChange = value => {
const { inputCurrency, outputCurrency } = this.state; const { inputCurrency, outputCurrency } = this.state;
const exchangeRate = this.getExchangeRate(); const exchangeRate = this.getExchangeRate();
@ -103,6 +150,39 @@ class AddLiquidity extends Component {
return tokenValue.dividedBy(ethValue); return tokenValue.dividedBy(ethValue);
} }
validate() {
const { selectors, account } = this.props;
const {
inputValue, outputValue,
inputCurrency, outputCurrency,
} = this.state;
let inputError;
let outputError;
let isValid = true;
if (!inputValue || !outputValue || !inputCurrency || !outputCurrency) {
isValid = false;
}
const { value: ethValue } = selectors().getBalance(account);
const { value: tokenValue, decimals } = selectors().getTokenBalance(outputCurrency, account);
if (ethValue.isLessThan(BN(inputValue * 10 ** 18))) {
inputError = 'Insufficient Balance';
}
if (tokenValue.isLessThan(BN(outputValue * 10 ** decimals))) {
outputError = 'Insufficient Balance';
}
return {
inputError,
outputError,
isValid: isValid && !inputError && !outputError,
};
}
renderInfo() { renderInfo() {
const { selectors, exchangeAddresses: { fromToken } } = this.props; const { selectors, exchangeAddresses: { fromToken } } = this.props;
const { inputCurrency, outputCurrency } = this.state; const { inputCurrency, outputCurrency } = this.state;
@ -136,11 +216,71 @@ class AddLiquidity extends Component {
<div className="pool__summary-panel"> <div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">Exchange Rate</span> <span className="pool__exchange-rate">Exchange Rate</span>
<span>{`1 ETH = ${tokenValue.dividedBy(ethValue).toFixed(4)} BAT`}</span> <span>{`1 ETH = ${tokenValue.dividedBy(ethValue).toFixed(4)} ${label}`}</span>
</div> </div>
<div className="pool__exchange-rate-wrapper"> <div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">Current Pool Size</span> <span className="swap__exchange-rate">Current Pool Size</span>
<span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} / ${tokenValue.dividedBy(10 ** decimals).toFixed(2)} ${label}`}</span> <span>{` ${ethValue.dividedBy(10 ** 18).toFixed(2)} ${eth} + ${tokenValue.dividedBy(10 ** decimals).toFixed(2)} ${label}`}</span>
</div>
</div>
)
}
renderSummary() {
const { selectors, exchangeAddresses: { fromToken } } = this.props;
const {
inputValue,
outputValue,
inputCurrency,
outputCurrency,
} = this.state;
if (!inputCurrency || !outputCurrency) {
return (
<div className="swap__summary-wrapper">
<div>Select a token to continue.</div>
</div>
)
}
if (inputCurrency === outputCurrency) {
return (
<div className="swap__summary-wrapper">
<div>Must be different token.</div>
</div>
)
}
if (![inputCurrency, outputCurrency].includes('ETH')) {
return (
<div className="swap__summary-wrapper">
<div>One of the input must be ETH.</div>
</div>
)
}
const { value, decimals, label } = selectors().getTokenBalance(outputCurrency, fromToken[outputCurrency]);
if (!inputValue || !outputValue) {
return (
<div className="swap__summary-wrapper">
<div>{`Enter a ${inputCurrency} or ${label} value to continue.`}</div>
</div>
)
}
const SLIPPAGE = 0.025;
const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE);
const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE);
const tokenReserve = value.dividedBy(10 ** decimals);
const minPercentage = minOutput.dividedBy(minOutput.plus(tokenReserve)).multipliedBy(100);
const maxPercentage = maxOutput.dividedBy(maxOutput.plus(tokenReserve)).multipliedBy(100);
return (
<div className="swap__summary-wrapper">
<div>You are adding between {b(`${minOutput.toFixed(2)} - ${maxOutput.toFixed(2)} ${label}`)} + {b(`${BN(inputValue).toFixed(2)} ETH`)} into the liquidity pool.</div>
<div className="pool__last-summary-text">
You will receive between {b(`${minPercentage.toFixed(2)}%`)} and {b(`${maxPercentage.toFixed(2)}%`)} of the {`${label}/ETH`} pool tokens.
</div> </div>
</div> </div>
) )
@ -159,19 +299,23 @@ class AddLiquidity extends Component {
lastEditedField, lastEditedField,
} = this.state; } = this.state;
const { inputError, outputError, isValid } = this.validate();
return ( return (
<div className={classnames('swap__content', { 'swap--inactive': !isConnected })}> <div
className={classnames('swap__content', {
'swap--inactive': !isConnected,
})}
>
<ModeSelector /> <ModeSelector />
<CurrencyInputPanel <CurrencyInputPanel
title="Deposit" title="Deposit"
description={lastEditedField === OUTPUT ? '(estimated)' : ''}
extraText={this.getBalance(inputCurrency)} extraText={this.getBalance(inputCurrency)}
onCurrencySelected={currency => {
this.setState({ inputCurrency: currency });
this.props.sync();
}}
onValueChange={this.onInputChange} onValueChange={this.onInputChange}
selectedTokenAddress="ETH"
value={inputValue} value={inputValue}
errorMessage={inputError}
disableTokenSelect
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
@ -180,32 +324,32 @@ class AddLiquidity extends Component {
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title="Deposit" title="Deposit"
description={lastEditedField === INPUT ? '(estimated)' : ''} description="(estimated)"
extraText={this.getBalance(outputCurrency)} extraText={this.getBalance(outputCurrency)}
selectedTokenAddress={outputCurrency}
onCurrencySelected={currency => { onCurrencySelected={currency => {
this.setState({ outputCurrency: currency }); this.setState({ outputCurrency: currency });
this.props.sync(); this.props.sync();
}} }}
onValueChange={this.onOutputChange} onValueChange={this.onOutputChange}
value={outputValue} value={outputValue}
errorMessage={outputError}
filteredTokens={[ 'ETH' ]}
/> />
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
{ this.renderInfo() } { this.renderInfo() }
</OversizedPanel> </OversizedPanel>
<div className="swap__summary-wrapper"> { this.renderSummary() }
<div>You are adding between {b`212000.00 - 216000.00 BAT`} + {b`166.683543 ETH`} into the liquidity pool.</div>
<div className="pool__last-summary-text">You will receive between {b`66%`} and {b`67%`} of the BAT/ETH pool tokens.</div>
</div>
<div className="pool__cta-container"> <div className="pool__cta-container">
<button <button
className={classnames('pool__cta-btn', { className={classnames('pool__cta-btn', {
'swap--inactive': !this.props.isConnected, 'swap--inactive': !this.props.isConnected,
'pool__cta-btn--inactive': !this.props.isValid, 'pool__cta-btn--inactive': !isValid,
})} })}
disabled={!this.props.isValid} disabled={!isValid}
onClick={this.onSwap} onClick={this.onAddLiquidity}
> >
Swap Add Liquidity
</button> </button>
</div> </div>
</div> </div>
@ -219,6 +363,7 @@ export default drizzleConnect(
isConnected: Boolean(state.web3connect.account), isConnected: Boolean(state.web3connect.account),
account: state.web3connect.account, account: state.web3connect.account,
balances: state.web3connect.balances, balances: state.web3connect.balances,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses, exchangeAddresses: state.addresses.exchangeAddresses,
}), }),
dispatch => ({ dispatch => ({

@ -11,6 +11,7 @@ $mine-shaft-gray: #2B2B2B;
// Blue // Blue
$zumthor-blue: #EBF4FF; $zumthor-blue: #EBF4FF;
$malibu-blue: #5CA2FF;
$royal-blue: #2F80ED; $royal-blue: #2F80ED;
// Purple // Purple