Created
February 5, 2019 14:54
-
-
Save mqklin/73bc7306e5ff3a30f599c463fc1537f7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
? <>–</> | |
: <><span className="gray">{Number(sendUsd) > 0 ? `($${formatToTwoNonZeroDecimals(sendUsd)})` : ''}</span> {String(send)} {sendTokenSymbol}</>, | |
], | |
[ | |
'Receive', | |
formIsNotReady | |
? <>–</> | |
: <><span className="gray">{Number(receiveUsd) > 0 ? `($${formatToTwoNonZeroDecimals(receiveUsd)})` : ''}</span> {String(receive)} {receiveTokenSymbol}</>, | |
], | |
[ | |
'Rate', | |
formIsNotReady | |
? <>–</> | |
: `1 ${sendTokenSymbol} = ${receiveAmount / sendAmount} ${receiveTokenSymbol}`, | |
], | |
[ | |
'Protocol', | |
<>🦄 <UniswapLink to="https://uniswap.io">Uniswap</UniswapLink></>, | |
], | |
[ | |
'Exchange fee', | |
formIsNotReady | |
? <>–</> | |
: <><span className="gray">{Number(exchangeFeeUsd) > 0 ? `($${formatToTwoNonZeroDecimals(exchangeFeeUsd)})` : ''}</span> {String(exchangeFee)} {sendTokenSymbol}</>, | |
], | |
[ | |
'Transaction fee', | |
formIsNotReady | |
? <>–</> | |
: <><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 | |
? <>–</> | |
: <><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