Commit 6ea7b6ac authored by Chi Kei Chan's avatar Chi Kei Chan Committed by GitHub

Add Web3Connect Watcher to replace drizzle (#74)

* Implement Basic web3connect

* Web3 Connect balance watching

* Partial refactor of Swap; Finish calc for AddLiquidity
parent 57a05471
......@@ -5,6 +5,7 @@ import exchangeContracts from './exchange-contract';
import tokenContracts from './token-contract';
import exchange from './exchange';
import swap from './swap';
import web3connect from './web3connect';
export default combineReducers({
addresses,
......@@ -12,5 +13,6 @@ export default combineReducers({
tokenContracts,
exchange,
swap,
web3connect,
...drizzleReducers,
});
import React, { Component } from 'react';
import PropTypes from 'prop-types';
// import { connect } from 'react-redux';
import { drizzleConnect } from 'drizzle-react';
import {BigNumber as BN} from 'bignumber.js';
import Web3 from 'web3';
import ERC20_ABI from "../abi/erc20";
export const INITIALIZE = 'we3connect/initialize';
export const UPDATE_ACCOUNT = 'we3connect/updateAccount';
export const WATCH_ETH_BALANCE = 'web3connect/watchEthBalance';
export const WATCH_TOKEN_BALANCE = 'web3connect/watchTokenBalance';
export const UPDATE_ETH_BALANCE = 'web3connect/updateEthBalance';
export const UPDATE_TOKEN_BALANCE = 'web3connect/updateTokenBalance';
export const ADD_CONTRACT = 'web3connect/addContract';
const initialState = {
web3: null,
account: '',
balances: {
ethereum: {},
},
pendingTransactions: [],
transactions: {},
errorMessage: '',
watched: {
balances: {
ethereum: [],
},
},
contracts: {},
};
// selectors
export const selectors = () => (dispatch, getState) => {
const state = getState().web3connect;
return {
getBalance: address => {
const balance = state.balances.ethereum[address];
console.log({balance})
if (!balance) {
dispatch(watchBalance({ balanceOf: address }));
return Balance(0, 'ETH');
}
return balance;
},
getTokenBalance: (tokenAddress, address) => {
const tokenBalances = state.balances[tokenAddress];
if (!tokenBalances) {
dispatch(watchBalance({ balanceOf: address, tokenAddress }));
return Balance(0);
}
const balance = tokenBalances[address];
if (!balance) {
dispatch(watchBalance({ balanceOf: address, tokenAddress }));
return Balance(0);
}
return balance;
},
}
};
const Balance = (value, label = '', decimals = 18) => ({
value: BN(value),
label: label.toUpperCase(),
decimals: +decimals,
});
export const initialize = () => (dispatch, getState) => {
const { web3connect } = getState();
return new Promise(async resolve => {
if (web3connect.web3) {
resolve(web3connect.web3);
return;
}
if (typeof window.ethereum !== 'undefined') {
try {
const web3 = new Web3(window.ethereum);
await window.ethereum.enable();
dispatch({
type: INITIALIZE,
payload: web3,
});
resolve(web3);
return;
} catch (error) {
console.error('User denied access.');
return;
}
}
if (typeof window.web3 !== 'undefined') {
const web3 = new Web3(window.web3.currentProvider);
await window.ethereum.enable();
dispatch({
type: INITIALIZE,
payload: web3,
});
resolve(web3);
}
})
};
export const watchBalance = ({ balanceOf, tokenAddress }) => {
if (!balanceOf) {
return { type: '' };
}
if (!tokenAddress) {
return {
type: WATCH_ETH_BALANCE,
payload: balanceOf,
};
} else if (tokenAddress) {
return {
type: WATCH_TOKEN_BALANCE,
payload: {
tokenAddress,
balanceOf,
},
};
}
}
export const sync = () => async (dispatch, getState) => {
const web3 = await dispatch(initialize());
const {
account,
watched,
contracts,
} = getState().web3connect;
// Sync Account
const accounts = await web3.eth.getAccounts();
if (account !== accounts[0]) {
dispatch({ type: UPDATE_ACCOUNT, payload: accounts[0] });
dispatch(watchBalance({ balanceOf: accounts[0] }));
// dispatch(watchBalance({ balanceOf: accounts[0], tokenAddress: '0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B' }));
}
// Sync Ethereum Balances
watched.balances.ethereum.forEach(async address => {
const balance = await web3.eth.getBalance(address);
dispatch({
type: UPDATE_ETH_BALANCE,
payload: {
balance: Balance(balance, 'ETH', 18),
balanceOf: address,
},
})
});
// Sync Token Balances
Object.keys(watched.balances)
.forEach(tokenAddress => {
if (tokenAddress === 'ethereum') {
return;
}
const contract = contracts[tokenAddress] || new web3.eth.Contract(ERC20_ABI, tokenAddress);
if (!contracts[tokenAddress]) {
dispatch({
type: ADD_CONTRACT,
payload: {
address: tokenAddress,
contract: contract,
},
});
}
const watchlist = watched.balances[tokenAddress] || [];
watchlist.forEach(async address => {
const balance = await contract.methods.balanceOf(address).call();
const decimals = await contract.methods.decimals().call();
const symbol = await contract.methods.symbol().call();
dispatch({
type: UPDATE_TOKEN_BALANCE,
payload: {
tokenAddress,
balanceOf: address,
balance: Balance(balance, symbol, decimals),
},
});
});
});
};
export const startWatching = () => async (dispatch, getState) => {
const { account } = getState().web3connect;
const timeout = !account
? 1000
: 5000;
dispatch(sync());
setTimeout(() => dispatch(startWatching()), timeout);
};
export default function web3connectReducer(state = initialState, { type, payload }) {
switch (type) {
case INITIALIZE:
return { ...state, web3: payload };
case UPDATE_ACCOUNT:
return {
...state,
account: payload,
};
case WATCH_ETH_BALANCE:
return {
...state,
watched: {
...state.watched,
balances: {
...state.watched.balances,
ethereum: [ ...state.watched.balances.ethereum, payload ],
},
},
};
case WATCH_TOKEN_BALANCE:
const { watched } = state;
const { balances } = watched;
const watchlist = balances[payload.tokenAddress] || [];
return {
...state,
watched: {
...watched,
balances: {
...balances,
[payload.tokenAddress]: [ ...watchlist, payload.balanceOf ],
},
},
};
case UPDATE_ETH_BALANCE:
return {
...state,
balances: {
...state.balances,
ethereum: {
...state.balances.ethereum,
[payload.balanceOf]: payload.balance,
},
},
};
case UPDATE_TOKEN_BALANCE:
const tokenBalances = state.balances[payload.tokenAddress] || {};
return {
...state,
balances: {
...state.balances,
[payload.tokenAddress]: {
...tokenBalances,
[payload.balanceOf]: payload.balance,
},
},
};
case ADD_CONTRACT:
return {
...state,
contracts: {
...state.contracts,
[payload.address]: payload.contract,
},
};
default:
return state;
}
}
// Connect Component
export class _Web3Connect extends Component {
static propTypes = {
initialize: PropTypes.func.isRequired,
};
static defaultProps = {
initialize() {}
};
componentWillMount() {
this.props.initialize()
.then(this.props.startWatching());
}
render() {
return <noscript />;
}
}
export const Web3Connect = drizzleConnect(
_Web3Connect,
({ web3connect }) => ({
web3: web3connect.web3,
}),
dispatch => ({
initialize: () => dispatch(initialize()),
startWatching: () => dispatch(startWatching()),
}),
);
......@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react'
import { BrowserRouter, Switch, Redirect, Route } from 'react-router-dom';
import { AnimatedSwitch } from 'react-router-transition';
import { Web3Connect } from '../ducks/web3connect';
import Swap from './Swap';
import Send from './Send';
import Pool from './Pool';
......@@ -15,19 +16,22 @@ class App extends Component {
}
return (
<BrowserRouter>
<AnimatedSwitch
atEnter={{ opacity: 0 }}
atLeave={{ opacity: 0 }}
atActive={{ opacity: 1 }}
className="app__switch-wrapper"
>
<Route exact path="/swap" component={Swap} />
<Route exact path="/send" component={Send} />
<Route exact path="/pool" component={Pool} />
<Redirect exact from="/" to="/swap" />
</AnimatedSwitch>
</BrowserRouter>
<div id="app-container">
<Web3Connect />
<BrowserRouter>
<AnimatedSwitch
atEnter={{ opacity: 0 }}
atLeave={{ opacity: 0 }}
atActive={{ opacity: 1 }}
className="app__switch-wrapper"
>
<Route exact path="/swap" component={Swap} />
<Route exact path="/send" component={Send} />
<Route exact path="/pool" component={Pool} />
<Redirect exact from="/" to="/swap" />
</AnimatedSwitch>
</BrowserRouter>
</div>
);
}
}
......
......@@ -13,4 +13,10 @@
bottom: 0;
}
}
}
#app-container {
width: 100vw;
height: 100vh;
@extend %col-nowrap;
}
\ No newline at end of file
......@@ -4,27 +4,174 @@ import PropTypes from 'prop-types';
import classnames from "classnames";
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
import OversizedPanel from '../../components/OversizedPanel';
import { selectors, sync } from '../../ducks/web3connect';
import ArrowDown from '../../assets/images/arrow-down-blue.svg';
import ModeSelector from './ModeSelector';
import {BigNumber as BN} from 'bignumber.js';
import "./pool.scss";
const INPUT = 0;
const OUTPUT = 1;
class AddLiquidity extends Component {
static propTypes = {
currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
account: PropTypes.string.isRequired,
selectors: PropTypes.func.isRequired,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired,
}).isRequired,
};
state = {
inputValue: '',
outputValue: '',
inputCurrency: '',
outputCurrency: '',
lastEditedField: '',
};
getBalance(currency) {
const { selectors, account } = this.props;
if (!currency) {
return '';
}
if (currency === 'ETH') {
const { value, decimals } = selectors().getBalance(account);
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
}
const { value, decimals } = selectors().getTokenBalance(currency, account);
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
}
onInputChange = value => {
const { inputCurrency, outputCurrency } = this.state;
const exchangeRate = this.getExchangeRate();
let outputValue;
if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') {
outputValue = exchangeRate.multipliedBy(value).toFixed(7);
}
if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') {
outputValue = BN(value).dividedBy(exchangeRate).toFixed(7);
}
this.setState({
outputValue,
inputValue: value,
lastEditedField: INPUT,
});
};
onOutputChange = value => {
const { inputCurrency, outputCurrency } = this.state;
const exchangeRate = this.getExchangeRate();
let inputValue;
if (inputCurrency === 'ETH' && outputCurrency && outputCurrency !== 'ETH') {
inputValue = BN(value).dividedBy(exchangeRate).toFixed(7);
}
if (outputCurrency === 'ETH' && inputCurrency && inputCurrency !== 'ETH') {
inputValue = exchangeRate.multipliedBy(value).toFixed(7);
}
this.setState({
inputValue,
outputValue: value,
lastEditedField: OUTPUT,
});
};
getExchangeRate() {
const { selectors, exchangeAddresses: { fromToken } } = this.props;
const { inputCurrency, outputCurrency } = this.state;
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0];
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0];
if (!eth || !token) {
return;
}
const { value: tokenValue } = selectors().getTokenBalance(token, fromToken[token]);
const { value: ethValue } = selectors().getBalance(fromToken[token]);
return tokenValue.dividedBy(ethValue);
}
renderInfo() {
const { selectors, exchangeAddresses: { fromToken } } = this.props;
const { inputCurrency, outputCurrency } = this.state;
const eth = [inputCurrency, outputCurrency].filter(currency => currency === 'ETH')[0];
const token = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0];
if (!eth || !token) {
return (
<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>
)
}
const {
value: tokenValue,
decimals,
label
} = selectors().getTokenBalance(token, fromToken[token]);
const { value: ethValue } = selectors().getBalance(fromToken[token]);
return (
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">Exchange Rate</span>
<span>{`1 ETH = ${tokenValue.dividedBy(ethValue).toFixed(4)} BAT`}</span>
</div>
<div className="pool__exchange-rate-wrapper">
<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>
</div>
</div>
)
}
render() {
const {
isConnected,
} = this.props;
const {
inputValue,
outputValue,
inputCurrency,
outputCurrency,
lastEditedField,
} = this.state;
return (
<div
className={classnames('swap__content', {
'swap--inactive': !this.props.isConnected,
})}
>
<div className={classnames('swap__content', { 'swap--inactive': !isConnected })}>
<ModeSelector />
<CurrencyInputPanel
title="Deposit"
extraText="Balance: 0.03141"
description={lastEditedField === OUTPUT ? '(estimated)' : ''}
extraText={this.getBalance(inputCurrency)}
onCurrencySelected={currency => {
this.setState({ inputCurrency: currency });
this.props.sync();
}}
onValueChange={this.onInputChange}
value={inputValue}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
......@@ -33,20 +180,17 @@ class AddLiquidity extends Component {
</OversizedPanel>
<CurrencyInputPanel
title="Deposit"
description="(estimated)"
extraText="Balance: 0.0"
description={lastEditedField === INPUT ? '(estimated)' : ''}
extraText={this.getBalance(outputCurrency)}
onCurrencySelected={currency => {
this.setState({ outputCurrency: currency });
this.props.sync();
}}
onValueChange={this.onOutputChange}
value={outputValue}
/>
<OversizedPanel hideBottom>
<div className="pool__summary-panel">
<div className="pool__exchange-rate-wrapper">
<span className="pool__exchange-rate">Exchange Rate</span>
<span>1 ETH = 1283.878 BAT</span>
</div>
<div className="pool__exchange-rate-wrapper">
<span className="swap__exchange-rate">Current Pool Size</span>
<span>321 ETH / 321,000 BAT</span>
</div>
</div>
{ this.renderInfo() }
</OversizedPanel>
<div className="swap__summary-wrapper">
<div>You are adding between {b`212000.00 - 216000.00 BAT`} + {b`166.683543 ETH`} into the liquidity pool.</div>
......@@ -71,10 +215,16 @@ class AddLiquidity extends Component {
export default drizzleConnect(
AddLiquidity,
(state, ownProps) => ({
currentAddress: state.accounts[0],
isConnected: !!(state.drizzleStatus.initialized && state.accounts[0]),
state => ({
isConnected: Boolean(state.web3connect.account),
account: state.web3connect.account,
balances: state.web3connect.balances,
exchangeAddresses: state.addresses.exchangeAddresses,
}),
dispatch => ({
selectors: () => dispatch(selectors()),
sync: () => dispatch(sync()),
})
)
function b(text) {
......
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { drizzleConnect } from 'drizzle-react';
import Header from '../../components/Header';
import AddLiquidity from './AddLiquidity';
import "./pool.scss";
......@@ -10,14 +7,6 @@ const ADD_LIQUIDITY = 'Add Liquidity';
const REMOVE_LIQUIDITY = 'Remove Liquidity';
class Pool extends Component {
static propTypes = {
// Injected by React Router Dom
push: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
};
state = {
selectedMode: ADD_LIQUIDITY,
};
......@@ -40,14 +29,4 @@ class Pool extends Component {
}
}
export default withRouter(
drizzleConnect(
Pool,
(state, ownProps) => ({
push: ownProps.history.push,
pathname: ownProps.location.pathname,
currentAddress: state.accounts[0],
isConnected: !!(state.drizzleStatus.initialized && state.accounts[0]),
}),
),
);
export default Pool;
......@@ -6,6 +6,7 @@ import classnames from 'classnames';
import {BigNumber as BN} from "bignumber.js";
import deepEqual from 'deep-equal';
import { isValidSwap, updateField, addError, removeError } from '../../ducks/swap';
import { selectors, sync } from '../../ducks/web3connect';
import Header from '../../components/Header';
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
import OversizedPanel from '../../components/OversizedPanel';
......@@ -22,7 +23,6 @@ import {
getApprovalTxStatus,
approveExchange,
} from '../../helpers/approval-utils';
import promisify from '../../helpers/web3-promisfy';
import "./swap.scss";
......@@ -42,6 +42,7 @@ class Swap extends Component {
lastEditedField: PropTypes.string,
inputErrors: PropTypes.arrayOf(PropTypes.string),
outputErrors: PropTypes.arrayOf(PropTypes.string),
selectors: PropTypes.func.isRequired,
};
static contextTypes = {
......@@ -54,19 +55,19 @@ class Swap extends Component {
};
componentWillReceiveProps(nextProps) {
this.getExchangeRate(nextProps)
.then(exchangeRate => {
this.setState({ exchangeRate });
if (!exchangeRate) {
return;
}
if (nextProps.lastEditedField === 'input') {
this.props.updateField('output', `${BN(nextProps.input).multipliedBy(exchangeRate).toFixed(7)}`);
} else if (nextProps.lastEditedField === 'output') {
this.props.updateField('input', `${BN(nextProps.output).multipliedBy(BN(1).dividedBy(exchangeRate)).toFixed(7)}`);
}
});
// this.getExchangeRate(nextProps)
// .then(exchangeRate => {
// this.setState({ exchangeRate });
// if (!exchangeRate) {
// return;
// }
//
// if (nextProps.lastEditedField === 'input') {
// this.props.updateField('output', `${BN(nextProps.input).multipliedBy(exchangeRate).toFixed(7)}`);
// } else if (nextProps.lastEditedField === 'output') {
// this.props.updateField('input', `${BN(nextProps.output).multipliedBy(BN(1).dividedBy(exchangeRate)).toFixed(7)}`);
// }
// });
}
shouldComponentUpdate(nextProps, nextState) {
......@@ -114,6 +115,9 @@ class Swap extends Component {
if (!amount) {
this.props.updateField('output', '');
}
// calculate exchangerate
// output = amount * exchagerate
// this.props.updateField('output', output);
this.props.updateField('lastEditedField', 'input');
}
......@@ -275,6 +279,58 @@ class Swap extends Component {
}
}
renderExchangeRate() {
const {
inputCurrency,
outputCurrency,
input,
exchangeAddresses: { fromToken },
selectors,
} = this.props;
if (!inputCurrency || !outputCurrency || !input) {
return (
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span>
</div>
</OversizedPanel>
)
}
const exchangeAddress = fromToken[outputCurrency];
const { value: inputReserve, decimals: inputDecimals, label: inputLabel } = selectors().getBalance(exchangeAddress);
const { value: outputReserve, decimals: outputDecimals, label: outputLabel }= selectors().getTokenBalance(outputCurrency, exchangeAddress);
const inputAmount = BN(input).multipliedBy(BN(10 ** 18));
const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997);
const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997));
const outputAmount = numerator.dividedBy(denominator);
const exchangeRate = outputAmount.dividedBy(inputAmount);
// console.log({
// exchangeAddress,
// outputCurrency,
// inputReserve: inputReserve.toFixed(),
// outputReserve: outputReserve.toFixed(),
// inputAmount: inputAmount.toFixed(),
// numerator: numerator.toFixed(),
// denominator: denominator.toFixed(),
// outputAmount: outputAmount.toFixed(),
// exchangeRate: exchangeRate.toFixed(),
// });
return (
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span>
<span>
{`1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}`}
</span>
</div>
</OversizedPanel>
)
}
render() {
const { lastEditedField, inputCurrency, outputCurrency, input, output, isValid, outputErrors, inputErrors } = this.props;
const { exchangeRate } = this.state;
......@@ -321,14 +377,7 @@ class Swap extends Component {
errors={outputErrors}
value={output}
/>
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span>
<span>
{exchangeRate ? `1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}` : ' - '}
</span>
</div>
</OversizedPanel>
{ this.renderExchangeRate() }
{
inputLabel && input
? (
......@@ -360,6 +409,7 @@ export default withRouter(
drizzleConnect(
Swap,
(state, ownProps) => ({
balances: state.web3connect.balances,
// React Router
push: ownProps.history.push,
pathname: ownProps.location.pathname,
......@@ -386,7 +436,8 @@ export default withRouter(
dispatch => ({
updateField: (name, value) => dispatch(updateField({ name, value })),
addError: (name, value) => dispatch(addError({ name, value })),
removeError: (name, value) => dispatch(removeError({ name, value }))
removeError: (name, value) => dispatch(removeError({ name, value })),
selectors: () => dispatch(selectors()),
})
),
);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment