Skip to content

Instantly share code, notes, and snippets.

@mqklin
Created February 5, 2019 14:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mqklin/73bc7306e5ff3a30f599c463fc1537f7 to your computer and use it in GitHub Desktop.
Save mqklin/73bc7306e5ff3a30f599c463fc1537f7 to your computer and use it in GitHub Desktop.
import React, {Component} from 'react';
import styled, {css} from 'styled-components';
import {forbidExtraProps, explicitNull, or} from 'airbnb-prop-types';
import getContext from 'getContext';
import {onlyUpdateForKeys} from 'recompose';
import {string, object, number, func, array} from 'prop-types';
import {Link, ButtonContainer, BounceSpinner, ReconnectOverlay} from 'App/dumb';
import SelectAssetModal from './SelectAssetModal';
import {t, formatToTwoNonZeroDecimals, webConnector, convertToEth} from 'App/utils';
import trustedAssetsRaw from './trustedAssetsRaw';
import erc20ABI from './erc20ABI';
import exchangeABI from './exchangeABI';
import {createIcon} from '@download/blockies';
import ModalSuccess from './ModalSuccess';
import {WALLETCONNECT} from 'App/utils/getEthereumProviderName';
const Wr = styled.div`
flex: 1;
position: relative;
`;
const Container = styled.div`
max-width: 1200px;
padding: 40px 15px 0 15px;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
filter: ${props => props.isReconnectOverlayShown ? 'blur(3px)' : 'none'};
-webkit-transform: translateZ(0); //retina filter bug fix
@media (max-width: 520px) {
padding: 40px 0 0 0;
}
`;
const Content = styled.div`
background: #fff;
width: 100%;
display: flex;
justify-content: center;
height: 100%;
padding: 24px 0;
@media (max-width: 1410px) {
margin: 0 111px;
}
@media (max-width: 1080px) {
margin: 0;
}
@media (max-width: 520px) {
padding: 24px 15px;
min-width: 320px;
}
`;
const ContentContainer = styled.div`
width: 450px;
@media (max-width: 520px) {
width: 100%;
}
`;
const Block = styled.div`
& ~ & {
margin-top: 16px;
}
@media (max-width: 520px) {
margin: 20px 0 0 0;
}
`;
const BlockTitle = styled.div`
line-height: 16px;
font-size: 13px;
color: #535463;
margin-bottom: 4px;
position: relative;
`;
const Balance = styled.div`
line-height: 16px;
font-size: 12px;
color: #A9AAB1;
position: absolute;
right: 0;
top: 0;
font-family: Roboto;
`;
const BalanceButton = styled(ButtonContainer)`
line-height: 16px;
font-size: 12px;
color: #4D84FC;
position: absolute;
right: 0;
top: 0;
width: auto;
font-family: Roboto;
`;
const SelectInputRow = styled.div`
display: flex;
position: relative;
`;
const SelectButton = styled(ButtonContainer)`
background: #EFF0F9;
border: 1px solid rgba(223, 228, 232, 0.6);
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
display: flex;
align-items: center;
padding: 10px 9px 10px 6px;
width: 104px;
span {
line-height: 24px;
font-size: 14px;
color: #010101;
padding-left: 6px;
}
position: relative;
&::after {
content: '';
position: absolute;
background-image: url(${require('./select-arrow.svg')});
width: 8px;
height: 5px;
right: 9px;
}
`;
const AssetIcon = styled.img`
width: 28px;
height: 28px;
display: ${props => props.areIconsLoaded ? 'block' : 'none'};
`;
const AssetIconTexted = styled.div`
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-weight: 500;
font-size: 11px;
line-height: 16px;
color: rgba(235, 87, 87, 1);
background: rgba(40, 48, 132, 1);
text-transform: uppercase;
`;
const Input = styled.input`
flex: 1;
border: 1px solid #E1E4E7;
box-sizing: border-box;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
border-left: none;
padding: 16px;
font-family: Roboto;
line-height: 16px;
font-size: 15px;
color: #535463;
&:focus {
outline: none;
position: relative;
border-bottom-color: #31B5FF;
box-shadow: inset 0 -1px 0 #31B5FF;
}
&::placeholder {
font-family: Roboto;
line-height: 16px;
font-size: 15px;
color: #A9AAB1;
}
@media (max-width: 520px) {
width: 100%;
color: rgba(9, 10, 31, 1);
}
`;
const UnlockButton = styled(
({showSpinner, ...props}) => <ButtonContainer {...props}/>,
)`
${css`
line-height: 15px;
font-size: 11px;
color: #2D368A;
padding: 5px 14px 5px 26px;
background: #EFF0F9;
position: absolute;
border: 1px solid rgba(45, 54, 138, 0.02);
border-radius: 16px;
width: auto;
right: 8px;
top: 50%;
transform: translateY(-50%);
&:disabled {
cursor: default;
}
span {
position: relative;
}
span::before {
content: '';
position: absolute;
width: 8px;
height: 10px;
background: url(${require('./unlock.svg')});
left: -12px;
top: 1px;
${props => props.showSpinner && css`
left: -16px;
top: 1px;
width: 12px;
height: 12px;
background: url(${require('./spinner.png')}) no-repeat;
background-size: cover;
@keyframes spinner {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
animation: spinner .6s linear infinite;
`};
}
@media (max-width: 520px) {
width: 100%;
position: static;
margin: 8px 0 0 0;
display: flex;
justify-content: center;
right: 8px;
top: unset;
transform: none;
padding: 8px 0;
&.lg-top-margin {
margin: 20px 0 0 0;
}
}
`}
`;
const InputError = styled.div`
font-family: 'Roboto';
line-height: 16px;
font-size: 11px;
color: #EB5757;
position: absolute;
bottom: 0;
transform: translateY(100%);
left: 105px;
`;
const ExchangeDetails = styled.div`
background: rgba(193, 200, 203, 0.1);
border: 1px solid rgba(120, 143, 177, 0.1);
border-radius: 4px;
padding: 12px 16px 20px;
`;
const ExchangeDetailsRow = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
& ~ & {
margin-top: 12px;
}
`;
const ExchangeDetailsRowName = styled.div`
line-height: 16px;
font-size: 13px;
color: #84858F;
`;
const ExchangeDetailsRowValue = styled.div`
font-family: 'Roboto';
line-height: 16px;
font-size: 13px;
position: relative;
color: #535463;
.gray {
font-family: inherit;
color: #A9AAB1;
}
`;
const WalletBlockie = styled.img`
width: 26px;
height: 26px;
border-radius: 50%;
position: absolute;
left: -3px;
top: 50%;
transform: translate(-100%, -50%);
`;
const ExchangeDetailsTotalRow = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 25px;
position: relative;
&::before {
content: '';
position: absolute;
top: 12px;
width: 100%;
height: 1px;
background: #C4C4C4;
opacity: 0.2;
}
`;
const ExchangeDetailsTotalRowName = styled.div`
font-weight: 500;
line-height: 16px;
font-size: 13px;
color: #84858F;
`;
const ExchangeDetailsTotalRowValue = styled.div`
font-family: Roboto;
font-weight: bold;
line-height: 16px;
font-size: 13px;
color: #535463;
.gray {
font-family: inherit;
color: #A9AAB1;
}
`;
const UniswapLink = styled(Link)`
text-decoration: underline;
`;
const ExchangeButton = styled(ButtonContainer)`
margin-top: 60px;
background: url(${require('./exchange.svg')}) no-repeat 40px center, #2D368A;
border-radius: 4px;
font-weight: 500;
line-height: 24px;
font-size: 16px;
text-align: center;
display: table;
width: auto;
padding: 8px 40px 8px 62px;
color: #fff;
&:disabled {
opacity: 0.5;
}
position: relative;
left: 50%;
transform: translateX(-50%);
span {
position: relative;
}
span::before {
display: none;
content: '';
position: absolute;
top: 1px;
left: -28px;
width: 16px;
height: 16px;
background: url(${require('./spinner.png')}) no-repeat;
background-size: cover;
@keyframes spinner {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
animation: spinner .6s linear infinite;
}
&.exchange-button-spinner {
background: rgba(45, 54, 138, 1);
span::before {
display: block;
}
};
@media (max-width: 520px) {
margin: 40px 0 0 0;
}
`;
const BounceSpinnerWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
filter: ${props => props.isReconnectOverlayShown ? 'blur(3px)' : 'none'};
-webkit-transform: translateZ(0); //retina filter bug fix
`;
const BounceSpinnerBlock = styled.div`
height: 80px;
width: 80px;
`;
const AssetIconPlaceholder = styled.div`
width: 28px;
height: 28px;
display: ${props => props.areIconsLoaded ? 'none' : 'block'};
background: rgba(214, 214, 214, 1);
border-radius: 50%;
`;
const DAI_CODE = '0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359';
const INITIAL_SEND_TOKEN = 'eth';
const INITIAL_RECEIVE_TOKEN = DAI_CODE;
@getContext([
'web3',
'selectedWalletAddress',
'selectedWalletAssets',
'assetsPrices',
'wiw',
'selectedWalletProviderName',
'addPendingTransaction',
'activeAddresses',
])
@onlyUpdateForKeys([
'web3',
'selectedWalletAddress',
'selectedWalletAssets',
'assetsPrices',
'wiw',
'selectedWalletProviderName',
'addPendingTransaction',
'activeAddresses',
])
export default class Exchange extends Component {
static propTypes = forbidExtraProps({
selectedWalletAddress: string.isRequired,
web3: object.isRequired,
selectedWalletAssets: object,
assetsPrices: object.isRequired,
wiw: number.isRequired,
selectedWalletProviderName: or([string.isRequired, explicitNull().isRequired]),
addPendingTransaction: func.isRequired,
activeAddresses: array.isRequired,
});
state = {
selectSendAssetModalIsShowed: false,
selectReceiveAssetModalIsShowed: false,
selectedSendAssetCode: INITIAL_SEND_TOKEN,
selectedReceiveAssetCode: INITIAL_RECEIVE_TOKEN,
trustedTokens: Object.keys(trustedAssetsRaw).reduce(
(acc, code) => {
acc[code] = {
decimals: 18,
icon_url: trustedAssetsRaw[code].icon_url,
price_usd: 0,
symbol: trustedAssetsRaw[code].symbol,
quantity: 0,
name: '',
decimaledQuantity: this.props.web3.toBigNumber(0),
};
return acc;
},
{
eth: {
decimals: 18,
icon_url: require('./trustedAssetsRaw/icons/eth.png'),
price_usd: 0,
symbol: 'ETH',
quantity: 0,
name: 'Ethereum',
decimaledQuantity: this.props.web3.toBigNumber(0),
},
},
),
tokensAreFetched: false,
sendAmount: '',
receiveAmount: '',
unlockButtonIsShowed: false,
unlockSpinnerIsShowed: false,
lastEditedInputName: null,
successTxHash: null,
isExchangeButtonSpinnerShowed: false,
blockchainStats: null,
gasCost: null,
initiallyLoadedTokenIconsCount: 0,
isReconnectOverlayShown: false,
};
componentDidMount() {
const {
web3,
activeAddresses,
selectedWalletAddress,
} = this.props;
this.SendContract = INITIAL_SEND_TOKEN === 'eth' ? null : web3.eth.contract(erc20ABI).at(INITIAL_SEND_TOKEN);
this.ReceiveContract = INITIAL_RECEIVE_TOKEN === 'eth' ? null : web3.eth.contract(erc20ABI).at(INITIAL_RECEIVE_TOKEN);
this.fetchBlockchainStats();
if (!activeAddresses.includes(selectedWalletAddress)) {
this.setState({isReconnectOverlayShown: true});
}
}
componentDidUpdate() {
const {
tokensAreFetched,
isReconnectOverlayShown,
} = this.state;
const {
selectedWalletAssets,
assetsPrices,
web3,
activeAddresses,
selectedWalletAddress,
} = this.props;
if (selectedWalletAssets !== undefined && !tokensAreFetched) {
fetch(`https://api.tokenary.io/api/v1/tokens/short?tokens=${Object.keys(trustedAssetsRaw).join(',')}`, {
method: 'GET',
headers: {
'X-Api-Key': process.env.TOKENARY_KEY,
Accepts: 'application/json',
},
})
.then(response => response.json())
.then(({data: {tokens}}) => {
this.setState(
() => ({
trustedTokens: Object.keys(tokens).reduce(
(acc, code) => {
acc[code] = {
decimals: tokens[code].decimals,
icon_url: trustedAssetsRaw[code].icon_url,
price_usd: tokens[code].price_usd,
symbol: trustedAssetsRaw[code].symbol,
name: tokens[code].title,
quantity: selectedWalletAssets[code] && selectedWalletAssets[code].quantity || 0,
};
acc[code].decimaledQuantity = web3.toBigNumber(10 ** -acc[code].decimals).mul(String(acc[code].quantity));
return acc;
},
{
eth: {
decimals: 18,
icon_url: require('./trustedAssetsRaw/icons/eth.png'),
price_usd: assetsPrices.eth.value.usd,
symbol: 'ETH',
quantity: selectedWalletAssets.eth.quantity,
name: 'Ethereum',
decimaledQuantity: web3.toBigNumber(10 ** -18).mul(String(selectedWalletAssets.eth.quantity)),
},
},
),
tokensAreFetched: true,
}),
this.checkIfHaveToUnlock,
);
});
}
if (!activeAddresses.includes(selectedWalletAddress)) {
if (!isReconnectOverlayShown) {
this.setState({isReconnectOverlayShown: true});
}
}
else {
if (isReconnectOverlayShown) {
this.setState({isReconnectOverlayShown: false});
}
}
}
fetchBlockchainStats = () => {
const url = process.env.MOCK_BACKEND === true
? 'http://localhost:3001/blockchain/stats'
: 'https://api-dev.tokenary.io/api/v1/blockchain/stats/full';
return fetch(url, {
method: 'GET',
headers: {
'X-Api-Key': 'testkey', // TODO
Accepts: 'application/json',
},
})
.then(response => response.json())
.then(data => {
this.setState(() => ({blockchainStats: data.data}));
});
};
handleUnlockClick = () => {
const {
web3,
selectedWalletProviderName,
} = this.props;
const {
trustedTokens,
selectedSendAssetCode,
} = this.state;
const {selectedWalletAddress} = this.props;
this.setState(() => ({unlockSpinnerIsShowed: true}));
const params = {
from: selectedWalletAddress,
to: selectedSendAssetCode,
data: this.SendContract.approve.getData(
trustedAssetsRaw[selectedSendAssetCode].exchangeAddress,
web3.toBigNumber(10).pow(trustedTokens[selectedSendAssetCode].decimals).mul(10 ** 9),
),
value: '0x0',
};
new Promise((resolve, reject) => {
if (selectedWalletProviderName === WALLETCONNECT) {
webConnector
.initSession()
.then(() => {
if (webConnector.isConnected) {
webConnector.sendTransaction(
params,
).then((hash, e) => {
e ? reject(e) : resolve(hash);
});
}
else {
console.error('Wallet session has expired. Please reconnect'); // eslint-disable-line
}
});
}
else {
web3.eth.sendTransaction(
params,
(e, hash) => {
e ? reject(e) : resolve(hash);
},
);
}
}).then(txHash => {
const intervalId = setInterval(
() => {
web3.eth.getTransactionReceipt(txHash, (e, tx) => {
if (e) {
throw new Error(e);
}
if (tx) {
clearInterval(intervalId);
this.setState(() => ({unlockSpinnerIsShowed: false, unlockButtonIsShowed: false}));
}
});
},
3000,
);
}).catch(e => {
this.setState(() => ({unlockSpinnerIsShowed: false}));
throw new Error(e);
});
};
checkIfHaveToUnlock = () => {
this.setState(
() => ({unlockButtonIsShowed: false}),
() => {
const {
selectedWalletAddress,
web3,
} = this.props;
const {
selectedSendAssetCode,
trustedTokens,
} = this.state;
if (selectedSendAssetCode === 'eth') {
return;
}
this.SendContract.allowance(selectedWalletAddress, trustedAssetsRaw[selectedSendAssetCode].exchangeAddress, (e, balance) => {
if (e) {
throw new Error(e);
}
if (balance.lessThan(web3.toBigNumber(10).pow(trustedTokens[selectedSendAssetCode].decimals).mul(10 ** 8))) {
this.setState(() => ({unlockButtonIsShowed: true}));
}
});
},
);
};
handleAmountChange = (fieldName, value) => {
const {[fieldName]: prevValue} = this.state;
this.setState(
() => ({lastEditedInputName: fieldName === 'sendAmount' ? 'send' : 'receive'}),
this.updateGasCost,
);
if (prevValue === '0' && value === '00') {
return;
}
if (/^\d+(?:\.(\d+)?)?$/g.test(value) || value === '') {
if (prevValue === '0' && value !== '0.') {
value = value.substring(1);
}
this.setState(
() => ({[fieldName]: value}),
this.updateGasCost,
);
}
};
handleTokenSelect = (type, code) => {
t.oneOf(['Send', 'Receive'], 'type')(type);
const {web3} = this.props;
this[`${type}Contract`] = code === 'eth' ? null : web3.eth.contract(erc20ABI).at(code);
this.setState(
() => ({
[`selected${type}AssetCode`]: code,
[`select${type}AssetModalIsShowed`]: false,
}),
() => {
this.checkIfHaveToUnlock();
const {sendAmount, receiveAmount} = this.state;
if (type === 'Send') {
this.handleReceiveInputChange(receiveAmount);
}
else {
this.handleSendInputChange(sendAmount);
}
},
);
};
handleSendInputChange = value => {
this.handleAmountChange('sendAmount', value);
if (value === '') {
this.setState(
() => ({receiveAmount: ''}),
this.updateGasCost,
);
return;
}
if (Number(value) === 0) {
this.setState(
() => ({receiveAmount: '0'}),
this.updateGasCost,
);
return;
}
if (!/^\d+(?:\.(\d+)?)?$/g.test(value)) {
return;
}
const {
selectedSendAssetCode,
selectedReceiveAssetCode,
trustedTokens,
} = this.state;
const {
web3,
} = this.props;
if (selectedSendAssetCode === selectedReceiveAssetCode) {
return;
}
if ([selectedSendAssetCode, selectedReceiveAssetCode].includes('eth')) { // ETH<->ERC20 or ERC20<->ETH
const sendIsEth = selectedSendAssetCode === 'eth';
const {exchangeAddress} = trustedAssetsRaw[sendIsEth ? selectedReceiveAssetCode : selectedSendAssetCode];
const inputAmount = web3.toBigNumber(10 ** trustedTokens[selectedSendAssetCode].decimals).mul(value);
web3.eth.getBalance(exchangeAddress, (e, exchangeEthBalance) => {
if (e) {
throw new Error(e);
}
this[sendIsEth ? 'ReceiveContract' : 'SendContract'].balanceOf(exchangeAddress, (e, exchangeTokenBalance) => {
if (e) {
throw new Error(e);
}
const [inputReserve, outputReserve] = sendIsEth ? [exchangeEthBalance, exchangeTokenBalance] : [exchangeTokenBalance, exchangeEthBalance];
const numerator = inputAmount.mul(outputReserve).mul(997);
const denominator = inputReserve.mul(1000).plus(inputAmount.mul(997));
const outputAmount = numerator.div(denominator);
const receiveDecimals = trustedTokens[selectedReceiveAssetCode].decimals;
this.setState(
() => ({receiveAmount: String(outputAmount.mul(10 ** -receiveDecimals).toFixed(receiveDecimals))}),
this.updateGasCost,
);
});
});
}
else { // ERC20<->ERC20
const [
{exchangeAddress: exchangeAddressA},
{exchangeAddress: exchangeAddressB},
] = [
trustedAssetsRaw[selectedSendAssetCode],
trustedAssetsRaw[selectedReceiveAssetCode],
];
const inputAmountA = web3.toBigNumber(10 ** trustedTokens[selectedSendAssetCode].decimals).mul(value);
web3.eth.getBalance(exchangeAddressA, (e, outputReserveA) => {
if (e) {
throw new Error(e);
}
this.SendContract.balanceOf(exchangeAddressA, (e, inputReserveA) => {
if (e) {
throw new Error(e);
}
const numeratorA = inputAmountA.mul(outputReserveA).mul(997);
const denominatorA = inputReserveA.mul(1000).plus(inputAmountA.mul(997));
const outputAmountA = numeratorA.div(denominatorA);
const inputAmountB = outputAmountA;
this.inputAmountB = inputAmountB;
web3.eth.getBalance(exchangeAddressB, (e, inputReserveB) => {
if (e) {
throw new Error(e);
}
this.ReceiveContract.balanceOf(exchangeAddressB, (e, outputReserveB) => {
if (e) {
throw new Error(e);
}
const numeratorB = inputAmountB.mul(outputReserveB).mul(997);
const denominatorB = inputReserveB.mul(1000).plus(inputAmountB.mul(997));
const outputAmountB = numeratorB.div(denominatorB);
const receiveDecimals = trustedTokens[selectedReceiveAssetCode].decimals;
this.setState(
() => ({receiveAmount: String(outputAmountB.mul(10 ** -receiveDecimals).toFixed(receiveDecimals))}),
this.updateGasCost,
);
});
});
});
});
}
};
handleReceiveInputChange = value => {
this.handleAmountChange('receiveAmount', value);
if (value === '') {
this.setState(
() => ({sendAmount: value}),
this.updateGasCost,
);
return;
}
if (Number(value) === 0) {
this.setState(
() => ({sendAmount: '0'}),
this.updateGasCost,
);
return;
}
if (!/^\d+(?:\.(\d+)?)?$/g.test(value)) {
return;
}
const {
selectedSendAssetCode,
selectedReceiveAssetCode,
trustedTokens,
} = this.state;
const {
web3,
} = this.props;
if (selectedSendAssetCode === selectedReceiveAssetCode) {
return;
}
if ([selectedSendAssetCode, selectedReceiveAssetCode].includes('eth')) { // ETH<->ERC20 or ERC20<->ETH
const sendIsEth = selectedSendAssetCode === 'eth';
const {exchangeAddress} = trustedAssetsRaw[sendIsEth ? selectedReceiveAssetCode : selectedSendAssetCode];
const outputAmount = web3.toBigNumber(10 ** trustedTokens[selectedReceiveAssetCode].decimals).mul(value);
web3.eth.getBalance(exchangeAddress, (e, exchangeEthBalance) => {
if (e) {
throw new Error(e);
}
this[sendIsEth ? 'ReceiveContract' : 'SendContract'].balanceOf(exchangeAddress, (e, exchangeTokenBalance) => {
if (e) {
throw new Error(e);
}
const [inputReserve, outputReserve] = sendIsEth ? [exchangeEthBalance, exchangeTokenBalance] : [exchangeTokenBalance, exchangeEthBalance];
const numerator = outputAmount.mul(inputReserve).mul(1000);
const denominator = (outputReserve.sub(outputAmount)).mul(997);
const inputAmount = numerator.div(denominator.plus(1));
const sendDecimals = trustedTokens[selectedSendAssetCode].decimals;
this.setState(
() => ({sendAmount: String(inputAmount.mul(10 ** -sendDecimals).toFixed(sendDecimals))}),
this.updateGasCost,
);
});
});
}
else { // ERC20<->ERC20
const [
{exchangeAddress: exchangeAddressA},
{exchangeAddress: exchangeAddressB},
] = [
trustedAssetsRaw[selectedSendAssetCode],
trustedAssetsRaw[selectedReceiveAssetCode],
];
const outputAmountB = web3.toBigNumber(10 ** trustedTokens[selectedReceiveAssetCode].decimals).mul(value);
web3.eth.getBalance(exchangeAddressB, (e, inputReserveB) => {
if (e) {
throw new Error(e);
}
this.ReceiveContract.balanceOf(exchangeAddressB, (e, outputReserveB) => {
if (e) {
throw new Error(e);
}
const numeratorB = outputAmountB.mul(inputReserveB).mul(1000);
const denominatorB = (outputReserveB.sub(outputAmountB)).mul(997);
const inputAmountB = numeratorB.div(denominatorB.plus(1));
this.inputAmountB = inputAmountB;
const outputAmountA = inputAmountB;
web3.eth.getBalance(exchangeAddressA, (e, outputReserveA) => {
if (e) {
throw new Error(e);
}
this.SendContract.balanceOf(exchangeAddressA, (e, inputReserveA) => {
if (e) {
throw new Error(e);
}
const numeratorA = outputAmountA.mul(inputReserveA).mul(1000);
const denominatorA = (outputReserveA.sub(outputAmountA)).mul(997);
const inputAmountA = numeratorA.div(denominatorA.plus(1));
const sendDecimals = trustedTokens[selectedSendAssetCode].decimals;
this.setState(
() => ({sendAmount: String(inputAmountA.mul(10 ** -sendDecimals).toFixed(sendDecimals))}),
this.updateGasCost,
);
});
});
});
});
}
};
handleExchangeClick = async () => {
const {gasCost} = this.state;
this.setState(() => ({isExchangeButtonSpinnerShowed: true}));
try {
const exchangeParams = await this.getExchangeParameters();
exchangeParams.gas = gasCost;
this.sendTransaction(exchangeParams);
}
catch (e) {
throw new Error(e);
}
};
sendTransaction = sendParams => {
const {
web3,
selectedWalletProviderName,
} = this.props;
if (selectedWalletProviderName === WALLETCONNECT) {
webConnector
.initSession()
.then(() => {
if (webConnector.isConnected) {
webConnector.sendTransaction(
sendParams,
).then((hash, error) => {
this.handleExchangeTransaction(error, hash);
});
}
else {
console.error('Wallet session has expired. Please reconnect'); // eslint-disable-line
}
});
}
else {
web3.eth.sendTransaction(
sendParams,
this.handleExchangeTransaction,
);
}
};
getExchangeFormParameters = () => {
const {
trustedTokens,
selectedSendAssetCode,
selectedReceiveAssetCode,
sendAmount,
receiveAmount,
blockchainStats,
gasCost,
} = this.state;
const {
selectedWalletAddress,
web3,
} = this.props;
const selectedWalletAddressChecksumed = web3.toChecksumAddress(selectedWalletAddress);
const sendTokenSymbol = trustedTokens[selectedSendAssetCode].symbol;
const receiveTokenSymbol = trustedTokens[selectedReceiveAssetCode].symbol;
const sendAmountFormatted = sendAmount[sendAmount.length - 1] === '.' ? sendAmount.slice(0, -1) : sendAmount;
const receiveAmountFormatted = receiveAmount[receiveAmount.length - 1] === '.' ? receiveAmount.slice(0, -1) : receiveAmount;
const send = web3.toBigNumber(sendAmountFormatted || 0);
const sendUsd = send.mul(trustedTokens[selectedSendAssetCode].price_usd || 0);
const receive = web3.toBigNumber(receiveAmountFormatted || 0);
const receiveUsd = receive.mul(trustedTokens[selectedReceiveAssetCode].price_usd || 0);
const tokensBothAreErc20 = [selectedSendAssetCode, selectedReceiveAssetCode].every(s => s !== 'eth');
const exchangeFee = tokensBothAreErc20 ? web3.toBigNumber(sendAmount).mul(0.00591) : web3.toBigNumber(sendAmount).mul(0.003);
const exchangeFeeUsd = exchangeFee.mul(trustedTokens[selectedSendAssetCode].price_usd || 0);
const transactionFee = web3.toBigNumber(blockchainStats ? convertToEth(blockchainStats.suggested.average, 'wei') * gasCost : 0);
const transactionFeeUsd = web3.toBigNumber(blockchainStats ? convertToEth(blockchainStats.suggested.average, 'wei') * blockchainStats.ethereum.price_usd * gasCost : 0);
const total = send.add(exchangeFee).add(transactionFee);
const totalUsd = sendUsd.add(exchangeFeeUsd).add(transactionFeeUsd);
return {
selectedWalletAddressChecksumed,
sendTokenSymbol,
receiveTokenSymbol,
send,
sendUsd,
receive,
receiveUsd,
exchangeFee,
exchangeFeeUsd,
transactionFee,
transactionFeeUsd,
total,
totalUsd,
};
};
handleExchangeTransaction = (e, txHash) => {
if (e) {
throw new Error(e);
}
else if (txHash) {
const {addPendingTransaction} = this.props;
const {
selectedSendAssetCode,
selectedReceiveAssetCode,
sendAmount,
receiveAmount,
blockchainStats,
} = this.state;
const {
totalUsd,
transactionFee,
exchangeFee,
transactionFeeUsd,
exchangeFeeUsd,
} = this.getExchangeFormParameters();
let recipientAddress;
if (selectedSendAssetCode === 'eth') { // ETH<->ERC20
recipientAddress = trustedAssetsRaw[selectedReceiveAssetCode];
}
else { // ERC20<->ETH and ERC20<->ERC20
recipientAddress = trustedAssetsRaw[selectedSendAssetCode];
}
addPendingTransaction({
hash: txHash,
assetCode: selectedSendAssetCode,
assetsAmount: sendAmount,
usdAmount: String(totalUsd),
recipientAddress: recipientAddress.exchangeAddress,
fee: blockchainStats.suggested.average,
});
addPendingTransaction({
hash: txHash,
assetCode: selectedReceiveAssetCode,
assetsAmount: receiveAmount,
usdAmount: String(totalUsd),
recipientAddress: recipientAddress.exchangeAddress,
fee: blockchainStats.suggested.average,
});
addPendingTransaction({
hash: txHash,
assetCode: 'eth',
assetsAmount: String(exchangeFee.add(transactionFee)),
usdAmount: String(exchangeFeeUsd.add(transactionFeeUsd)),
recipientAddress: recipientAddress.exchangeAddress,
fee: blockchainStats.suggested.average,
});
this.setState(
() => ({
successTxHash: txHash,
isExchangeButtonSpinnerShowed: false,
}),
() => this.handleSendInputChange(''),
);
}
};
updateGasCost = async () => {
const {sendAmount, receiveAmount} = this.state;
const {web3} = this.props;
if (sendAmount === '' || receiveAmount === '') {
return;
}
try {
const exchangeParams = await this.getExchangeParameters();
web3.eth.estimateGas(
exchangeParams,
(e, gasCost) => {
if (e) {
throw new Error(e);
}
this.setState(() => ({gasCost: web3.fromDecimal(gasCost)}));
},
);
}
catch (e) {
throw new Error(e);
}
};
getExchangeParameters = () => {
const {
web3,
selectedWalletAddress,
} = this.props;
const {
blockchainStats,
} = this.state;
const ALLOWED_SLIPPAGE = 0.025;
const TOKEN_ALLOWED_SLIPPAGE = 0.04;
return new Promise((resolve, reject) => {
web3.eth.getBlock('latest', (e, block) => {
if (e) {
reject(e);
return;
}
const deadline = block.timestamp + 300;
const {
lastEditedInputName,
selectedSendAssetCode,
selectedReceiveAssetCode,
sendAmount,
receiveAmount,
trustedTokens,
} = this.state;
let exchangeParams;
if (lastEditedInputName === 'send') { // swap input
if (selectedSendAssetCode === 'eth') { // ETH<->ERC20
const {exchangeAddress} = trustedAssetsRaw[selectedReceiveAssetCode];
const contract = web3.eth.contract(exchangeABI).at(exchangeAddress);
exchangeParams = {
from: selectedWalletAddress,
to: exchangeAddress,
data: contract.ethToTokenSwapInput.getData(
web3.toBigNumber(10).pow(trustedTokens[selectedReceiveAssetCode].decimals).mul(sendAmount).mul(1 - ALLOWED_SLIPPAGE).toFixed(0),
deadline,
),
value: web3.fromDecimal(web3.toBigNumber(sendAmount).mul(10 ** 18).toFixed(0)),
};
}
else if (selectedReceiveAssetCode === 'eth') { // ERC20<->ETH
const {exchangeAddress} = trustedAssetsRaw[selectedSendAssetCode];
const contract = web3.eth.contract(exchangeABI).at(exchangeAddress);
exchangeParams = {
from: selectedWalletAddress,
to: exchangeAddress,
data: contract.tokenToEthSwapInput.getData(
web3.toBigNumber(10).pow(trustedTokens[selectedSendAssetCode].decimals).mul(sendAmount).toFixed(0),
web3.toBigNumber(10).pow(18).mul(receiveAmount).mul(1 - ALLOWED_SLIPPAGE).toFixed(0),
deadline,
),
value: '0x0',
};
}
else { // ERC20<->ERC20
const {exchangeAddress} = trustedAssetsRaw[selectedSendAssetCode];
const contract = web3.eth.contract(exchangeABI).at(exchangeAddress);
exchangeParams = {
from: selectedWalletAddress,
to: exchangeAddress,
data: contract.tokenToTokenSwapInput.getData(
web3.toBigNumber(10).pow(trustedTokens[selectedSendAssetCode].decimals).mul(sendAmount).toFixed(0),
web3.toBigNumber(10).pow(trustedTokens[selectedReceiveAssetCode].decimals).mul(receiveAmount).mul(1 - TOKEN_ALLOWED_SLIPPAGE).toFixed(0),
'1',
deadline,
selectedReceiveAssetCode,
),
value: '0x0',
};
}
}
else { // swap output
if (selectedSendAssetCode === 'eth') { // ETH<->ERC20
const {exchangeAddress} = trustedAssetsRaw[selectedReceiveAssetCode];
const contract = web3.eth.contract(exchangeABI).at(exchangeAddress);
exchangeParams = {
from: selectedWalletAddress,
to: exchangeAddress,
data: contract.ethToTokenSwapOutput.getData(
web3.toBigNumber(10).pow(trustedTokens[selectedReceiveAssetCode].decimals).mul(receiveAmount).toFixed(0),
deadline,
),
value: web3.fromDecimal(web3.toBigNumber(sendAmount).mul(10 ** 18).mul(1 + ALLOWED_SLIPPAGE).toFixed(0)),
};
}
else if (selectedReceiveAssetCode === 'eth') { // ERC20<->ETH
const {exchangeAddress} = trustedAssetsRaw[selectedSendAssetCode];
const contract = web3.eth.contract(exchangeABI).at(exchangeAddress);
exchangeParams = {
from: selectedWalletAddress,
to: exchangeAddress,
data: contract.tokenToEthSwapOutput.getData(
web3.toBigNumber(10).pow(18).mul(receiveAmount).toFixed(0),
web3.toBigNumber(10).pow(trustedTokens[selectedReceiveAssetCode].decimals).mul(sendAmount).mul(1 + ALLOWED_SLIPPAGE).toFixed(0),
deadline,
),
value: '0x0',
};
}
else { // ERC20<->ERC20
const {exchangeAddress} = trustedAssetsRaw[selectedSendAssetCode];
const contract = web3.eth.contract(exchangeABI).at(exchangeAddress);
exchangeParams = {
from: selectedWalletAddress,
to: exchangeAddress,
data: contract.tokenToTokenSwapOutput.getData(
web3.toBigNumber(10).pow(trustedTokens[selectedReceiveAssetCode].decimals).mul(receiveAmount).toFixed(0),
web3.toBigNumber(10).pow(trustedTokens[selectedSendAssetCode].decimals).mul(sendAmount).mul(1 + TOKEN_ALLOWED_SLIPPAGE).toFixed(0),
this.inputAmountB.mul(1.2).toFixed(0),
deadline,
selectedReceiveAssetCode,
),
value: '0x0',
};
}
}
if (blockchainStats) {
exchangeParams.gasPrice = web3.fromDecimal(blockchainStats.suggested.average);
}
resolve(exchangeParams);
});
});
};
handleTokenIconLoaded = () => {
const {initiallyLoadedTokenIconsCount} = this.state;
if (initiallyLoadedTokenIconsCount === 2) {
return;
}
this.setState(() => ({initiallyLoadedTokenIconsCount: initiallyLoadedTokenIconsCount + 1}));
};
render() {
const {
selectedWalletAddress,
wiw,
selectedWalletAssets,
} = this.props;
const {
selectSendAssetModalIsShowed,
selectReceiveAssetModalIsShowed,
selectedSendAssetCode,
selectedReceiveAssetCode,
trustedTokens,
tokensAreFetched,
sendAmount,
receiveAmount,
unlockButtonIsShowed,
unlockSpinnerIsShowed,
successTxHash,
isExchangeButtonSpinnerShowed,
blockchainStats,
initiallyLoadedTokenIconsCount,
isReconnectOverlayShown,
} = this.state;
t.String(sendAmount);
t.String(receiveAmount);
const tokensAreTheSame = selectedSendAssetCode === selectedReceiveAssetCode;
const sendTokensBalance = trustedTokens[selectedSendAssetCode].decimaledQuantity;
const notEnoughTokens = sendTokensBalance.lessThan(sendAmount || 0);
const formIsNotReady = tokensAreTheSame || [sendAmount, receiveAmount].some(s => s === '');
const sendInputError = (() => {
if (tokensAreTheSame) {
return 'Cannot exchange the same token';
}
if ((sendAmount !== '') && notEnoughTokens) {
return 'Insufficient balance in this account';
}
return null;
})();
const trustedTokensFiltered = Object.keys(trustedTokens)
.filter(code => code === 'eth' || trustedTokens[code].quantity > 0)
.reduce((acc, code) => (acc[code] = trustedTokens[code], acc), {});
const {
selectedWalletAddressChecksumed,
sendTokenSymbol,
receiveTokenSymbol,
send,
sendUsd,
receive,
receiveUsd,
exchangeFee,
exchangeFeeUsd,
transactionFee,
transactionFeeUsd,
total,
totalUsd,
} = this.getExchangeFormParameters();
const spinnerIsShown = selectedWalletAssets === undefined;
return (
<>
{selectSendAssetModalIsShowed &&
<SelectAssetModal
title="Pay with"
onClose={() => this.setState(() => ({selectSendAssetModalIsShowed: false}))}
assets={trustedTokensFiltered}
onSelect={code => this.handleTokenSelect('Send', code)}
selectedAssetCode={selectedSendAssetCode}
/>
}
{selectReceiveAssetModalIsShowed &&
<SelectAssetModal
title="Receive"
onClose={() => this.setState(() => ({selectReceiveAssetModalIsShowed: false}))}
assets={trustedTokens}
onSelect={code => this.handleTokenSelect('Receive', code)}
selectedAssetCode={selectedReceiveAssetCode}
/>
}
{successTxHash &&
<ModalSuccess
transactionHash={successTxHash}
onClose={() => this.setState(() => ({successTxHash: null}))}
/>
}
<Wr>
{
!spinnerIsShown && isReconnectOverlayShown && <ReconnectOverlay/>
}
{
spinnerIsShown
? (
<BounceSpinnerWrapper
isReconnectOverlayShown={isReconnectOverlayShown}
>
<BounceSpinnerBlock>
<BounceSpinner color="rgba(45, 54, 138, 1)"/>
</BounceSpinnerBlock>
</BounceSpinnerWrapper>
)
: (
<Container
isReconnectOverlayShown={!spinnerIsShown && isReconnectOverlayShown}
>
<Content>
<ContentContainer>
<Block>
<BlockTitle>
Pay with
{tokensAreFetched &&
<BalanceButton
onClick={() => this.handleSendInputChange(String(sendTokensBalance))}
>
Balance {String(sendTokensBalance)} {sendTokenSymbol}
</BalanceButton>
}
</BlockTitle>
<SelectInputRow>
<SelectButton
onClick={() => this.setState(() => ({selectSendAssetModalIsShowed: true}))}
disabled={unlockSpinnerIsShowed}
>
{trustedTokens[selectedSendAssetCode].icon_url
? (
<>
<AssetIcon
src={trustedTokens[selectedSendAssetCode].icon_url}
onLoad={this.handleTokenIconLoaded}
areIconsLoaded={initiallyLoadedTokenIconsCount === 2}
/>
<AssetIconPlaceholder
areIconsLoaded={initiallyLoadedTokenIconsCount === 2}
>
</AssetIconPlaceholder>
</>
)
: (
<AssetIconTexted>
{trustedTokens[selectedSendAssetCode].symbol.slice(0, 3)}
</AssetIconTexted>
)
}
<span>{sendTokenSymbol}</span>
</SelectButton>
<Input
placeholder="Enter amount"
value={sendAmount}
onChange={({target: {value}}) => this.handleSendInputChange(value)}
/>
{unlockButtonIsShowed && wiw > 520 &&
<UnlockButton
onClick={this.handleUnlockClick}
showSpinner={unlockSpinnerIsShowed}
disabled={unlockSpinnerIsShowed}
>
<span>{unlockSpinnerIsShowed ? 'Pending' : 'Unlock'}</span>
</UnlockButton>
}
{sendInputError &&
<InputError>
{sendInputError}
</InputError>
}
</SelectInputRow>
{
unlockButtonIsShowed && wiw <= 520 &&
<UnlockButton
onClick={this.handleUnlockClick}
showSpinner={unlockSpinnerIsShowed}
disabled={unlockSpinnerIsShowed}
className={sendInputError && 'lg-top-margin'}
>
<span>{unlockSpinnerIsShowed ? 'Pending' : 'Unlock'}</span>
</UnlockButton>
}
</Block>
<Block>
<BlockTitle>
Receive
{tokensAreFetched &&
<Balance>
Balance {String(trustedTokens[selectedReceiveAssetCode].decimaledQuantity)} {receiveTokenSymbol}
</Balance>
}
</BlockTitle>
<SelectInputRow>
<SelectButton
onClick={() => this.setState(() => ({selectReceiveAssetModalIsShowed: true}))}
>
{trustedTokens[selectedReceiveAssetCode].icon_url
? (
<>
<AssetIcon
src={trustedTokens[selectedReceiveAssetCode].icon_url}
onLoad={this.handleTokenIconLoaded}
areIconsLoaded={initiallyLoadedTokenIconsCount === 2}
/>
<AssetIconPlaceholder
areIconsLoaded={initiallyLoadedTokenIconsCount === 2}
>
</AssetIconPlaceholder>
</>
)
: (
<AssetIconTexted>
{trustedTokens[selectedReceiveAssetCode].symbol.slice(0, 3)}
</AssetIconTexted>
)
}
<span>{receiveTokenSymbol}</span>
</SelectButton>
<Input
placeholder="Enter amount"
value={receiveAmount}
onChange={({target: {value}}) => this.handleReceiveInputChange(value)}
/>
</SelectInputRow>
</Block>
<Block>
<BlockTitle>
Exchange details
</BlockTitle>
<ExchangeDetails>
{(() => {
return (
<>
{[
[
'Wallet',
<>
<WalletBlockie
src={
createIcon({
seed: selectedWalletAddress,
size: 8,
scale: 16,
}).toDataURL()
}
/>
{
wiw > 520
? selectedWalletAddressChecksumed
: `${selectedWalletAddressChecksumed.slice(0, 7)}...${selectedWalletAddressChecksumed.slice(- 5)}`
}
</>,
],
[
'Pay',
formIsNotReady
? <>&ndash;</>
: <><span className="gray">{Number(sendUsd) > 0 ? `($${formatToTwoNonZeroDecimals(sendUsd)})` : ''}</span> {String(send)} {sendTokenSymbol}</>,
],
[
'Receive',
formIsNotReady
? <>&ndash;</>
: <><span className="gray">{Number(receiveUsd) > 0 ? `($${formatToTwoNonZeroDecimals(receiveUsd)})` : ''}</span> {String(receive)} {receiveTokenSymbol}</>,
],
[
'Rate',
formIsNotReady
? <>&ndash;</>
: `1 ${sendTokenSymbol} = ${receiveAmount / sendAmount} ${receiveTokenSymbol}`,
],
[
'Protocol',
<>🦄 <UniswapLink to="https://uniswap.io">Uniswap</UniswapLink></>,
],
[
'Exchange fee',
formIsNotReady
? <>&ndash;</>
: <><span className="gray">{Number(exchangeFeeUsd) > 0 ? `($${formatToTwoNonZeroDecimals(exchangeFeeUsd)})` : ''}</span> {String(exchangeFee)} {sendTokenSymbol}</>,
],
[
'Transaction fee',
formIsNotReady
? <>&ndash;</>
: <><span className="gray">{blockchainStats ? `($${formatToTwoNonZeroDecimals(transactionFeeUsd)})` : ''}</span> {String(transactionFee)} ETH</>,
],
].map(([rowName, rowValue], idx) => (
<ExchangeDetailsRow key={idx}>
<ExchangeDetailsRowName>
{rowName}
</ExchangeDetailsRowName>
<ExchangeDetailsRowValue>
{rowValue}
</ExchangeDetailsRowValue>
</ExchangeDetailsRow>
))}
</>
);
})()}
<ExchangeDetailsTotalRow>
<ExchangeDetailsTotalRowName>
Total cost
</ExchangeDetailsTotalRowName>
<ExchangeDetailsTotalRowValue>
{formIsNotReady
? <>&ndash;</>
: <><span className="gray">{Number(totalUsd) > 0 ? `($${formatToTwoNonZeroDecimals(totalUsd)})` : ''}</span> {String(total)} {sendTokenSymbol}</>
}
</ExchangeDetailsTotalRowValue>
</ExchangeDetailsTotalRow>
</ExchangeDetails>
</Block>
<ExchangeButton
disabled={unlockButtonIsShowed || formIsNotReady || notEnoughTokens}
onClick={this.handleExchangeClick}
className={isExchangeButtonSpinnerShowed && 'exchange-button-spinner'}
>
<span>Exchange</span>
</ExchangeButton>
</ContentContainer>
</Content>
</Container>
)
}
</Wr>
</>
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment