Refactor Swap (#80)

* Refactor calculateInput for ETH-TOKEN swap

* Refactor Swap Input to not use drizzle

* Refactor Swap Output
This commit is contained in:
Chi Kei Chan 2018-10-25 03:46:46 -07:00 committed by GitHub
parent 6464e44310
commit dadbc4e441
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 591 additions and 380 deletions

@ -11,6 +11,7 @@
border-radius: 1.25rem;
box-shadow: 0 0 0 .5px $mercury-gray;
background-color: $white;
transition: box-shadow 200ms ease-in-out;
&--error {
box-shadow: 0 0 0 .5px $salmon-red;

@ -37,8 +37,28 @@ const initialState = {
export const selectors = () => (dispatch, getState) => {
const state = getState().web3connect;
return {
getBalance: address => {
const 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 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];
if (!balance) {
@ -46,23 +66,16 @@ export const selectors = () => (dispatch, getState) => {
return Balance(0, 'ETH');
}
return balance;
},
} else if (tokenAddress) {
return getTokenBalance(tokenAddress, address);
}
getTokenBalance: (tokenAddress, address) => {
const tokenBalances = state.balances[tokenAddress];
return Balance(NaN);
};
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;
},
return {
getBalance,
getTokenBalance,
}
};
@ -124,6 +137,7 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState
type: WATCH_ETH_BALANCE,
payload: balanceOf,
});
setTimeout(() => dispatch(sync()), 0);
} else if (tokenAddress) {
if (watched.balances[tokenAddress] && watched.balances[tokenAddress].includes(balanceOf)) {
return;
@ -135,6 +149,7 @@ export const watchBalance = ({ balanceOf, tokenAddress }) => (dispatch, getState
balanceOf,
},
});
setTimeout(() => dispatch(sync()), 0);
}
};
@ -199,7 +214,6 @@ export const sync = () => async (dispatch, getState) => {
const symbol = tokenBalance.label || await contract.methods.symbol().call();
if (tokenBalance.value.isEqualTo(BN(balance))) {
console.log('block');
return;
}

@ -57,12 +57,7 @@ class AddLiquidity extends Component {
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);
const { value, decimals } = selectors().getBalance(account, currency);
return `Balance: ${value.dividedBy(10 ** decimals).toFixed(4)}`;
}
@ -85,7 +80,7 @@ class AddLiquidity extends Component {
const maxTokens = tokenAmount.multipliedBy(1 + MAX_LIQUIDITY_SLIPPAGE);
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,
value: ethAmount.toFixed(0)
});
@ -144,8 +139,8 @@ class AddLiquidity extends Component {
return;
}
const { value: tokenValue } = selectors().getTokenBalance(token, fromToken[token]);
const { value: ethValue } = selectors().getBalance(fromToken[token]);
const { value: tokenValue } = selectors().getBalance(fromToken[token], token);
const { value: ethValue } = selectors().getBalance(fromToken[token], eth);
return tokenValue.dividedBy(ethValue);
}
@ -165,8 +160,8 @@ class AddLiquidity extends Component {
isValid = false;
}
const { value: ethValue } = selectors().getBalance(account);
const { value: tokenValue, decimals } = selectors().getTokenBalance(outputCurrency, account);
const { value: ethValue } = selectors().getBalance(account, inputCurrency);
const { value: tokenValue, decimals } = selectors().getBalance(account, outputCurrency);
if (ethValue.isLessThan(BN(inputValue * 10 ** 18))) {
inputError = 'Insufficient Balance';
@ -329,7 +324,6 @@ class AddLiquidity extends Component {
selectedTokenAddress={outputCurrency}
onCurrencySelected={currency => {
this.setState({ outputCurrency: currency });
this.props.sync();
}}
onValueChange={this.onOutputChange}
value={outputValue}

@ -1,326 +1,498 @@
import React, { Component } from 'react';
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 deepEqual from 'deep-equal';
import { isValidSwap, updateField, addError, removeError, resetSwap } from '../../ducks/swap';
import { selectors, sync } from '../../ducks/web3connect';
import { selectors } from '../../ducks/web3connect';
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 Pending from '../../assets/images/pending.svg';
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 EXCHANGE_ABI from '../../abi/exchange';
import "./swap.scss";
import promisify from "../../helpers/web3-promisfy";
const INPUT = 0;
const OUTPUT = 1;
class Swap extends Component {
static propTypes = {
// Injected by React Router Dom
push: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired,
currentAddress: PropTypes.string,
account: PropTypes.string,
isConnected: 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,
};
static contextTypes = {
drizzle: PropTypes.object,
web3: PropTypes.object.isRequired,
};
state = {
exchangeRate: BN(0),
approvalTxId: null,
swapTxId: null,
inputValue: '',
outputValue: '',
inputCurrency: '',
outputCurrency: '',
inputAmountB: '',
lastEditedField: '',
};
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(nextProps, this.props) ||
!deepEqual(nextState, this.state);
return true;
}
componentWillUnmount() {
this.resetSwap();
}
resetSwap() {
this.props.resetSwap();
this.setState({approvalTxId: null, swapTxId: null});
}
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;
}
updateInput(amount) {
this.props.updateField('input', amount);
if (!amount) {
this.props.updateField('output', '');
}
// calculate exchangerate
// output = amount * exchagerate
// this.props.updateField('output', output);
this.props.updateField('lastEditedField', 'input');
}
updateOutput(amount) {
this.props.updateField('output', amount);
if (!amount) {
this.props.updateField('input', '');
}
this.props.updateField('lastEditedField', 'output');
}
async getExchangeRate(props) {
const {
input,
output,
inputCurrency,
outputCurrency,
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() {
const {
input,
inputCurrency,
account,
contracts,
exchangeAddresses
} = this.props;
const { drizzle } = this.context;
return isExchangeUnapproved({
value: input,
currency: inputCurrency,
drizzleCtx: drizzle,
contractStore: contracts,
account,
exchangeAddresses,
reset() {
this.setState({
inputValue: '',
outputValue: '',
inputCurrency: '',
outputCurrency: '',
inputAmountB: '',
lastEditedField: '',
});
}
approveExchange = async () => {
componentWillReceiveProps() {
this.recalcForm();
}
validate() {
const { selectors, account } = this.props;
const {
inputCurrency,
exchangeAddresses,
account,
contracts,
} = this.props;
const { drizzle } = this.context;
inputValue, outputValue,
inputCurrency, outputCurrency,
} = this.state;
if (this.getIsUnapproved()) {
const approvalTxId = await approveExchange({
currency: inputCurrency,
drizzleCtx: drizzle,
contractStore: contracts,
account,
exchangeAddresses,
});
let inputError = '';
let outputError = '';
let isValid = true;
this.setState({ approvalTxId })
if (!inputValue || !outputValue || !inputCurrency || !outputCurrency) {
isValid = false;
}
const { value: inputBalance, decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
if (inputBalance.isLessThan(BN(inputValue * 10 ** inputDecimals))) {
inputError = 'Insufficient Balance';
}
if (inputValue === 'N/A') {
inputError = 'Not a valid input value';
}
return {
inputError,
outputError,
isValid: isValid && !inputError && !outputError,
};
}
getApprovalStatus() {
const { drizzle } = this.context;
recalcForm() {
const { inputCurrency, outputCurrency } = this.state;
return getTxStatus({
drizzleCtx: drizzle,
txId: this.state.approvalTxId,
});
if (!inputCurrency || !outputCurrency) {
return;
}
if (inputCurrency === outputCurrency) {
this.setState({
inputValue: '',
outputValue: '',
});
return;
}
if (inputCurrency !== 'ETH' && outputCurrency !== 'ETH') {
this.recalcTokenTokenForm();
return;
}
this.recalcEthTokenForm();
}
getSwapStatus() {
const { drizzle } = this.context;
return getTxStatus({
drizzleCtx: drizzle,
txId: this.state.swapTxId,
});
}
onSwap = async () => {
recalcTokenTokenForm = () => {
const {
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
lastEditedField,
account,
contracts,
} = this.props;
const { drizzle } = this.context;
let swapTxId;
if (lastEditedField === 'input') {
swapTxId = await swapInput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
account,
});
}
if (lastEditedField === 'output') {
swapTxId = await swapOutput({
drizzleCtx: drizzle,
contractStore: contracts,
input,
output,
inputCurrency,
outputCurrency,
exchangeAddresses,
account,
});
}
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 = () => {
if (this.getIsUnapproved() && this.getApprovalStatus() !== 'pending') {
this.approveExchange();
}
}
renderSubButtonText() {
if (this.getApprovalStatus() === 'pending') {
return [
(<img key="pending" className="swap__sub-icon" src={Pending} />),
(<span key="text" className="swap__sub-text">Pending</span>)
];
} else {
return '🔒 Unlock'
}
}
renderExchangeRate() {
const {
inputCurrency,
outputCurrency,
input,
exchangeAddresses: { fromToken },
selectors,
} = this.props;
if (!inputCurrency || !outputCurrency || !input) {
const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency,
outputCurrency,
lastEditedField,
exchangeRate: oldExchangeRate,
inputAmountB: oldInputAmountB,
} = this.state;
const exchangeAddressA = fromToken[inputCurrency];
const exchangeAddressB = fromToken[outputCurrency];
const { value: inputReserveA, decimals: inputDecimalsA } = selectors().getBalance(exchangeAddressA, inputCurrency);
const { value: outputReserveA }= selectors().getBalance(exchangeAddressA, 'ETH');
const { value: inputReserveB } = selectors().getBalance(exchangeAddressB, 'ETH');
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);
const appendState = {};
if (!exchangeRate.isEqualTo(BN(oldExchangeRate))) {
appendState.exchangeRate = exchangeRate;
}
if (outputValue !== oldOutputValue) {
appendState.outputValue = outputValue;
}
this.setState(appendState);
}
if (lastEditedField === OUTPUT) {
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,
});
// Redundant Variable for readability of the formala
// InputAmount from the first swap becomes OutputAmount of the second swap
const outputAmountA = inputAmountB;
const inputAmountA = calculateEtherTokenInput({
outputAmount: outputAmountA,
inputReserve: inputReserveA,
outputReserve: outputReserveA,
});
const exchangeRate = outputAmountB.dividedBy(inputAmountA);
const inputValue = inputAmountA.isNegative()
? 'N/A'
: inputAmountA.dividedBy(BN(10 ** inputDecimalsA)).toFixed(7);
const appendState = {};
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 {
exchangeAddresses: { fromToken },
selectors,
} = this.props;
const {
inputValue: oldInputValue,
outputValue: oldOutputValue,
inputCurrency,
outputCurrency,
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,
web3,
selectors,
} = this.props;
const {
inputValue,
outputValue,
inputCurrency,
outputCurrency,
inputAmountB,
lastEditedField,
} = this.state;
const ALLOWED_SLIPPAGE = 0.025;
const TOKEN_ALLOWED_SLIPPAGE = 0.04;
const type = getSwapType(inputCurrency, outputCurrency);
const { decimals: inputDecimals } = selectors().getBalance(account, inputCurrency);
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) {
// swap output
switch (type) {
case 'ETH_TO_TOKEN':
new web3.eth.Contract(EXCHANGE_ABI, fromToken[outputCurrency])
.methods
.ethToTokenSwapOutput(
BN(outputValue).multipliedBy(10 ** outputDecimals).toFixed(0),
deadline,
)
.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;
}
}
};
renderSummary() {
const {
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>
)
}
if (!inputValue || !outputValue) {
return (
<div className="swap__summary-wrapper">
<div>Enter a value to continue.</div>
</div>
)
}
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() {
const { account, selectors } = this.props;
const { exchangeRate, inputCurrency, outputCurrency } = this.state;
const { label: inputLabel } = selectors().getBalance(account, inputCurrency);
const { label: outputLabel } = selectors().getBalance(account, outputCurrency);
if (!exchangeRate || exchangeRate.isNaN() || !inputCurrency || !outputCurrency) {
return (
<OversizedPanel hideBottom>
<div className="swap__exchange-rate-wrapper">
<span className="swap__exchange-rate">Exchange Rate</span>
<span> - </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>
@ -331,16 +503,25 @@ class Swap extends Component {
</span>
</div>
</OversizedPanel>
)
);
}
render() {
const { lastEditedField, inputCurrency, outputCurrency, input, output, isValid, outputErrors, inputErrors } = this.props;
const { exchangeRate } = this.state;
const inputLabel = this.getTokenLabel(inputCurrency);
const outputLabel = this.getTokenLabel(outputCurrency);
const { selectors, account } = this.props;
const {
lastEditedField,
inputCurrency,
outputCurrency,
inputValue,
outputValue,
} = this.state;
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 (
<div className="swap">
<Header />
@ -351,19 +532,17 @@ class Swap extends Component {
>
<CurrencyInputPanel
title="Input"
description={lastEditedField === 'output' ? estimatedText : ''}
onCurrencySelected={d => this.props.updateField('inputCurrency', d)}
onValueChange={d => this.updateInput(d)}
description={lastEditedField === OUTPUT ? estimatedText : ''}
extraText={inputCurrency
? `Balance: ${inputBalance.dividedBy(BN(10 ** inputDecimals)).toFixed(4)}`
: ''
}
onCurrencySelected={inputCurrency => this.setState({ inputCurrency }, this.recalcForm)}
onValueChange={this.updateInput}
selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('inputErrors', error)}
removeError={error => this.props.removeError('inputErrors', error)}
errors={inputErrors}
value={input}
selectedTokenAddress={inputCurrency}
shouldValidateBalance
showSubButton={this.getIsUnapproved()}
subButtonContent={this.renderSubButtonText()}
onSubButtonClick={this.handleSubButtonClick}
value={inputValue}
errorMessage={inputError}
/>
<OversizedPanel>
<div className="swap__down-arrow-background">
@ -372,35 +551,26 @@ class Swap extends Component {
</OversizedPanel>
<CurrencyInputPanel
title="Output"
description={lastEditedField === 'input' ? estimatedText : ''}
onCurrencySelected={d => this.props.updateField('outputCurrency', d)}
onValueChange={d => this.updateOutput(d)}
description={lastEditedField === INPUT ? estimatedText : ''}
extraText={outputCurrency
? `Balance: ${outputBalance.dividedBy(BN(10 ** outputDecimals)).toFixed(4)}`
: ''
}
onCurrencySelected={outputCurrency => this.setState({ outputCurrency }, this.recalcForm)}
onValueChange={this.updateOutput}
selectedTokens={[inputCurrency, outputCurrency]}
addError={error => this.props.addError('outputErrors', error)}
removeError={error => this.props.removeError('outputErrors', error)}
errors={outputErrors}
value={output}
value={outputValue}
selectedTokenAddress={outputCurrency}
errorMessage={outputError}
/>
{ this.renderExchangeRate() }
{
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
}
{ this.renderSummary() }
</div>
<button
className={classnames('swap__cta-btn', {
'swap--inactive': !this.props.isConnected,
'swap__cta-btn--inactive': !this.props.isValid,
})}
disabled={!this.props.isValid}
disabled={!isValid}
onClick={this.onSwap}
>
Swap
@ -410,40 +580,71 @@ class Swap extends Component {
}
}
export default withRouter(
drizzleConnect(
Swap,
(state, ownProps) => ({
balances: state.web3connect.balances,
// 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,
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()),
})
),
export default drizzleConnect(
Swap,
state => ({
balances: state.web3connect.balances,
isConnected: !!state.web3connect.account,
account: state.web3connect.account,
web3: state.web3connect.web3,
exchangeAddresses: state.addresses.exchangeAddresses,
}),
dispatch => ({
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 @@
&__cta-btn {
@extend %primary-button;
margin: 1rem auto;
&--inactive {
background: $mercury-gray;
}
}
&__sub-icon {

@ -41,6 +41,7 @@ $salmon-red: #FF8368;
outline: none;
border: 1px solid transparent;
user-select: none;
transition: background-color 300ms ease-in-out;
&:hover {
background-color: lighten($royal-blue, 5);
@ -50,6 +51,10 @@ $salmon-red: #FF8368;
background-color: darken($royal-blue, 5);
}
&:disabled {
background-color: $mercury-gray;
}
}
%borderless-input {