Commit a0ed20cd authored by Chi Kei Chan's avatar Chi Kei Chan Committed by GitHub

Add input to output calculation (#36)

parent c3123a49
This diff is collapsed.
......@@ -33,8 +33,15 @@ class CurrencyInputPanel extends Component {
static propTypes = {
title: PropTypes.string,
description: PropTypes.string,
extraText: PropTypes.string,
value: PropTypes.string,
initialized: PropTypes.bool,
onCurrencySelected: PropTypes.func,
onValueChange: PropTypes.func,
};
static defaultProps = {
onCurrencySelected() {},
onValueChange() {},
};
static contextTypes = {
......@@ -113,7 +120,7 @@ class CurrencyInputPanel extends Component {
let tokenList = [ { value: 'ETH', label: 'ETH', address: 'ETH' } ];
for (let i = 0; i < tokens.length; i++) {
let entry = { value: '', label: '' }
let entry = { value: '', label: '' };
entry.value = tokens[i][0];
entry.label = tokens[i][0];
entry.address = tokens[i][1];
......@@ -124,6 +131,28 @@ class CurrencyInputPanel extends Component {
return tokenList;
};
onTokenSelect = (address) => {
this.setState({
selectedTokenAddress: address || 'ETH',
searchQuery: '',
isShowingModal: false,
});
this.props.onCurrencySelected(address);
if (address && address !== 'ETH') {
const { drizzle } = this.context;
const { web3 } = drizzle;
const contractConfig = {
contractName: address,
web3Contract: new web3.eth.Contract(ERC20_ABI, address),
};
const events = ['Approval', 'Transfer'];
this.context.drizzle.addContract(contractConfig, events, { from: this.props.account });
}
};
renderTokenList() {
const tokens = this.createTokenList();
const { searchQuery } = this.state;
......@@ -141,25 +170,7 @@ class CurrencyInputPanel extends Component {
<div
key={label}
className="token-modal__token-row"
onClick={() => {
this.setState({
selectedTokenAddress: address || 'ETH',
searchQuery: '',
isShowingModal: false,
});
if (address && address !== 'ETH') {
const { drizzle } = this.context;
const { web3 } = drizzle;
const contractConfig = {
contractName: address,
web3Contract: new web3.eth.Contract(ERC20_ABI, address),
};
const events = ['Approval', 'Transfer'];
this.context.drizzle.addContract(contractConfig, events, { from: this.props.account });
}
}}
onClick={() => this.onTokenSelect(address)}
>
<TokenLogo className="token-modal__token-logo" address={address} />
<div className="token-modal__token-label" >{label}</div>
......@@ -188,9 +199,9 @@ class CurrencyInputPanel extends Component {
type="text"
placeholder="Search Token or Paste Address"
className="token-modal__search-input"
onChange={e => this.setState({
searchQuery: e.target.value,
})}
onChange={e => {
this.setState({ searchQuery: e.target.value });
}}
/>
<img src={SearchIcon} className="token-modal__search-icon" />
</div>
......@@ -228,9 +239,8 @@ class CurrencyInputPanel extends Component {
type="number"
className="currency-input-panel__input"
placeholder="0.0"
onChange={e => {
this.props.updateField('input', e.target.value);
}}
onChange={e => this.props.onValueChange(e.target.value)}
value={this.props.value}
/>
<button
className={classnames("currency-input-panel__currency-select", {
......
......@@ -8,7 +8,14 @@ const initialState = {
['MKR','0x4c86a3b3cf926de3644f60658071ca604949609f'],
['OMG','0x1033f09e293200de63AF16041e83000aFBBfF5c0'],
['ZRX','0x42E109452F4055c82a513A527690F2D73251367e']
]
],
fromToken: {
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x80f5C1beA2Ea4a9C21E4c6D7831ae2Dbce45674d',
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x9eb0461bcc20229bE61319372cCA84d782823FCb',
'0xf9ba5210f91d0474bd1e1dcdaec4c58e359aad85': '0x4c86a3b3cf926de3644f60658071ca604949609f',
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0x1033f09e293200de63AF16041e83000aFBBfF5c0',
'0xF22e3F33768354c9805d046af3C0926f27741B43': '0x42E109452F4055c82a513A527690F2D73251367e',
}
},
tokenAddresses: {
addresses: [
......@@ -16,7 +23,7 @@ const initialState = {
['DAI','0x2448eE2641d78CC42D7AD76498917359D961A783'],
['MKR','0xf9ba5210f91d0474bd1e1dcdaec4c58e359aad85'],
['OMG','0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0'],
['ZRX','0xF22e3F33768354c9805d046af3C0926f27741B43']
['ZRX','0xF22e3F33768354c9805d046af3C0926f27741B43'],
]
},
};
......
......@@ -2,15 +2,15 @@ const UPDATE_FIELD = 'app/swap/updateField';
const initialState = {
input: '',
inputToken: '',
output: '',
outputToken: '',
inputCurrency: '',
outputCurrency: '',
};
export const updateField = ({ name, value }) => ({
type: UPDATE_FIELD,
payload: { name, value },
});
})
export default function swapReducer(state = initialState, { type, payload }) {
switch (type) {
......
import EXCHANGE_ABI from "../abi/exchange";
import {BigNumber as BN} from "bignumber.js";
export const calculateExchangeRate = async ({drizzleCtx, contractStore, input, inputCurrency, outputCurrency, exchangeAddresses }) => {
if (!inputCurrency || !outputCurrency || !input) {
return 0;
}
if (inputCurrency === outputCurrency) {
console.error(`Input and Output currency cannot be the same`);
return 0;
}
const currencies = [ inputCurrency, outputCurrency ];
const exchangeAddress = exchangeAddresses.fromToken[currencies.filter(d => d !== 'ETH')[0]];
if (!exchangeAddress) {
return 0;
}
if (currencies.includes('ETH')) {
const inputReserve = await getBalance({
currency: inputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
const outputReserve = await getBalance({
currency: outputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
const inputDecimals = await getDecimals({ address: inputCurrency, drizzleCtx, contractStore });
const outputDecimals = await getDecimals({ address: outputCurrency, drizzleCtx, contractStore });
const inputAmount = BN(input).multipliedBy(BN(10 ** inputDecimals));
const numerator = inputAmount.multipliedBy(BN(outputReserve).multipliedBy(997));
const denominator = BN(inputReserve).multipliedBy(1000).plus(BN(inputAmount).multipliedBy(997));
const outputAmount = numerator.dividedBy(denominator);
const exchangeRate = outputAmount.dividedBy(inputAmount);
if (exchangeRate.isNaN()) {
return 0;
}
return exchangeRate.toFixed(7);
} else {
return 0;
}
};
function getDecimals({ address, drizzleCtx, contractStore }) {
return new Promise(async (resolve, reject) => {
if (address === 'ETH') {
resolve('18');
return;
}
const decimalsKey = drizzleCtx.contracts[address].methods.decimals.cacheCall();
const decimals = contractStore[address].decimals[decimalsKey];
resolve(decimals && decimals.value);
});
}
function getBalance({ currency, address, drizzleCtx, contractStore }) {
return new Promise(async (resolve, reject) => {
if (currency === 'ETH') {
drizzleCtx.web3.eth.getBalance(address, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
} else {
const token = drizzleCtx.contracts[currency];
if (!token) {
return;
}
const balanceKey = token.methods.balanceOf.cacheCall(address);
const tokenStore = contractStore[currency];
if (!tokenStore) {
reject(new Error(`Cannot find ${currency} in contract store`));
return;
}
let balance = tokenStore.balanceOf[balanceKey];
resolve(balance && balance.value);
}
});
}
......@@ -3,12 +3,15 @@ import { drizzleConnect } from 'drizzle-react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { updateField } from '../../ducks/swap';
import Header from '../../components/Header';
import CurrencyInputPanel from '../../components/CurrencyInputPanel';
import OversizedPanel from '../../components/OversizedPanel';
import ArrowDown from '../../assets/images/arrow-down-blue.svg';
import { calculateExchangeRate } from '../../helpers/exchange-utils';
import "./swap.scss";
import EXCHANGE_ABI from "../../abi/exchange";
class Swap extends Component {
static propTypes = {
......@@ -17,9 +20,125 @@ class Swap extends Component {
pathname: PropTypes.string.isRequired,
currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired,
updateField: PropTypes.func.isRequired,
input: PropTypes.string,
output: PropTypes.string,
inputCurrency: PropTypes.string,
outputCurrency: PropTypes.string,
};
static contextTypes = {
drizzle: PropTypes.object,
};
state = {
exchangeRate: 0,
};
getTokenLabel(address) {
if (address === 'ETH') {
return 'ETH';
}
const {
initialized,
contracts,
} = this.props;
const { drizzle } = this.context;
const { web3 } = drizzle;
if (!initialized || !web3 || !address) {
return '';
}
const symbolKey = drizzle.contracts[address].methods.symbol.cacheCall();
const token = contracts[address];
const symbol = token.symbol[symbolKey];
if (!symbol) {
return '';
}
return symbol.value;
}
async updateInput(input) {
const {
outputCurrency,
exchangeAddresses: { fromToken },
} = this.props;
this.props.updateField('input', input);
if (!outputCurrency) {
return;
}
const { drizzle } = this.context;
const { web3 } = drizzle;
const exchangeAddress = fromToken[outputCurrency];
const token = drizzle.contracts[outputCurrency];
if (!exchangeAddress || !token) {
return;
}
if (!drizzle.contracts[exchangeAddress]) {
const contractConfig = {
contractName: exchangeAddress,
web3Contract: new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress),
};
const events = ['Approval', 'Transfer', 'TokenPurchase', 'EthPurchase', 'AddLiquidity', 'RemoveLiquidity'];
this.context.drizzle.addContract(contractConfig, events, { from: this.props.account });
}
}
async getExchangeRate(props) {
const {
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
contracts,
} = props;
const { drizzle } = this.context;
return await calculateExchangeRate({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
});
}
componentWillReceiveProps(nextProps) {
this.getExchangeRate(nextProps)
.then(exchangeRate => {
this.setState({ exchangeRate });
if (!exchangeRate) {
return;
}
this.props.updateField('output', `${nextProps.input * exchangeRate}`);
});
}
componentWillUnmount() {
this.props.updateField('output', '');
this.props.updateField('input', '');
this.props.updateField('outputCurrency', '');
this.props.updateField('inputCurrency', '');
}
render() {
const { inputCurrency, outputCurrency, input, output } = this.props;
const { exchangeRate } = this.state;
const inputLabel = this.getTokenLabel(inputCurrency);
const outputLabel = this.getTokenLabel(outputCurrency);
return (
<div className="swap">
<Header />
......@@ -30,7 +149,9 @@ class Swap extends Component {
>
<CurrencyInputPanel
title="Input"
extraText="Balance: 0.03141"
onCurrencySelected={d => this.props.updateField('inputCurrency', d)}
onValueChange={d => this.updateInput(d)}
value={input}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
......@@ -40,18 +161,29 @@ class Swap extends Component {
<CurrencyInputPanel
title="Output"
description="(estimated)"
extraText="Balance: 0.0"
onCurrencySelected={d => this.props.updateField('outputCurrency', d)}
onValueChange={d => this.props.updateField('output', d)}
value={output}
/>
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span>
<span>1 ETH = 1283.878 BAT</span>
<span>
{exchangeRate ? `1 ${inputLabel} = ${exchangeRate} ${outputLabel}` : ' - '}
</span>
</div>
</OversizedPanel>
<div className="swap__summary-wrapper">
<div>You are selling <span className="swap__highlight-text">0.01 ETH</span></div>
<div>You will receive between <span className="swap__highlight-text">12.80</span> and <span className="swap__highlight-text">12.83 BAT</span></div>
</div>
{
inputLabel && input
? (
<div className="swap__summary-wrapper">
<div>You are selling <span className="swap__highlight-text">{`${input} ${inputLabel}`}</span></div>
<div>You will receive between <span className="swap__highlight-text">12.80</span> and <span
className="swap__highlight-text">12.83 BAT</span></div>
</div>
)
: null
}
</div>
<button
className={classnames('swap__cta-btn', {
......@@ -69,10 +201,33 @@ export default withRouter(
drizzleConnect(
Swap,
(state, ownProps) => ({
// React Router
push: ownProps.history.push,
pathname: ownProps.location.pathname,
// From Drizzle
initialized: state.drizzleStatus.initialized,
balance: state.accountBalances[state.accounts[0]] || null,
account: state.accounts[0],
contracts: state.contracts,
currentAddress: state.accounts[0],
isConnected: !!(state.drizzleStatus.initialized && state.accounts[0]),
// Redux Store
input: state.swap.input,
output: state.swap.output,
inputCurrency: state.swap.inputCurrency,
outputCurrency: state.swap.outputCurrency,
exchangeAddresses: state.addresses.exchangeAddresses,
}),
dispatch => ({
updateField: (name, value) => dispatch(updateField({ name, value })),
})
),
);
function timeout(time = 0) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
import { generateContractsInitialState } from 'drizzle'
export default {
addresses: {
exchangeAddresses: {
addresses: [
['BAT','0x80f5C1beA2Ea4a9C21E4c6D7831ae2Dbce45674d'],
['DAI','0x9eb0461bcc20229bE61319372cCA84d782823FCb'],
['MKR','0x4c86a3b3cf926de3644f60658071ca604949609f'],
['OMG','0x1033f09e293200de63AF16041e83000aFBBfF5c0'],
['ZRX','0x42E109452F4055c82a513A527690F2D73251367e']
]
},
tokenAddresses: {
addresses: [
['BAT','0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B'],
['DAI','0x2448eE2641d78CC42D7AD76498917359D961A783'],
['MKR','0xf9ba5210f91d0474bd1e1dcdaec4c58e359aad85'],
['OMG','0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0'],
['ZRX','0xF22e3F33768354c9805d046af3C0926f27741B43']
]
}
},
contracts: generateContractsInitialState({ contracts: [], events: [], polls: [] }),
exchangeContracts: {},
tokenContracts: {},
......
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