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

Refactor Swap (#80)

* Refactor calculateInput for ETH-TOKEN swap

* Refactor Swap Input to not use drizzle

* Refactor Swap Output
parent 6464e443
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
border-radius: 1.25rem; border-radius: 1.25rem;
box-shadow: 0 0 0 .5px $mercury-gray; box-shadow: 0 0 0 .5px $mercury-gray;
background-color: $white; background-color: $white;
transition: box-shadow 200ms ease-in-out;
&--error { &--error {
box-shadow: 0 0 0 .5px $salmon-red; box-shadow: 0 0 0 .5px $salmon-red;
......
...@@ -37,8 +37,28 @@ const initialState = { ...@@ -37,8 +37,28 @@ const initialState = {
export const selectors = () => (dispatch, getState) => { export const selectors = () => (dispatch, getState) => {
const state = getState().web3connect; const state = getState().web3connect;
return { const getTokenBalance = (tokenAddress, address) => {
getBalance: 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 getBalance = (address, tokenAddress) => {
if (process.env.NODE_ENV === 'production' || !tokenAddress) {
console.warn('No token address found - return ETH balance');
}
if (!tokenAddress || tokenAddress === 'ETH') {
const balance = state.balances.ethereum[address]; const balance = state.balances.ethereum[address];
if (!balance) { if (!balance) {
...@@ -46,23 +66,16 @@ export const selectors = () => (dispatch, getState) => { ...@@ -46,23 +66,16 @@ export const selectors = () => (dispatch, getState) => {
return Balance(0, 'ETH'); return Balance(0, 'ETH');
} }
return balance; return balance;
}, } else if (tokenAddress) {
return getTokenBalance(tokenAddress, address);
getTokenBalance: (tokenAddress, address) => { }
const tokenBalances = state.balances[tokenAddress];
if (!tokenBalances) { return Balance(NaN);
dispatch(watchBalance({ balanceOf: address, tokenAddress })); };
return Balance(0);
}
const balance = tokenBalances[address]; return {
if (!balance) { getBalance,
dispatch(watchBalance({ balanceOf: address, tokenAddress })); getTokenBalance,
return Balance(0);
}
return balance;
},
} }
}; };
...@@ -124,6 +137,7 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState ...@@ -124,6 +137,7 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState
type: WATCH_ETH_BALANCE, type: WATCH_ETH_BALANCE,
payload: balanceOf, payload: balanceOf,
}); });
setTimeout(() => dispatch(sync()), 0);
} else if (tokenAddress) { } else if (tokenAddress) {
if (watched.balances[tokenAddress] && watched.balances[tokenAddress].includes(balanceOf)) { if (watched.balances[tokenAddress] && watched.balances[tokenAddress].includes(balanceOf)) {
return; return;
...@@ -135,6 +149,7 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState ...@@ -135,6 +149,7 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState
balanceOf, balanceOf,
}, },
}); });
setTimeout(() => dispatch(sync()), 0);
} }
}; };
...@@ -199,7 +214,6 @@ export const sync = () => async (dispatch, getState) => { ...@@ -199,7 +214,6 @@ export const sync = () => async (dispatch, getState) => {
const symbol = tokenBalance.label || await contract.methods.symbol().call(); const symbol = tokenBalance.label || await contract.methods.symbol().call();
if (tokenBalance.value.isEqualTo(BN(balance))) { if (tokenBalance.value.isEqualTo(BN(balance))) {
console.log('block');
return; return;
} }
......
...@@ -57,12 +57,7 @@ class AddLiquidity extends Component { ...@@ -57,12 +57,7 @@ class AddLiquidity extends Component {
return ''; return '';
} }
if (currency === 'ETH') { const { value, decimals } = selectors().getBalance(account, currency);
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)}`; return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
} }
...@@ -85,7 +80,7 @@ class AddLiquidity extends Component { ...@@ -85,7 +80,7 @@ class AddLiquidity extends Component {
const maxTokens = tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE); const maxTokens = tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE);
try { try {
const tx = await exchange.methods.addLiquidity(minLiquidity.toFixed(0), maxTokens.toFixed(0), deadline).send({ await exchange.methods.addLiquidity(minLiquidity.toFixed(0), maxTokens.toFixed(0), deadline).send({
from: account, from: account,
value: ethAmount.toFixed(0) value: ethAmount.toFixed(0)
}); });
...@@ -144,8 +139,8 @@ class AddLiquidity extends Component { ...@@ -144,8 +139,8 @@ class AddLiquidity extends Component {
return; return;
} }
const { value: tokenValue } = selectors().getTokenBalance(token, fromToken[token]); const { value: tokenValue } = selectors().getBalance(fromToken[token], token);
const { value: ethValue } = selectors().getBalance(fromToken[token]); const { value: ethValue } = selectors().getBalance(fromToken[token], eth);
return tokenValue.dividedBy(ethValue); return tokenValue.dividedBy(ethValue);
} }
...@@ -165,8 +160,8 @@ class AddLiquidity extends Component { ...@@ -165,8 +160,8 @@ class AddLiquidity extends Component {
isValid = false; isValid = false;
} }
const { value: ethValue } = selectors().getBalance(account); const { value: ethValue } = selectors().getBalance(account, inputCurrency);
const { value: tokenValue, decimals } = selectors().getTokenBalance(outputCurrency, account); const { value: tokenValue, decimals } = selectors().getBalance(account, outputCurrency);
if (ethValue.isLessThan(BN(inputValue * 10 ** 18))) { if (ethValue.isLessThan(BN(inputValue * 10 ** 18))) {
inputError = 'Insufficient Balance'; inputError = 'Insufficient Balance';
...@@ -329,7 +324,6 @@ class AddLiquidity extends Component { ...@@ -329,7 +324,6 @@ class AddLiquidity extends Component {
selectedTokenAddress={outputCurrency} selectedTokenAddress={outputCurrency}
onCurrencySelected={currency => { onCurrencySelected={currency => {
this.setState({ outputCurrency: currency }); this.setState({ outputCurrency: currency });
this.props.sync();
}} }}
onValueChange={this.onOutputChange} onValueChange={this.onOutputChange}
value={outputValue} value={outputValue}
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import { drizzleConnect } from 'drizzle-react'; import { drizzleConnect } from 'drizzle-react';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import {BigNumber as BN} from "bignumber.js"; import {BigNumber as BN} from "bignumber.js";
import deepEqual from 'deep-equal'; import { selectors } from '../../ducks/web3connect';
import { isValidSwap, updateField, addError, removeError, resetSwap } from '../../ducks/swap';
import { selectors, sync } from '../../ducks/web3connect';
import Header from '../../components/Header'; import Header from '../../components/Header';
import CurrencyInputPanel from '../../components/CurrencyInputPanel'; import CurrencyInputPanel from '../../components/CurrencyInputPanel';
import OversizedPanel from '../../components/OversizedPanel'; import OversizedPanel from '../../components/OversizedPanel';
import ArrowDown from '../../assets/images/arrow-down-blue.svg'; import ArrowDown from '../../assets/images/arrow-down-blue.svg';
import Pending from '../../assets/images/pending.svg'; import EXCHANGE_ABI from '../../abi/exchange';
import {
calculateExchangeRateFromInput,
calculateExchangeRateFromOutput,
swapInput,
swapOutput,
} from '../../helpers/exchange-utils';
import {
isExchangeUnapproved,
approveExchange,
} from '../../helpers/approval-utils';
import {
getTxStatus,
} from '../../helpers/contract-utils';
import promisify from '../../helpers/web3-promisfy';
import "./swap.scss"; import "./swap.scss";
import promisify from "../../helpers/web3-promisfy";
const INPUT = 0;
const OUTPUT = 1;
class Swap extends Component { class Swap extends Component {
static propTypes = { static propTypes = {
// Injected by React Router Dom account: PropTypes.string,
push: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
currentAddress: PropTypes.string,
isConnected: PropTypes.bool.isRequired, isConnected: PropTypes.bool.isRequired,
isValid: PropTypes.bool.isRequired, isValid: PropTypes.bool.isRequired,
updateField: PropTypes.func.isRequired,
input: PropTypes.string,
output: PropTypes.string,
inputCurrency: PropTypes.string,
outputCurrency: PropTypes.string,
lastEditedField: PropTypes.string,
inputErrors: PropTypes.arrayOf(PropTypes.string),
outputErrors: PropTypes.arrayOf(PropTypes.string),
selectors: PropTypes.func.isRequired, selectors: PropTypes.func.isRequired,
}; web3: PropTypes.object.isRequired,
static contextTypes = {
drizzle: PropTypes.object,
}; };
state = { state = {
exchangeRate: BN(0), inputValue: '',
approvalTxId: null, outputValue: '',
swapTxId: null, inputCurrency: '',
outputCurrency: '',
inputAmountB: '',
lastEditedField: '',
}; };
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(nextProps, this.props) || return true;
!deepEqual(nextState, this.state);
} }
componentWillUnmount() { reset() {
this.resetSwap(); this.setState({
inputValue: '',
outputValue: '',
inputCurrency: '',
outputCurrency: '',
inputAmountB: '',
lastEditedField: '',
});
} }
resetSwap() { componentWillReceiveProps() {
this.props.resetSwap(); this.recalcForm();
this.setState({approvalTxId: null, swapTxId: null});
} }
getTokenLabel(address) { validate() {
if (address === 'ETH') { const { selectors, account } = this.props;
return 'ETH';
}
const { const {
initialized, inputValue, outputValue,
contracts, inputCurrency, outputCurrency,
} = this.props; } = this.state;
const { drizzle } = this.context;
const { web3 } = drizzle;
if (!initialized || !web3 || !address) { let inputError = '';
return ''; let outputError = '';
let isValid = true;
if (!inputValue || !outputValue || !inputCurrency || !outputCurrency) {
isValid = false;
} }
const symbolKey = drizzle.contracts[address].methods.symbol.cacheCall(); const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
const token = contracts[address];
const symbol = token.symbol[symbolKey];
if (!symbol) { if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
return ''; inputError = 'Insufficient Balance';
} }
return symbol.value; if (inputValue === 'N/A') {
inputError = 'Not a valid input value';
}
return {
inputError,
outputError,
isValid: isValid && !inputError && !outputError,
};
} }
updateInput(amount) { recalcForm() {
this.props.updateField('input', amount); const { inputCurrency, outputCurrency } = this.state;
if (!amount) {
this.props.updateField('output', ''); if (!inputCurrency || !outputCurrency) {
return;
} }
// calculate exchangerate
// output = amount * exchagerate
// this.props.updateField('output', output);
this.props.updateField('lastEditedField', 'input');
}
updateOutput(amount) { if (inputCurrency === outputCurrency) {
this.props.updateField('output', amount); this.setState({
if (!amount) { inputValue: '',
this.props.updateField('input', ''); outputValue: '',
});
return;
} }
this.props.updateField('lastEditedField', 'output');
}
async getExchangeRate(props) { if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
const { this.recalcTokenTokenForm();
input, return;
output, }
inputCurrency,
outputCurrency, this.recalcEthTokenForm();
exchangeAddresses,
lastEditedField,
contracts,
} = props;
const { drizzle } = this.context;
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,
}) ;
} }
getIsUnapproved() { recalcTokenTokenForm = () => {
const { const {
input, exchangeAddresses: { fromToken },
inputCurrency, selectors,
account,
contracts,
exchangeAddresses
} = this.props; } = this.props;
const { drizzle } = this.context;
return isExchangeUnapproved({
value: input,
currency: inputCurrency,
drizzleCtx: drizzle,
contractStore: contracts,
account,
exchangeAddresses,
});
}
approveExchange = async () => {
const { const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency, inputCurrency,
exchangeAddresses, outputCurrency,
account, lastEditedField,
contracts, exchangeRate: oldExchangeRate,
} = this.props; inputAmountB: oldInputAmountB,
const { drizzle } = this.context; } = this.state;
if (this.getIsUnapproved()) { const exchangeAddressA = fromToken[inputCurrency];
const approvalTxId = await approveExchange({ const exchangeAddressB = fromToken[outputCurrency];
currency: inputCurrency,
drizzleCtx: drizzle, const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency);
contractStore: contracts, const { value: outputReserveA }= selectors().getBalance(exchangeAddressA, 'ETH');
account, const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH');
exchangeAddresses, const { value: outputReserveB, decimals: outputDecimalsB }= selectors().getBalance(exchangeAddressB, outputCurrency);
if (lastEditedField === INPUT) {
if (!oldInputValue) {
return this.setState({
outputValue: '',
exchangeRate: BN(0),
});
}
const inputAmountA = BN(oldInputValue).multipliedBy(10 ** inputDecimalsA);
const outputAmountA = calculateEtherTokenOutput({
inputAmount: inputAmountA,
inputReserve: inputReserveA,
outputReserve: outputReserveA,
}); });
// Redundant Variable for readability of the formala
// OutputAmount from the first swap becomes InputAmount of the second swap
const inputAmountB = outputAmountA;
const outputAmountB = calculateEtherTokenOutput({
inputAmount: inputAmountB,
inputReserve: inputReserveB,
outputReserve: outputReserveB,
});
const exchangeRate = outputAmountB.dividedBy(inputAmountA);
const outputValue = outputAmountB.dividedBy(BN(10 ** outputDecimalsB)).toFixed(7);
this.setState({ approvalTxId }) const appendState = {};
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate;
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue;
}
this.setState(appendState);
} }
}
getApprovalStatus() { if (lastEditedField === OUTPUT) {
const { drizzle } = this.context; if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0),
});
}
const outputAmountB = BN(oldOutputValue).multipliedBy(10 ** outputDecimalsB);
const inputAmountB = calculateEtherTokenInput({
outputAmount: outputAmountB,
inputReserve: inputReserveB,
outputReserve: outputReserveB,
});
return getTxStatus({ // Redundant Variable for readability of the formala
drizzleCtx: drizzle, // InputAmount from the first swap becomes OutputAmount of the second swap
txId: this.state.approvalTxId, const outputAmountA = inputAmountB;
}); const inputAmountA = calculateEtherTokenInput({
} outputAmount: outputAmountA,
inputReserve: inputReserveA,
outputReserve: outputReserveA,
});
getSwapStatus() { const exchangeRate = outputAmountB.dividedBy(inputAmountA);
const { drizzle } = this.context; const inputValue = inputAmountA.isNegative()
? 'N/A'
: inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7);
return getTxStatus({ const appendState = {};
drizzleCtx: drizzle,
txId: this.state.swapTxId,
});
}
onSwap = async () => { if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate;
}
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue;
}
if (!inputAmountB.isEqualTo(BN(oldInputAmountB))) {
appendState.inputAmountB = inputAmountB;
}
this.setState(appendState);
}
};
recalcEthTokenForm = () => {
const { const {
input, exchangeAddresses: { fromToken },
output, selectors,
} = this.props;
const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency, inputCurrency,
outputCurrency, outputCurrency,
exchangeAddresses,
lastEditedField, lastEditedField,
exchangeRate: oldExchangeRate,
} = this.state;
const tokenAddress = [inputCurrency, outputCurrency].filter(currency => currency !== 'ETH')[0];
const exchangeAddress = fromToken[tokenAddress];
if (!exchangeAddress) {
return;
}
const { value: inputReserve, decimals: inputDecimals } = selectors().getBalance(exchangeAddress, inputCurrency);
const { value: outputReserve, decimals: outputDecimals }= selectors().getBalance(exchangeAddress, outputCurrency);
if (lastEditedField === INPUT) {
if (!oldInputValue) {
return this.setState({
outputValue: '',
exchangeRate: BN(0),
});
}
const inputAmount = BN(oldInputValue).multipliedBy(10 ** inputDecimals);
const outputAmount = calculateEtherTokenOutput({ inputAmount, inputReserve, outputReserve });
const exchangeRate = outputAmount.dividedBy(inputAmount);
const outputValue = outputAmount.dividedBy(BN(10 ** outputDecimals)).toFixed(7);
const appendState = {};
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate;
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue;
}
this.setState(appendState);
} else if (lastEditedField === OUTPUT) {
if (!oldOutputValue) {
return this.setState({
inputValue: '',
exchangeRate: BN(0),
});
}
const outputAmount = BN(oldOutputValue).multipliedBy(10 ** outputDecimals);
const inputAmount = calculateEtherTokenInput({ outputAmount, inputReserve, outputReserve });
const exchangeRate = outputAmount.dividedBy(inputAmount);
const inputValue = inputAmount.isNegative()
? 'N/A'
: inputAmount.dividedBy(BN(10 ** inputDecimals)).toFixed(7);
const appendState = {};
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate;
}
if (inputValue !== oldInputValue) {
appendState.inputValue = inputValue;
}
this.setState(appendState);
}
};
updateInput = amount => {
this.setState({
inputValue: amount,
lastEditedField: INPUT,
}, this.recalcForm);
};
updateOutput = amount => {
this.setState({
outputValue: amount,
lastEditedField: OUTPUT,
}, this.recalcForm);
};
onSwap = async () => {
const {
exchangeAddresses: { fromToken },
account, account,
contracts, web3,
selectors,
} = this.props; } = this.props;
const {
const { drizzle } = this.context; inputValue,
let swapTxId; outputValue,
inputCurrency,
if (lastEditedField === 'input') { outputCurrency,
swapTxId = await swapInput({ inputAmountB,
drizzleCtx: drizzle, lastEditedField,
contractStore: contracts, } = this.state;
input, const ALLOWED_SLIPPAGE = 0.025;
output, const TOKEN_ALLOWED_SLIPPAGE = 0.04;
inputCurrency,
outputCurrency, const type = getSwapType(inputCurrency, outputCurrency);
exchangeAddresses, const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
account, const { decimals: outputDecimals } = selectors().getBalance(account, outputCurrency);
}); const blockNumber = await promisify(web3, 'getBlockNumber');
const block = await promisify(web3, 'getBlock', blockNumber);
const deadline = block.timestamp + 300;
if (lastEditedField === INPUT) {
// swap input
switch(type) {
case 'ETH_TO_TOKEN':
// let exchange = new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency]);
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency])
.methods
.ethToTokenSwapInput(
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(0),
deadline,
)
.send({
from: account,
value: BN(inputValue).multipliedBy(10 ** 18).toFixed(0),
}, err => !err && this.reset());
break;
case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency])
.methods
.tokenToEthSwapInput(
BN(inputValue).multipliedBy(10 ** inputDecimals).toFixed(0),
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - ALLOWED_SLIPPAGE).toFixed(0),
deadline,
)
.send({
from: account,
}, err => !err && this.reset());
break;
case 'TOKEN_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency])
.methods
.tokenToTokenSwapInput(
BN(inputValue).multipliedBy(10 ** inputDecimals).toFixed(0),
BN(outputValue).multipliedBy(10 ** outputDecimals).multipliedBy(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(0),
'1',
deadline,
outputCurrency,
)
.send({
from: account,
}, err => !err && this.reset());
break;
default:
break;
}
} }
if (lastEditedField === 'output') { if (lastEditedField === OUTPUT) {
swapTxId = await swapOutput({ // swap output
drizzleCtx: drizzle, switch (type) {
contractStore: contracts, case 'ETH_TO_TOKEN':
input, new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency])
output, .methods
inputCurrency, .ethToTokenSwapOutput(
outputCurrency, BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0),
exchangeAddresses, deadline,
account, )
}); .send({
from: account,
value: BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(0),
}, err => !err && this.reset());
break;
case 'TOKEN_TO_ETH':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[inputCurrency])
.methods
.tokenToEthSwapOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0),
BN(inputValue).multipliedBy(10 ** inputDecimals).multipliedBy(1 + ALLOWED_SLIPPAGE).toFixed(0),
deadline,
)
.send({
from: account,
}, err => !err && this.reset());
break;
case 'TOKEN_TO_TOKEN':
if (!inputAmountB) {
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(
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,
).send({
from: account,
}, err => !err && this.reset());
break;
default:
break;
}
} }
this.resetSwap();
this.setState({swapTxId});
// this.context.drizzle.web3.eth.getBlockNumber((_, d) => this.context.drizzle.web3.eth.getBlock(d, (_,d) => {
// const deadline = d.timestamp + 300;
// const id = exchange.methods.ethToTokenSwapInput.cacheSend(`${output * 10 ** 18}`, deadline, {
// from: "0xCf1dE0b4d1e492080336909f70413a5F4E7eEc62",
// value: `${input * 10 ** 18}`,
// }, );
// }));
}; };
handleSubButtonClick = () => { renderSummary() {
if (this.getIsUnapproved() && this.getApprovalStatus() !== 'pending') { const {
this.approveExchange(); inputValue,
inputCurrency,
outputValue,
outputCurrency,
} = this.state;
const { selectors, account } = this.props;
const { label: inputLabel } = selectors().getBalance(account, inputCurrency);
const { label: outputLabel } = selectors().getBalance(account, outputCurrency);
const SLIPPAGE = 0.025;
const minOutput = BN(outputValue).multipliedBy(1 - SLIPPAGE).toFixed(2);
const maxOutput = BN(outputValue).multipliedBy(1 + SLIPPAGE).toFixed(2);
if (!inputCurrency || !outputCurrency) {
return (
<div className="swap__summary-wrapper">
<div>Select a token to continue.</div>
</div>
)
} }
}
renderSubButtonText() { if (!inputValue || !outputValue) {
if (this.getApprovalStatus() === 'pending') { return (
return [ <div className="swap__summary-wrapper">
(<img key="pending" className="swap__sub-icon" src={Pending} />), <div>Enter a value to continue.</div>
(<span key="text" className="swap__sub-text">Pending</span>) </div>
]; )
} else {
return '🔒 Unlock'
} }
return (
<div className="swap__summary-wrapper">
<div>You are selling {b(`${inputValue} ${inputLabel}`)}</div>
<div>You will receive between {b(minOutput)} and {b(`${maxOutput} ${outputLabel}`)}</div>
</div>
)
} }
renderExchangeRate() { renderExchangeRate() {
const { const { account, selectors } = this.props;
inputCurrency, const { exchangeRate, inputCurrency, outputCurrency } = this.state;
outputCurrency, const { label: inputLabel } = selectors().getBalance(account, inputCurrency);
input, const { label: outputLabel } = selectors().getBalance(account, outputCurrency);
exchangeAddresses: { fromToken },
selectors,
} = this.props;
if (!inputCurrency || !outputCurrency || !input) { if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) {
return ( return (
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper"> <div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span> <span className="swap__exchange-rate">Exchange Rate</span>
<span> - </span>
</div> </div>
</OversizedPanel> </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 ( return (
<OversizedPanel hideBottom> <OversizedPanel hideBottom>
...@@ -331,16 +503,25 @@ class Swap extends Component { ...@@ -331,16 +503,25 @@ class Swap extends Component {
</span> </span>
</div> </div>
</OversizedPanel> </OversizedPanel>
) );
} }
render() { render() {
const { lastEditedField, inputCurrency, outputCurrency, input, output, isValid, outputErrors, inputErrors } = this.props; const { selectors, account } = this.props;
const { exchangeRate } = this.state; const {
const inputLabel = this.getTokenLabel(inputCurrency); lastEditedField,
const outputLabel = this.getTokenLabel(outputCurrency); inputCurrency,
outputCurrency,
inputValue,
outputValue,
} = this.state;
const estimatedText = '(estimated)'; const estimatedText = '(estimated)';
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
const { value: outputBalance, decimals: outputDecimals } = selectors().getBalance(account, outputCurrency);
const { inputError, outputError, isValid } = this.validate();
return ( return (
<div className="swap"> <div className="swap">
<Header /> <Header />
...@@ -351,19 +532,17 @@ class Swap extends Component { ...@@ -351,19 +532,17 @@ class Swap extends Component {
> >
<CurrencyInputPanel <CurrencyInputPanel
title="Input" title="Input"
description={lastEditedField === 'output' ? estimatedText : ''} description={lastEditedField === OUTPUT ? estimatedText : ''}
onCurrencySelected={d => this.props.updateField('inputCurrency', d)} extraText={inputCurrency
onValueChange={d => this.updateInput(d)} ? `Balance: ${inputBalance.dividedBy(BN(10 ** inputDecimals)).toFixed(4)}`
: ''
}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
selectedTokens={[inputCurrency, outputCurrency]} selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('inputErrors', error)}
removeError={error => this.props.removeError('inputErrors', error)}
errors={inputErrors}
value={input}
selectedTokenAddress={inputCurrency} selectedTokenAddress={inputCurrency}
shouldValidateBalance value={inputValue}
showSubButton={this.getIsUnapproved()} errorMessage={inputError}
subButtonContent={this.renderSubButtonText()}
onSubButtonClick={this.handleSubButtonClick}
/> />
<OversizedPanel> <OversizedPanel>
<div className="swap__down-arrow-background"> <div className="swap__down-arrow-background">
...@@ -372,35 +551,26 @@ class Swap extends Component { ...@@ -372,35 +551,26 @@ class Swap extends Component {
</OversizedPanel> </OversizedPanel>
<CurrencyInputPanel <CurrencyInputPanel
title="Output" title="Output"
description={lastEditedField === 'input' ? estimatedText : ''} description={lastEditedField === INPUT ? estimatedText : ''}
onCurrencySelected={d => this.props.updateField('outputCurrency', d)} extraText={outputCurrency
onValueChange={d => this.updateOutput(d)} ? `Balance: ${outputBalance.dividedBy(BN(10 ** outputDecimals)).toFixed(4)}`
: ''
}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]} selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('outputErrors', error)} value={outputValue}
removeError={error => this.props.removeError('outputErrors', error)}
errors={outputErrors}
value={output}
selectedTokenAddress={outputCurrency} selectedTokenAddress={outputCurrency}
errorMessage={outputError}
/> />
{ this.renderExchangeRate() } { this.renderExchangeRate() }
{ { this.renderSummary() }
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> </div>
<button <button
className={classnames('swap__cta-btn', { className={classnames('swap__cta-btn', {
'swap--inactive': !this.props.isConnected, 'swap--inactive': !this.props.isConnected,
'swap__cta-btn--inactive': !this.props.isValid,
})} })}
disabled={!this.props.isValid} disabled={!isValid}
onClick={this.onSwap} onClick={this.onSwap}
> >
Swap Swap
...@@ -410,40 +580,71 @@ class Swap extends Component { ...@@ -410,40 +580,71 @@ class Swap extends Component {
} }
} }
export default withRouter( export default drizzleConnect(
drizzleConnect( Swap,
Swap, state => ({
(state, ownProps) => ({ balances: state.web3connect.balances,
balances: state.web3connect.balances, isConnected: !!state.web3connect.account,
// React Router account: state.web3connect.account,
push: ownProps.history.push, web3: state.web3connect.web3,
pathname: ownProps.location.pathname, exchangeAddresses: state.addresses.exchangeAddresses,
}),
// From Drizzle dispatch => ({
initialized: state.drizzleStatus.initialized, selectors: () => dispatch(selectors()),
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,
lastEditedField: state.swap.lastEditedField,
exchangeAddresses: state.addresses.exchangeAddresses,
isValid: isValidSwap(state),
inputErrors: state.swap.inputErrors,
outputErrors: state.swap.outputErrors,
}),
dispatch => ({
updateField: (name, value) => dispatch(updateField({ name, value })),
addError: (name, value) => dispatch(addError({ name, value })),
removeError: (name, value) => dispatch(removeError({ name, value })),
resetSwap: () => dispatch(resetSwap()),
selectors: () => dispatch(selectors()),
})
),
); );
const b = text => <span className="swap__highlight-text">{text}</span>;
function calculateEtherTokenOutput({ inputAmount: rawInput, inputReserve: rawReserveIn, outputReserve: rawReserveOut }) {
const inputAmount = BN(rawInput);
const inputReserve = BN(rawReserveIn);
const outputReserve = BN(rawReserveOut);
if (inputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${inputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`);
}
const numerator = inputAmount.multipliedBy(outputReserve).multipliedBy(997);
const denominator = inputReserve.multipliedBy(1000).plus(inputAmount.multipliedBy(997));
return numerator.dividedBy(denominator);
}
function calculateEtherTokenInput({ outputAmount: rawOutput, inputReserve: rawReserveIn, outputReserve: rawReserveOut }) {
const outputAmount = BN(rawOutput);
const inputReserve = BN(rawReserveIn);
const outputReserve = BN(rawReserveOut);
if (outputAmount.isLessThan(BN(10 ** 9))) {
console.warn(`inputAmount is only ${outputAmount.toFixed(0)}. Did you forget to multiply by 10 ** decimals?`);
}
const numerator = outputAmount.multipliedBy(inputReserve).multipliedBy(1000);
const denominator = outputReserve.minus(outputAmount).multipliedBy(997);
return numerator.dividedBy(denominator.plus(1));
}
function getSwapType(inputCurrency, outputCurrency) {
if (!inputCurrency || !outputCurrency) {
return;
}
if (inputCurrency === outputCurrency) {
return;
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
return 'TOKEN_TO_TOKEN'
}
if (inputCurrency === 'ETH') {
return 'ETH_TO_TOKEN';
}
if (outputCurrency === 'ETH') {
return 'TOKEN_TO_ETH';
}
return;
}
...@@ -59,10 +59,6 @@ ...@@ -59,10 +59,6 @@
&__cta-btn { &__cta-btn {
@extend %primary-button; @extend %primary-button;
margin: 1rem auto; margin: 1rem auto;
&--inactive {
background: $mercury-gray;
}
} }
&__sub-icon { &__sub-icon {
......
...@@ -41,6 +41,7 @@ $salmon-red: #FF8368; ...@@ -41,6 +41,7 @@ $salmon-red: #FF8368;
outline: none; outline: none;
border: 1px solid transparent; border: 1px solid transparent;
user-select: none; user-select: none;
transition: background-color 300ms ease-in-out;
&:hover { &:hover {
background-color: lighten($royal-blue, 5); background-color: lighten($royal-blue, 5);
...@@ -50,6 +51,10 @@ $salmon-red: #FF8368; ...@@ -50,6 +51,10 @@ $salmon-red: #FF8368;
background-color: darken($royal-blue, 5); background-color: darken($royal-blue, 5);
} }
&:disabled {
background-color: $mercury-gray;
}
} }
%borderless-input { %borderless-input {
......
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