nifty-wallet/old-ui/app/components/add-token/add-token.component.js

561 lines
16 KiB
JavaScript

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