Refactor Swap (#80)
* Refactor calculateInput for ETH-TOKEN swap * Refactor Swap Input to not use drizzle * Refactor Swap Output
This commit is contained in:
parent
6464e44310
commit
dadbc4e441
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user