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

Implements calculateInput ETH to ERC20 (#38)

* Update Rinkeby exchange address

* Implement ETH to ERC20 calculateInput and calculateOutput
parent 12445c00
......@@ -12,6 +12,7 @@ import SearchIcon from '../../assets/images/magnifying-glass.svg';
import ERC20_ABI from '../../abi/erc20';
import './currency-panel.scss';
import EXCHANGE_ABI from "../../abi/exchange";
const FUSE_OPTIONS = {
includeMatches: false,
......@@ -37,6 +38,9 @@ class CurrencyInputPanel extends Component {
initialized: PropTypes.bool,
onCurrencySelected: PropTypes.func,
onValueChange: PropTypes.func,
exchangeAddresses: PropTypes.shape({
fromToken: PropTypes.object.isRequired,
}).isRequired,
};
static defaultProps = {
......@@ -141,15 +145,32 @@ class CurrencyInputPanel extends Component {
this.props.onCurrencySelected(address);
if (address && address !== 'ETH') {
// Add Token Contract
const { drizzle } = this.context;
const { fromToken } = this.props.exchangeAddresses;
const { web3 } = drizzle;
const contractConfig = {
const tokenConfig = {
contractName: address,
web3Contract: new web3.eth.Contract(ERC20_ABI, address),
};
const events = ['Approval', 'Transfer'];
const tokenEvents = ['Approval', 'Transfer'];
this.context.drizzle.addContract(contractConfig, events, { from: this.props.account });
this.context.drizzle.addContract(tokenConfig, tokenEvents, { from: this.props.account });
// Add Exchange Contract
const exchangeAddress = fromToken[address];
if (!exchangeAddress) {
return;
}
const exchangeConfig = {
contractName: exchangeAddress,
web3Contract: new web3.eth.Contract(EXCHANGE_ABI, exchangeAddress),
};
const exchangeEvents = ['Approval', 'Transfer', 'TokenPurchase', 'EthPurchase', 'AddLiquidity', 'RemoveLiquidity'];
this.context.drizzle.addContract(exchangeConfig, exchangeEvents , { from: this.props.account });
}
};
......@@ -280,6 +301,7 @@ export default drizzleConnect(
return {
tokenAddresses: state.addresses.tokenAddresses,
exchangeAddresses: state.addresses.exchangeAddresses,
initialized,
balance: accountBalances[accounts[0]] || null,
account: accounts[0],
......
......@@ -97,7 +97,7 @@ function Header (props) {
isMobile()
? (
[
<img src={CoinbaseWalletLogo} onClick={() => window.open(getCoinbaseWalletLink(), '_blank')} />,
<img src={CoinbaseWalletLogo} key="coinbase-wallet" onClick={() => window.open(getCoinbaseWalletLink(), '_blank')} />,
<img src={TrustLogo} key="trust" onClick={() => window.open(getTrustLink(), '_blank')} />
]
)
......
......@@ -11,11 +11,6 @@ export default class Modal extends Component {
onClose: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
// this.el = document.createElement('div');
}
componentDidMount() {
// The portal element is inserted in the DOM tree after
// the Modal's children are mounted, meaning that children
......
......@@ -4,7 +4,7 @@ import EthereumLogo from '../../assets/images/ethereum-logo.png';
import GenericTokenLogo from '../../assets/images/generic-token-logo.png';
const TOKEN_ICON_API = 'https://raw.githubusercontent.com/TrustWallet/tokens/master/images';
const BAD_IMAGES = {};
export default class TokenLogo extends Component {
static propTypes = {
address: PropTypes.string,
......@@ -30,11 +30,10 @@ export default class TokenLogo extends Component {
path = EthereumLogo;
}
if (!this.state.error) {
if (!this.state.error && !BAD_IMAGES[address]) {
path = `${TOKEN_ICON_API}/${address}.png`;
}
return (
<img
src={path}
......@@ -43,7 +42,10 @@ export default class TokenLogo extends Component {
width: size,
height: size,
}}
onError={() => this.setState({ error: true })}
onError={() => {
this.setState({ error: true });
BAD_IMAGES[address] = true;
}}
/>
);
}
......
......@@ -5,26 +5,26 @@ const initialState = {
addresses: [
['BAT','0x80f5C1beA2Ea4a9C21E4c6D7831ae2Dbce45674d'],
['DAI','0x9eb0461bcc20229bE61319372cCA84d782823FCb'],
['MKR','0x4c86a3b3cf926de3644f60658071ca604949609f'],
['OMG','0x1033f09e293200de63AF16041e83000aFBBfF5c0'],
['ZRX','0x42E109452F4055c82a513A527690F2D73251367e']
['MKR','0x93bB63aFe1E0180d0eF100D774B473034fd60C36'],
['OMG','0x26C226EBb6104676E593F8A070aD6f25cDa60F8D'],
['ZRX','0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D'],
],
fromToken: {
'0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B': '0x80f5C1beA2Ea4a9C21E4c6D7831ae2Dbce45674d',
'0x2448eE2641d78CC42D7AD76498917359D961A783': '0x9eb0461bcc20229bE61319372cCA84d782823FCb',
'0xf9ba5210f91d0474bd1e1dcdaec4c58e359aad85': '0x4c86a3b3cf926de3644f60658071ca604949609f',
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0x1033f09e293200de63AF16041e83000aFBBfF5c0',
'0xF22e3F33768354c9805d046af3C0926f27741B43': '0x42E109452F4055c82a513A527690F2D73251367e',
}
'0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85': '0x93bB63aFe1E0180d0eF100D774B473034fd60C36',
'0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0': '0x26C226EBb6104676E593F8A070aD6f25cDa60F8D',
'0xF22e3F33768354c9805d046af3C0926f27741B43': '0xaBD44a1D1b9Fb0F39fE1D1ee6b1e2a14916D067D',
},
},
tokenAddresses: {
addresses: [
['BAT','0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B'],
['DAI','0x2448eE2641d78CC42D7AD76498917359D961A783'],
['MKR','0xf9ba5210f91d0474bd1e1dcdaec4c58e359aad85'],
['MKR','0xF9bA5210F91D0474bd1e1DcDAeC4C58E359AaD85'],
['OMG','0x879884c3C46A24f56089f3bBbe4d5e38dB5788C0'],
['ZRX','0xF22e3F33768354c9805d046af3C0926f27741B43'],
]
],
},
};
......
......@@ -5,6 +5,7 @@ const initialState = {
output: '',
inputCurrency: '',
outputCurrency: '',
lastEditedField: '',
};
export const updateField = ({ name, value }) => ({
......
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;
export const calculateExchangeRateFromInput = async opts => {
const { inputCurrency, outputCurrency } = opts;
if (inputCurrency === outputCurrency) {
console.error(`Input and Output currency cannot be the same`);
return;
}
if (inputCurrency === 'ETH' && outputCurrency !== 'ETH') {
return ETH_TO_ERC20.calculateOutput(opts);
}
if (outputCurrency === 'ETH' && inputCurrency !== 'ETH') {
return ERC20_TO_ETH.calculateOutput(opts);
}
};
export const calculateExchangeRateFromOutput = async opts => {
const { inputCurrency, outputCurrency } = opts;
if (inputCurrency === outputCurrency) {
console.error(`Input and Output currency cannot be the same`);
return 0;
return;
}
const currencies = [ inputCurrency, outputCurrency ];
const exchangeAddress = exchangeAddresses.fromToken[currencies.filter(d => d !== 'ETH')[0]];
if (inputCurrency === 'ETH' && outputCurrency !== 'ETH') {
return ETH_TO_ERC20.calculateInput(opts);
}
if (!exchangeAddress) {
return 0;
if (outputCurrency === 'ETH' && inputCurrency !== 'ETH') {
return ERC20_TO_ETH.calculateInput(opts);
}
};
const ETH_TO_ERC20 = {
calculateOutput: async ({drizzleCtx, contractStore, input, inputCurrency, outputCurrency, exchangeAddresses }) => {
if (inputCurrency !== 'ETH') {
console.error('Input Currency should be ETH');
return;
}
if (!outputCurrency || outputCurrency === 'ETH') {
console.error('Output Currency should be ERC20');
return;
}
const exchangeAddress = exchangeAddresses.fromToken[outputCurrency];
if (!exchangeAddress) {
console.error(`Cannot find Exchange Address for ${outputCurrency}`);
return;
}
const inputReserve = await getBalance({
currency: inputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
const outputReserve = await getBalance({
currency: outputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
const inputAmount = BN(input).multipliedBy(BN(10 ** 18));
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;
}
return exchangeRate;
},
calculateInput: async ({drizzleCtx, contractStore, output, inputCurrency, outputCurrency, exchangeAddresses }) => {
if (inputCurrency !== 'ETH') {
console.error('Input Currency should be ETH');
return;
}
if (!outputCurrency || outputCurrency === 'ETH') {
console.error('Output Currency should be ERC20');
return;
}
const exchangeAddress = exchangeAddresses.fromToken[outputCurrency];
if (!exchangeAddress) {
console.error(`Cannot find Exchange Address for ${outputCurrency}`);
return;
}
const inputReserve = await getBalance({
currency: inputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
const outputReserve = await getBalance({
currency: outputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
const outputDecimals = await getDecimals({ address: inputCurrency, contractStore, drizzleCtx });
const outputAmount = BN(output).multipliedBy(10 ** outputDecimals);
const numerator = outputAmount .multipliedBy(BN(inputReserve).multipliedBy(1000));
const denominator = BN(outputReserve).minus(outputAmount).multipliedBy(997);
const inputAmount = numerator.dividedBy(denominator.plus(1));
const exchangeRate = outputAmount.dividedBy(inputAmount);
if (exchangeRate.isNaN()) {
return;
}
return exchangeRate;
},
};
const ERC20_TO_ETH = {
calculateOutput: async ({drizzleCtx, contractStore, input, inputCurrency, outputCurrency, exchangeAddresses }) => {
if (outputCurrency !== 'ETH') {
console.error('Output Currency should be ETH');
return;
}
if (!inputCurrency || inputCurrency === 'ETH') {
console.error('Input Currency should be ERC20');
return;
}
const exchangeAddress = exchangeAddresses.fromToken[inputCurrency];
if (!exchangeAddress) {
console.error(`Cannot find Exchange Address for ${inputCurrency}`);
return;
}
if (currencies.includes('ETH')) {
const inputReserve = await getBalance({
currency: inputCurrency,
address: exchangeAddress,
......@@ -33,8 +157,7 @@ export const calculateExchangeRate = async ({drizzleCtx, contractStore, input, i
contractStore,
});
const inputDecimals = await getDecimals({ address: inputCurrency, drizzleCtx, contractStore });
const outputDecimals = await getDecimals({ address: outputCurrency, drizzleCtx, contractStore });
const inputDecimals = await getDecimals({ address: inputCurrency, contractStore, drizzleCtx });
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));
......@@ -42,15 +165,60 @@ export const calculateExchangeRate = async ({drizzleCtx, contractStore, input, i
const exchangeRate = outputAmount.dividedBy(inputAmount);
if (exchangeRate.isNaN()) {
return 0;
return;
}
return exchangeRate.toFixed(7);
} else {
return 0;
}
return exchangeRate;
},
calculateInput: async ({drizzleCtx, contractStore, output, inputCurrency, outputCurrency, exchangeAddresses }) => {
if (outputCurrency !== 'ETH') {
console.error('Output Currency should be ETH');
return;
}
if (!inputCurrency || inputCurrency === 'ETH') {
console.error('Output Currency should be ERC20');
return;
}
const exchangeAddress = exchangeAddresses.fromToken[inputCurrency];
if (!exchangeAddress) {
console.error(`Cannot find Exchange Address for ${inputCurrency}`);
return;
}
const inputReserve = await getBalance({
currency: inputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
const outputReserve = await getBalance({
currency: outputCurrency,
address: exchangeAddress,
drizzleCtx,
contractStore,
});
// const outputDecimals = await getDecimals({ address: inputCurrency, contractStore, drizzleCtx });
const outputAmount = BN(output).multipliedBy(10 ** 18);
const numerator = outputAmount .multipliedBy(BN(inputReserve).multipliedBy(1000));
const denominator = BN(outputReserve).minus(outputAmount).multipliedBy(997);
const inputAmount = numerator.dividedBy(denominator.plus(1));
const exchangeRate = outputAmount.dividedBy(inputAmount);
if (exchangeRate.isNaN()) {
return;
}
return exchangeRate;
},
};
function getDecimals({ address, drizzleCtx, contractStore }) {
return new Promise(async (resolve, reject) => {
if (address === 'ETH') {
......
// eslint-disable-next-line no-unused-escape
const SPECIAL_CHARS_REGEX = /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g
export default function (text, pattern, tokenSeparator = / +/g) {
......
import Bitap from "./index";
import bitapScore from './bitap_score';
import matchedIndices from './bitap_matched_indices';
......
......@@ -3,15 +3,15 @@ import { drizzleConnect } from 'drizzle-react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import {BigNumber as BN} from "bignumber.js";
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 { calculateExchangeRateFromInput, calculateExchangeRateFromOutput } from '../../helpers/exchange-utils';
import "./swap.scss";
import EXCHANGE_ABI from "../../abi/exchange";
class Swap extends Component {
static propTypes = {
......@@ -25,6 +25,7 @@ class Swap extends Component {
output: PropTypes.string,
inputCurrency: PropTypes.string,
outputCurrency: PropTypes.string,
lastEditedField: PropTypes.string,
};
static contextTypes = {
......@@ -32,7 +33,7 @@ class Swap extends Component {
};
state = {
exchangeRate: 0,
exchangeRate: BN(0),
};
getTokenLabel(address) {
......@@ -62,36 +63,20 @@ class Swap extends Component {
return symbol.value;
}
async updateInput(input) {
const {
outputCurrency,
exchangeAddresses: { fromToken },
} = this.props;
this.props.updateField('input', input);
if (!outputCurrency) {
return;
updateInput(amount) {
this.props.updateField('input', amount);
if (!amount) {
this.props.updateField('output', '');
}
this.props.updateField('lastEditedField', 'input');
}
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 });
updateOutput(amount) {
this.props.updateField('output', amount);
if (!amount) {
this.props.updateField('input', '');
}
this.props.updateField('lastEditedField', 'output');
}
async getExchangeRate(props) {
......@@ -101,18 +86,31 @@ class Swap extends Component {
inputCurrency,
outputCurrency,
exchangeAddresses,
lastEditedField,
contracts,
} = props;
const { drizzle } = this.context;
return await calculateExchangeRate({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
});
return lastEditedField === 'input'
? await calculateExchangeRateFromInput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
})
: await calculateExchangeRateFromOutput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
}) ;
}
componentWillReceiveProps(nextProps) {
......@@ -122,7 +120,12 @@ class Swap extends Component {
if (!exchangeRate) {
return;
}
this.props.updateField('output', `${nextProps.input * exchangeRate}`);
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)}`);
}
});
}
......@@ -131,6 +134,12 @@ class Swap extends Component {
this.props.updateField('input', '');
this.props.updateField('outputCurrency', '');
this.props.updateField('inputCurrency', '');
this.props.updateField('lastEditedField', '');
}
onCurrencySelected(field, data) {
this.props.updateField(field, data);
// this.props
}
render() {
......@@ -162,14 +171,14 @@ class Swap extends Component {
title="Output"
description="(estimated)"
onCurrencySelected={d => this.props.updateField('outputCurrency', d)}
onValueChange={d => this.props.updateField('output', d)}
onValueChange={d => this.updateOutput(d)}
value={output}
/>
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span>
<span>
{exchangeRate ? `1 ${inputLabel} = ${exchangeRate} ${outputLabel}` : ' - '}
{exchangeRate ? `1 ${inputLabel} = ${exchangeRate.toFixed(7)} ${outputLabel}` : ' - '}
</span>
</div>
</OversizedPanel>
......@@ -218,6 +227,7 @@ export default withRouter(
output: state.swap.output,
inputCurrency: state.swap.inputCurrency,
outputCurrency: state.swap.outputCurrency,
lastEditedField: state.swap.lastEditedField,
exchangeAddresses: state.addresses.exchangeAddresses,
}),
dispatch => ({
......@@ -225,9 +235,3 @@ export default withRouter(
})
),
);
function timeout(time = 0) {
return new Promise(resolve => {
setTimeout(resolve, time);
});
}
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