const React = require('react') const { Component } = React const h = require('react-hyperscript') const Tooltip = require('../tooltip.js') const TabBar = require('../tab-bar') const { checkExistingAddresses } = require('./util') const { getCurrentKeyring, ifContractAcc } = require('../../util') const TokenList = require('./token-list') const TokenSearch = require('./token-search') const { tokenInfoGetter } = require('../../../../ui/app/token-util') const ethUtil = require('ethereumjs-util') const abi = require('human-standard-token-abi') const Eth = require('ethjs-query') const EthContract = require('ethjs-contract') const PropTypes = require('prop-types') const emptyAddr = '0x0000000000000000000000000000000000000000' const SEARCH_TAB = 'SEARCH' const CUSTOM_TOKEN_TAB = 'CUSTOM_TOKEN' const { POA_CODE, MAINNET_CODE } = require('../../../../app/scripts/controllers/network/enums') class AddTokenScreen extends Component { static contextTypes = { t: PropTypes.func, } static propTypes = { goHome: PropTypes.func, setPendingTokens: PropTypes.func, pendingTokens: PropTypes.object, clearPendingTokens: PropTypes.func, showConfirmAddTokensPage: PropTypes.func, displayWarning: PropTypes.func, tokens: PropTypes.array, identities: PropTypes.object, keyrings: PropTypes.array, address: PropTypes.string, dispatch: PropTypes.func, network: PropTypes.string, } constructor (props) { super(props) this.state = { warning: null, customAddress: '', customSymbol: '', customDecimals: '', searchResults: [], selectedTokens: {}, tokenSelectorError: null, customAddressError: true, customSymbolError: true, customDecimalsError: true, autoFilled: false, displayedTab: SEARCH_TAB, } Component.call(this) } componentDidMount () { this.tokenInfoGetter = tokenInfoGetter() const { pendingTokens = {} } = this.props const pendingTokenKeys = Object.keys(pendingTokens) if (pendingTokenKeys.length > 0) { let selectedTokens = {} let customToken = {} pendingTokenKeys.forEach(tokenAddress => { const token = pendingTokens[tokenAddress] const { isCustom } = token if (isCustom) { customToken = { ...token } } else { selectedTokens = { ...selectedTokens, [tokenAddress]: { ...token } } } }) const { address: customAddress = '', symbol: customSymbol = '', decimals: customDecimals = '', } = customToken const displayedTab = Object.keys(selectedTokens).length > 0 ? SEARCH_TAB : CUSTOM_TOKEN_TAB this.setState({ selectedTokens, customAddress, customSymbol, customDecimals, displayedTab }) } } render () { const { network } = this.props const networkID = parseInt(network) let views = [] const isProdNetworkWithKnownTokens = networkID === MAINNET_CODE || networkID === POA_CODE isProdNetworkWithKnownTokens ? views = [h(TabBar, { style: { paddingTop: '0px', }, tabs: [ { content: 'Search', key: SEARCH_TAB }, { content: 'Custom', key: CUSTOM_TOKEN_TAB }, ], defaultTab: this.state.displayedTab || CUSTOM_TOKEN_TAB, tabSelected: (key) => this.setCurrentAddTokenTab(key), }), this.tabSwitchView()] : views = [this.renderAddToken()] return ( h('.flex-column.flex-grow', { style: { width: '100%', }, }, [ // subtitle and nav h('.section-title.flex-row.flex-center', { style: { background: '#60269c', borderTop: 'none', }, }, [ h('h2.page-subtitle', { style: { color: '#ffffff', }, }, 'Add Token'), ]), ...views, ]) ) } setCurrentAddTokenTab (key) { this.setState({displayedTab: key}) } tabSwitchView () { const state = this.state const { displayedTab } = state switch (displayedTab) { case CUSTOM_TOKEN_TAB: return this.renderAddToken() default: return this.renderTabBar() } } renderAddToken () { const props = this.props const state = this.state const { warning, customAddress, customSymbol, customDecimals, autoFilled } = state const { network, goHome, addToken } = props return h('.flex-column.flex-justify-center.flex-grow.select-none', [ warning ? h('div', { style: { margin: '20px 30px 0 30px', }, }, [ h('.error', { style: { display: 'block', }, }, warning), ]) : null, h('.flex-space-around', { style: { padding: '30px', }, }, [ h('div', [ h(Tooltip, { position: 'top', title: 'The contract of the actual token contract.', }, [ h('span', { style: { fontWeight: 'bold'}, }, 'Token Address' /* this.context.t('tokenAddress')*/), ]), ]), h('section.flex-row.flex-center', [ h('input.large-input#token-address', { name: 'address', placeholder: 'Token Contract Address', value: customAddress, style: { width: '100%', margin: '10px 0', }, onChange: e => this.handleCustomAddressChange(e.target.value), }), ]), h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, }, 'Token Symbol' /* this.context.t('tokenSymbol')*/), ]), h('div', { style: {display: 'flex'} }, [ h('input.large-input#token_symbol', { disabled: !autoFilled, placeholder: `Like "ETH"`, value: customSymbol, style: { width: '100%', margin: '10px 0', }, onChange: e => this.handleCustomSymbolChange(e.target.value), }), ]), h('div', [ h('span', { style: { fontWeight: 'bold', paddingRight: '10px'}, }, 'Decimals of Precision' /* this.context.t('decimal')*/), ]), h('div', { style: {display: 'flex'} }, [ h('input.large-input#token_decimals', { disabled: true, value: customDecimals, type: 'number', min: 0, max: 36, style: { width: '100%', margin: '10px 0', }, onChange: e => this.handleCustomDecimalsChange(e.target.value), }), ]), h('div', { key: 'buttons', style: { alignSelf: 'center', float: 'right', marginTop: '10px', }, }, [ h('button.btn-violet', { onClick: () => { goHome() }, }, 'Cancel' /* this.context.t('cancel')*/), h('button', { disabled: this.hasError() || !this.hasSelected(), onClick: (event) => { const valid = this.validateInputs() if (!valid) return const { customAddress, customSymbol, customDecimals } = this.state addToken(customAddress.trim(), customSymbol.trim(), customDecimals, network) .then(() => { goHome() }) }, }, 'Add' /* this.context.t('addToken')*/), ]), ]), ]) } renderTabBar () { const { tokenSelectorError, selectedTokens, searchResults } = this.state const { clearPendingTokens, goHome, network } = this.props return h('div', [ h('.add-token__search-token', [ h(TokenSearch, { onSearch: ({ results = [] }) => this.setState({ searchResults: results }), error: tokenSelectorError, network: network, }), h('.add-token__token-list', { style: { marginTop: '20px', height: '250px', overflow: 'auto', }, }, [ h(TokenList, { results: searchResults, selectedTokens: selectedTokens, network: network, onToggleToken: token => this.handleToggleToken(token), }), ]), ]), h('.page-container__footer', [ h('.page-container__footer-container', [ h('button.btn-violet', { onClick: () => { clearPendingTokens() goHome() }, }, 'Cancel' /* this.context.t('cancel')*/), h('button.btn-primary', { onClick: () => this.handleNext(), disabled: !this.hasSelected(), }, 'Next' /* this.context.t('next')*/), ]), ]), ]) } componentWillMount () { if (typeof global.ethereumProvider === 'undefined') return this.eth = new Eth(global.ethereumProvider) this.contract = new EthContract(this.eth) this.TokenContract = this.contract(abi) } componentWillUnmount () { const { displayWarning } = this.props displayWarning('') } componentWillUpdate (nextProps) { const { network: oldNet, } = this.props const { network: newNet, } = nextProps if (oldNet !== newNet) { this.tokenInfoGetter = tokenInfoGetter() this.setState({ selectedTokens: {}, searchResults: [], customAddress: '', customSymbol: '', customDecimals: '', }) } } validateInputs () { let msg = '' const { network, keyrings, identities } = this.props const state = this.state const identitiesList = Object.keys(this.props.identities) const { customAddress: address, customSymbol: symbol, customDecimals: decimals } = state const standardAddress = ethUtil.addHexPrefix(address).toLowerCase() const validAddress = ethUtil.isValidAddress(address) if (!validAddress) { msg += 'Address is invalid.' } const validDecimals = decimals >= 0 && decimals < 36 if (!validDecimals) { msg += 'Decimals must be at least 0, and not over 36. ' } const symbolLen = symbol.trim().length const validSymbol = symbolLen > 0 && symbolLen < 10 if (!validSymbol) { msg += 'Symbol must be between 0 and 10 characters.' } let ownAddress = identitiesList.includes(standardAddress) if (ownAddress) { const keyring = getCurrentKeyring(standardAddress, network, keyrings, identities) if (!ifContractAcc(keyring)) { msg = 'Personal address detected. Input the token contract address.' } else { ownAddress = false } } const isValid = validAddress && validDecimals && validSymbol && !ownAddress if (!isValid) { this.setState({ warning: msg, }) } else { this.setState({ warning: null }) } return isValid } handleToggleToken = (token) => { const { address } = token const { selectedTokens = {} } = this.state const selectedTokensCopy = { ...selectedTokens } if (address in selectedTokensCopy) { delete selectedTokensCopy[address] } else { selectedTokensCopy[address] = token } this.setState({ selectedTokens: selectedTokensCopy, tokenSelectorError: null, }) } hasError = () => { const { tokenSelectorError, customAddressError, customSymbolError, customDecimalsError, } = this.state return tokenSelectorError || customAddressError || customSymbolError || customDecimalsError } hasSelected = () => { const { customAddress = '', customDecimals = '', customSymbol = '', selectedTokens = {} } = this.state const validDecimals = this.isValidDecimals(customDecimals) return (customAddress && validDecimals && customSymbol) || Object.keys(selectedTokens).length > 0 } handleNext = () => { if (!this.hasSelected()) { this.setState({ tokenSelectorError: 'Must select at least 1 token.' /* this.context.t('mustSelectOne')*/ }) return } const { setPendingTokens, network, showConfirmAddTokensPage } = this.props const { customAddress: address, customSymbol: symbol, customDecimals: decimals, selectedTokens, } = this.state const customToken = { address, symbol, decimals, network: network, } setPendingTokens({ customToken, selectedTokens }) showConfirmAddTokensPage() } attemptToAutoFillTokenParams = async (address) => { const { symbol = '', decimals = '' } = await this.tokenInfoGetter(address) const autoFilled = Boolean(symbol && decimals) this.setState({ autoFilled, warning: '', customAddressError: null, }) this.handleCustomSymbolChange(symbol || '') this.handleCustomDecimalsChange(decimals || '') } handleCustomAddressChange = (value) => { const { identities, keyrings, tokens, network } = this.props const customAddress = value.trim() this.setState({ customAddress, customAddressError: null, tokenSelectorError: null, autoFilled: false, }) const isValidAddress = ethUtil.isValidAddress(customAddress) const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase() let warning switch (true) { case !isValidAddress: warning = 'Invalid address' this.setState({ warning, customAddressError: warning /* this.context.t('invalidAddress')*/, customSymbol: '', customDecimals: null, customSymbolError: null, customDecimalsError: null, }) break case Boolean(identities[standardAddress]): const keyring = getCurrentKeyring(standardAddress, network, keyrings, identities) if (!ifContractAcc(keyring)) { warning = 'Personal address detected. Input the token contract address.' /* this.context.t('personalAddressDetected')*/ this.setState({ warning, customAddressError: warning /* this.context.t('personalAddressDetected')*/, }) } else { this.attemptToAutoFillTokenParams(customAddress) } break case checkExistingAddresses(customAddress, tokens): warning = 'Token has already been added.' this.setState({ warning, customAddressError: warning /* this.context.t('tokenAlreadyAdded')*/, }) break default: if (customAddress !== emptyAddr) { this.attemptToAutoFillTokenParams(customAddress) } } } handleCustomSymbolChange = (value) => { const customSymbol = value.trim() const symbolLength = customSymbol.length let customSymbolError = null if (symbolLength <= 0 || symbolLength >= 10) { customSymbolError = 'Symbol must be between 0 and 10 characters.' /* this.context.t('symbolBetweenZeroTen')*/ } this.setState({ customSymbol, customSymbolError }) } handleCustomDecimalsChange = (value) => { let customDecimals = Number(value && value.toString().trim()) customDecimals = isNaN(customDecimals) ? '' : customDecimals const validDecimals = this.isValidDecimals(customDecimals) let customDecimalsError = null if (!validDecimals) { customDecimalsError = 'Decimals must be at least 0, and not over 36.' /* this.context.t('decimalsMustZerotoTen')*/ } this.setState({ customDecimals, customDecimalsError }) } /** * Returns validity status of token decimals * * @param {number} customDecimals A token decimals number to validate * @returns {boolean} The status of validatity of token decimals * */ isValidDecimals = (customDecimals) => { const validDecimals = customDecimals !== null && customDecimals !== '' && customDecimals >= 0 && customDecimals < 36 return validDecimals } } module.exports = AddTokenScreen