|
|
|
const inherits = require('util').inherits
|
|
|
|
const Component = require('react').Component
|
|
|
|
const classnames = require('classnames')
|
|
|
|
const h = require('react-hyperscript')
|
|
|
|
const PropTypes = require('prop-types')
|
|
|
|
const connect = require('react-redux').connect
|
|
|
|
const R = require('ramda')
|
|
|
|
const Fuse = require('fuse.js')
|
|
|
|
const contractMap = require('eth-contract-metadata')
|
|
|
|
const TokenBalance = require('./components/token-balance')
|
|
|
|
const Identicon = require('./components/identicon')
|
|
|
|
const contractList = Object.entries(contractMap)
|
|
|
|
.map(([ _, tokenData]) => tokenData)
|
|
|
|
.filter(tokenData => Boolean(tokenData.erc20))
|
|
|
|
const fuse = new Fuse(contractList, {
|
|
|
|
shouldSort: true,
|
|
|
|
threshold: 0.45,
|
|
|
|
location: 0,
|
|
|
|
distance: 100,
|
|
|
|
maxPatternLength: 32,
|
|
|
|
minMatchCharLength: 1,
|
|
|
|
keys: [
|
|
|
|
{ name: 'name', weight: 0.5 },
|
|
|
|
{ name: 'symbol', weight: 0.5 },
|
|
|
|
],
|
|
|
|
})
|
|
|
|
const actions = require('./actions')
|
|
|
|
const ethUtil = require('ethereumjs-util')
|
|
|
|
const { tokenInfoGetter } = require('./token-util')
|
|
|
|
|
|
|
|
const emptyAddr = '0x0000000000000000000000000000000000000000'
|
|
|
|
|
|
|
|
AddTokenScreen.contextTypes = {
|
|
|
|
t: PropTypes.func,
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)
|
|
|
|
|
|
|
|
|
|
|
|
function mapStateToProps (state) {
|
|
|
|
const { identities, tokens } = state.metamask
|
|
|
|
return {
|
|
|
|
identities,
|
|
|
|
tokens,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function mapDispatchToProps (dispatch) {
|
|
|
|
return {
|
|
|
|
goHome: () => dispatch(actions.goHome()),
|
|
|
|
addTokens: tokens => dispatch(actions.addTokens(tokens)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inherits(AddTokenScreen, Component)
|
|
|
|
function AddTokenScreen () {
|
|
|
|
this.state = {
|
|
|
|
isShowingConfirmation: false,
|
|
|
|
isShowingInfoBox: true,
|
|
|
|
customAddress: '',
|
|
|
|
customSymbol: '',
|
|
|
|
customDecimals: '',
|
|
|
|
searchQuery: '',
|
|
|
|
selectedTokens: {},
|
|
|
|
errors: {},
|
|
|
|
autoFilled: false,
|
|
|
|
displayedTab: 'SEARCH',
|
|
|
|
}
|
|
|
|
this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
|
|
|
|
this.tokenSymbolDidChange = this.tokenSymbolDidChange.bind(this)
|
|
|
|
this.tokenDecimalsDidChange = this.tokenDecimalsDidChange.bind(this)
|
|
|
|
this.onNext = this.onNext.bind(this)
|
|
|
|
Component.call(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.componentWillMount = function () {
|
|
|
|
this.tokenInfoGetter = tokenInfoGetter()
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.toggleToken = function (address, token) {
|
|
|
|
const { selectedTokens = {}, errors } = this.state
|
|
|
|
const selectedTokensCopy = { ...selectedTokens }
|
|
|
|
|
|
|
|
if (address in selectedTokensCopy) {
|
|
|
|
delete selectedTokensCopy[address]
|
|
|
|
} else {
|
|
|
|
selectedTokensCopy[address] = token
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
selectedTokens: selectedTokensCopy,
|
|
|
|
errors: {
|
|
|
|
...errors,
|
|
|
|
tokenSelector: null,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.onNext = function () {
|
|
|
|
const { isValid, errors } = this.validate()
|
|
|
|
|
|
|
|
return !isValid
|
|
|
|
? this.setState({ errors })
|
|
|
|
: this.setState({ isShowingConfirmation: true })
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.tokenAddressDidChange = function (e) {
|
|
|
|
const customAddress = e.target.value.trim()
|
|
|
|
this.setState({ customAddress })
|
|
|
|
if (ethUtil.isValidAddress(customAddress) && customAddress !== emptyAddr) {
|
|
|
|
this.attemptToAutoFillTokenParams(customAddress)
|
|
|
|
} else {
|
|
|
|
this.setState({
|
|
|
|
customSymbol: '',
|
|
|
|
customDecimals: 0,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.tokenSymbolDidChange = function (e) {
|
|
|
|
const customSymbol = e.target.value.trim()
|
|
|
|
this.setState({ customSymbol })
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.tokenDecimalsDidChange = function (e) {
|
|
|
|
const customDecimals = e.target.value.trim()
|
|
|
|
this.setState({ customDecimals })
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.checkExistingAddresses = function (address) {
|
|
|
|
if (!address) return false
|
|
|
|
const tokensList = this.props.tokens
|
|
|
|
const matchesAddress = existingToken => {
|
|
|
|
return existingToken.address.toLowerCase() === address.toLowerCase()
|
|
|
|
}
|
|
|
|
|
|
|
|
return R.any(matchesAddress)(tokensList)
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.validate = function () {
|
|
|
|
const errors = {}
|
|
|
|
const identitiesList = Object.keys(this.props.identities)
|
|
|
|
const { customAddress, customSymbol, customDecimals, selectedTokens } = this.state
|
|
|
|
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
|
|
|
|
|
|
|
|
if (customAddress) {
|
|
|
|
const validAddress = ethUtil.isValidAddress(customAddress)
|
|
|
|
if (!validAddress) {
|
|
|
|
errors.customAddress = this.context.t('invalidAddress')
|
|
|
|
}
|
|
|
|
|
|
|
|
const validDecimals = customDecimals !== null
|
|
|
|
&& customDecimals !== ''
|
|
|
|
&& customDecimals >= 0
|
|
|
|
&& customDecimals < 36
|
|
|
|
if (!validDecimals) {
|
|
|
|
errors.customDecimals = this.context.t('decimalsMustZerotoTen')
|
|
|
|
}
|
|
|
|
|
|
|
|
const symbolLen = customSymbol.trim().length
|
|
|
|
const validSymbol = symbolLen > 0 && symbolLen < 10
|
|
|
|
if (!validSymbol) {
|
|
|
|
errors.customSymbol = this.context.t('symbolBetweenZeroTen')
|
|
|
|
}
|
|
|
|
|
|
|
|
const ownAddress = identitiesList.includes(standardAddress)
|
|
|
|
if (ownAddress) {
|
|
|
|
errors.customAddress = this.context.t('personalAddressDetected')
|
|
|
|
}
|
|
|
|
|
|
|
|
const tokenAlreadyAdded = this.checkExistingAddresses(customAddress)
|
|
|
|
if (tokenAlreadyAdded) {
|
|
|
|
errors.customAddress = this.context.t('tokenAlreadyAdded')
|
|
|
|
}
|
|
|
|
} else if (
|
|
|
|
Object.entries(selectedTokens)
|
|
|
|
.reduce((isEmpty, [ symbol, isSelected ]) => (
|
|
|
|
isEmpty && !isSelected
|
|
|
|
), true)
|
|
|
|
) {
|
|
|
|
errors.tokenSelector = this.context.t('mustSelectOne')
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
isValid: !Object.keys(errors).length,
|
|
|
|
errors,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.attemptToAutoFillTokenParams = async function (address) {
|
|
|
|
const { symbol, decimals } = await this.tokenInfoGetter(address)
|
|
|
|
if (symbol && decimals) {
|
|
|
|
this.setState({
|
|
|
|
customSymbol: symbol,
|
|
|
|
customDecimals: decimals.toString(),
|
|
|
|
autoFilled: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.renderCustomForm = function () {
|
|
|
|
const { autoFilled, customAddress, customSymbol, customDecimals, errors } = this.state
|
|
|
|
|
|
|
|
return (
|
|
|
|
h('div.add-token__add-custom-form', [
|
|
|
|
h('div', {
|
|
|
|
className: classnames('add-token__add-custom-field', {
|
|
|
|
'add-token__add-custom-field--error': errors.customAddress,
|
|
|
|
}),
|
|
|
|
}, [
|
|
|
|
h('div.add-token__add-custom-label', this.context.t('tokenAddress')),
|
|
|
|
h('input.add-token__add-custom-input', {
|
|
|
|
type: 'text',
|
|
|
|
onChange: this.tokenAddressDidChange,
|
|
|
|
value: customAddress,
|
|
|
|
}),
|
|
|
|
h('div.add-token__add-custom-error-message', errors.customAddress),
|
|
|
|
]),
|
|
|
|
h('div', {
|
|
|
|
className: classnames('add-token__add-custom-field', {
|
|
|
|
'add-token__add-custom-field--error': errors.customSymbol,
|
|
|
|
}),
|
|
|
|
}, [
|
|
|
|
h('div.add-token__add-custom-label', this.context.t('tokenSymbol')),
|
|
|
|
h('input.add-token__add-custom-input', {
|
|
|
|
type: 'text',
|
|
|
|
onChange: this.tokenSymbolDidChange,
|
|
|
|
value: customSymbol,
|
|
|
|
disabled: autoFilled,
|
|
|
|
}),
|
|
|
|
h('div.add-token__add-custom-error-message', errors.customSymbol),
|
|
|
|
]),
|
|
|
|
h('div', {
|
|
|
|
className: classnames('add-token__add-custom-field', {
|
|
|
|
'add-token__add-custom-field--error': errors.customDecimals,
|
|
|
|
}),
|
|
|
|
}, [
|
|
|
|
h('div.add-token__add-custom-label', this.context.t('decimal')),
|
|
|
|
h('input.add-token__add-custom-input', {
|
|
|
|
type: 'number',
|
|
|
|
onChange: this.tokenDecimalsDidChange,
|
|
|
|
value: customDecimals,
|
|
|
|
disabled: autoFilled,
|
|
|
|
}),
|
|
|
|
h('div.add-token__add-custom-error-message', errors.customDecimals),
|
|
|
|
]),
|
|
|
|
])
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.renderTokenList = function () {
|
|
|
|
const { searchQuery = '', selectedTokens } = this.state
|
|
|
|
const fuseSearchResult = fuse.search(searchQuery)
|
|
|
|
const addressSearchResult = contractList.filter(token => {
|
|
|
|
return token.address.toLowerCase() === searchQuery.toLowerCase()
|
|
|
|
})
|
|
|
|
const results = [...addressSearchResult, ...fuseSearchResult]
|
|
|
|
|
|
|
|
return h('div', [
|
|
|
|
results.length > 0 && h('div.add-token__token-icons-title', this.context.t('popularTokens')),
|
|
|
|
h('div.add-token__token-icons-container', Array(6).fill(undefined)
|
|
|
|
.map((_, i) => {
|
|
|
|
const { logo, symbol, name, address } = results[i] || {}
|
|
|
|
const tokenAlreadyAdded = this.checkExistingAddresses(address)
|
|
|
|
return Boolean(logo || symbol || name) && (
|
|
|
|
h('div.add-token__token-wrapper', {
|
|
|
|
className: classnames({
|
|
|
|
'add-token__token-wrapper--selected': selectedTokens[address],
|
|
|
|
'add-token__token-wrapper--disabled': tokenAlreadyAdded,
|
|
|
|
}),
|
|
|
|
onClick: () => !tokenAlreadyAdded && this.toggleToken(address, results[i]),
|
|
|
|
}, [
|
|
|
|
h('div.add-token__token-icon', {
|
|
|
|
style: {
|
|
|
|
backgroundImage: logo && `url(images/contract/${logo})`,
|
|
|
|
},
|
|
|
|
}),
|
|
|
|
h('div.add-token__token-data', [
|
|
|
|
h('div.add-token__token-symbol', symbol),
|
|
|
|
h('div.add-token__token-name', name),
|
|
|
|
]),
|
|
|
|
// tokenAlreadyAdded && (
|
|
|
|
// h('div.add-token__token-message', 'Already added')
|
|
|
|
// ),
|
|
|
|
])
|
|
|
|
)
|
|
|
|
})),
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.renderConfirmation = function () {
|
|
|
|
const {
|
|
|
|
customAddress: address,
|
|
|
|
customSymbol: symbol,
|
|
|
|
customDecimals: decimals,
|
|
|
|
selectedTokens,
|
|
|
|
} = this.state
|
|
|
|
|
|
|
|
const { addTokens, goHome } = this.props
|
|
|
|
|
|
|
|
const customToken = {
|
|
|
|
address,
|
|
|
|
symbol,
|
|
|
|
decimals,
|
|
|
|
}
|
|
|
|
|
|
|
|
const tokens = address && symbol && decimals
|
|
|
|
? { ...selectedTokens, [address]: customToken }
|
|
|
|
: selectedTokens
|
|
|
|
|
|
|
|
return (
|
|
|
|
h('div.add-token', [
|
|
|
|
h('div.add-token__wrapper', [
|
|
|
|
h('div.add-token__content-container.add-token__confirmation-content', [
|
|
|
|
h('div.add-token__description.add-token__confirmation-description', this.context.t('balances')),
|
|
|
|
h('div.add-token__confirmation-token-list',
|
|
|
|
Object.entries(tokens)
|
|
|
|
.map(([ address, token ]) => (
|
|
|
|
h('span.add-token__confirmation-token-list-item', [
|
|
|
|
h(Identicon, {
|
|
|
|
className: 'add-token__confirmation-token-icon',
|
|
|
|
diameter: 75,
|
|
|
|
address,
|
|
|
|
}),
|
|
|
|
h(TokenBalance, { token }),
|
|
|
|
])
|
|
|
|
))
|
|
|
|
),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
h('div.add-token__buttons', [
|
|
|
|
h('button.btn-secondary--lg.add-token__cancel-button', {
|
|
|
|
onClick: () => this.setState({ isShowingConfirmation: false }),
|
|
|
|
}, this.context.t('back')),
|
|
|
|
h('button.btn-primary--lg', {
|
|
|
|
onClick: () => addTokens(tokens).then(goHome),
|
|
|
|
}, this.context.t('addTokens')),
|
|
|
|
]),
|
|
|
|
])
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.displayTab = function (selectedTab) {
|
|
|
|
this.setState({ displayedTab: selectedTab })
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.renderTabs = function () {
|
|
|
|
const { isShowingInfoBox, displayedTab, errors } = this.state
|
|
|
|
|
|
|
|
return displayedTab === 'CUSTOM_TOKEN'
|
|
|
|
? this.renderCustomForm()
|
|
|
|
: h('div', [
|
|
|
|
h('div.add-token__wrapper', [
|
|
|
|
h('div.add-token__content-container', [
|
|
|
|
isShowingInfoBox && h('div.add-token__info-box', [
|
|
|
|
h('div.add-token__info-box__close', {
|
|
|
|
onClick: () => this.setState({ isShowingInfoBox: false }),
|
|
|
|
}),
|
|
|
|
h('div.add-token__info-box__title', this.context.t('whatsThis')),
|
|
|
|
h('div.add-token__info-box__copy', this.context.t('keepTrackTokens')),
|
|
|
|
h('a.add-token__info-box__copy--blue', {
|
|
|
|
href: 'http://metamask.helpscoutdocs.com/article/16-managing-erc20-tokens',
|
|
|
|
}, this.context.t('learnMore')),
|
|
|
|
]),
|
|
|
|
h('div.add-token__input-container', [
|
|
|
|
h('input.add-token__input', {
|
|
|
|
type: 'text',
|
|
|
|
placeholder: this.context.t('searchTokens'),
|
|
|
|
onChange: e => this.setState({ searchQuery: e.target.value }),
|
|
|
|
}),
|
|
|
|
h('div.add-token__search-input-error-message', errors.tokenSelector),
|
|
|
|
]),
|
|
|
|
this.renderTokenList(),
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
AddTokenScreen.prototype.render = function () {
|
|
|
|
const {
|
|
|
|
isShowingConfirmation,
|
|
|
|
displayedTab,
|
|
|
|
} = this.state
|
|
|
|
const { goHome } = this.props
|
|
|
|
|
|
|
|
return h('div.add-token', [
|
|
|
|
h('div.add-token__header', [
|
|
|
|
h('div.add-token__header__cancel', {
|
|
|
|
onClick: () => goHome(),
|
|
|
|
}, [
|
|
|
|
h('i.fa.fa-angle-left.fa-lg'),
|
|
|
|
h('span', this.context.t('cancel')),
|
|
|
|
]),
|
|
|
|
h('div.add-token__header__title', this.context.t('addTokens')),
|
|
|
|
isShowingConfirmation && h('div.add-token__header__subtitle', this.context.t('likeToAddTokens')),
|
|
|
|
!isShowingConfirmation && h('div.add-token__header__tabs', [
|
|
|
|
|
|
|
|
h('div.add-token__header__tabs__tab', {
|
|
|
|
className: classnames('add-token__header__tabs__tab', {
|
|
|
|
'add-token__header__tabs__selected': displayedTab === 'SEARCH',
|
|
|
|
'add-token__header__tabs__unselected': displayedTab !== 'SEARCH',
|
|
|
|
}),
|
|
|
|
onClick: () => this.displayTab('SEARCH'),
|
|
|
|
}, this.context.t('search')),
|
|
|
|
|
|
|
|
h('div.add-token__header__tabs__tab', {
|
|
|
|
className: classnames('add-token__header__tabs__tab', {
|
|
|
|
'add-token__header__tabs__selected': displayedTab === 'CUSTOM_TOKEN',
|
|
|
|
'add-token__header__tabs__unselected': displayedTab !== 'CUSTOM_TOKEN',
|
|
|
|
}),
|
|
|
|
onClick: () => this.displayTab('CUSTOM_TOKEN'),
|
|
|
|
}, this.context.t('customToken')),
|
|
|
|
|
|
|
|
]),
|
|
|
|
]),
|
|
|
|
//
|
|
|
|
isShowingConfirmation
|
|
|
|
? this.renderConfirmation()
|
|
|
|
: this.renderTabs(),
|
|
|
|
|
|
|
|
!isShowingConfirmation && h('div.add-token__buttons', [
|
|
|
|
h('button.btn-secondary--lg.add-token__cancel-button', {
|
|
|
|
onClick: goHome,
|
|
|
|
}, this.context.t('cancel')),
|
|
|
|
h('button.btn-primary--lg.add-token__confirm-button', {
|
|
|
|
onClick: this.onNext,
|
|
|
|
}, this.context.t('next')),
|
|
|
|
]),
|
|
|
|
])
|
|
|
|
}
|