const inherits = require ( 'util' ) . inherits
const Component = require ( 'react' ) . Component
const classnames = require ( 'classnames' )
const h = require ( 'react-hyperscript' )
const connect = require ( 'react-redux' ) . connect
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 : [ 'address' , 'name' , 'symbol' ] ,
} )
const actions = require ( './actions' )
const ethUtil = require ( 'ethereumjs-util' )
const abi = require ( 'human-standard-token-abi' )
const Eth = require ( 'ethjs-query' )
const EthContract = require ( 'ethjs-contract' )
const R = require ( 'ramda' )
const emptyAddr = '0x0000000000000000000000000000000000000000'
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 ,
customAddress : '' ,
customSymbol : '' ,
customDecimals : 0 ,
searchQuery : '' ,
isCollapsed : true ,
selectedTokens : { } ,
errors : { } ,
}
this . tokenAddressDidChange = this . tokenAddressDidChange . bind ( this )
this . onNext = this . onNext . bind ( this )
Component . call ( this )
}
AddTokenScreen . prototype . componentWillMount = function ( ) {
if ( typeof global . ethereumProvider === 'undefined' ) return
this . eth = new Eth ( global . ethereumProvider )
this . contract = new EthContract ( this . eth )
this . TokenContract = this . contract ( abi )
}
AddTokenScreen . prototype . toggleToken = function ( address , token ) {
const { selectedTokens , errors } = this . state
const { [ address ] : selectedToken } = selectedTokens
this . setState ( {
selectedTokens : {
... selectedTokens ,
[ address ] : selectedToken ? null : token ,
} ,
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 . 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 = 'Address is invalid. '
}
const validDecimals = customDecimals >= 0 && customDecimals < 36
if ( ! validDecimals ) {
errors . customDecimals = 'Decimals must be at least 0, and not over 36.'
}
const symbolLen = customSymbol . trim ( ) . length
const validSymbol = symbolLen > 0 && symbolLen < 10
if ( ! validSymbol ) {
errors . customSymbol = 'Symbol must be between 0 and 10 characters.'
}
const ownAddress = identitiesList . includes ( standardAddress )
if ( ownAddress ) {
errors . customAddress = 'Personal address detected. Input the token contract address.'
}
const tokenAlreadyAdded = this . checkExistingAddresses ( customAddress )
if ( tokenAlreadyAdded ) {
errors . customAddress = 'Token has already been added.'
}
} else if (
Object . entries ( selectedTokens )
. reduce ( ( isEmpty , [ symbol , isSelected ] ) => (
isEmpty && ! isSelected
) , true )
) {
errors . tokenSelector = 'Must select at least 1 token.'
}
return {
isValid : ! Object . keys ( errors ) . length ,
errors ,
}
}
AddTokenScreen . prototype . attemptToAutoFillTokenParams = async function ( address ) {
const contract = this . TokenContract . at ( address )
const results = await Promise . all ( [
contract . symbol ( ) ,
contract . decimals ( ) ,
] )
const [ symbol , decimals ] = results
if ( symbol && decimals ) {
this . setState ( {
customSymbol : symbol [ 0 ] ,
customDecimals : decimals [ 0 ] . toString ( ) ,
} )
}
}
AddTokenScreen . prototype . renderCustomForm = function ( ) {
const { customAddress , customSymbol , customDecimals , errors } = this . state
return ! this . state . isCollapsed && (
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' , 'Token Address' ) ,
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' , 'Token Symbol' ) ,
h ( 'input.add-token__add-custom-input' , {
type : 'text' ,
value : customSymbol ,
disabled : true ,
} ) ,
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' , 'Decimals of Precision' ) ,
h ( 'input.add-token__add-custom-input' , {
type : 'number' ,
value : customDecimals ,
disabled : true ,
} ) ,
h ( 'div.add-token__add-custom-error-message' , errors . customDecimals ) ,
] ) ,
] )
)
}
AddTokenScreen . prototype . renderTokenList = function ( ) {
const { searchQuery = '' , selectedTokens } = this . state
const results = searchQuery
? fuse . search ( searchQuery ) || [ ]
: contractList
return 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 : ` 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__title-container.add-token__confirmation-title' , [
h ( 'div.add-token__title' , 'Add Token' ) ,
h ( 'div.add-token__description' , 'Would you like to add these tokens?' ) ,
] ) ,
h ( 'div.add-token__content-container.add-token__confirmation-content' , [
h ( 'div.add-token__description.add-token__confirmation-description' , 'Your 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' , {
onClick : ( ) => addTokens ( tokens ) . then ( goHome ) ,
} , 'Add Tokens' ) ,
h ( 'button.btn-tertiary' , {
onClick : ( ) => this . setState ( { isShowingConfirmation : false } ) ,
} , 'Back' ) ,
] ) ,
] )
)
}
AddTokenScreen . prototype . render = function ( ) {
const { isCollapsed , errors , isShowingConfirmation } = this . state
const { goHome } = this . props
return isShowingConfirmation
? this . renderConfirmation ( )
: (
h ( 'div.add-token' , [
h ( 'div.add-token__wrapper' , [
h ( 'div.add-token__title-container' , [
h ( 'div.add-token__title' , 'Add Token' ) ,
h ( 'div.add-token__description' , 'Keep track of the tokens you’ve bought with your MetaMask account. If you bought tokens using a different account, those tokens will not appear here.' ) ,
h ( 'div.add-token__description' , 'Search for tokens or select from our list of popular tokens.' ) ,
] ) ,
h ( 'div.add-token__content-container' , [
h ( 'div.add-token__input-container' , [
h ( 'input.add-token__input' , {
type : 'text' ,
placeholder : 'Search' ,
onChange : e => this . setState ( { searchQuery : e . target . value } ) ,
} ) ,
h ( 'div.add-token__search-input-error-message' , errors . tokenSelector ) ,
] ) ,
h (
'div.add-token__token-icons-container' ,
this . renderTokenList ( ) ,
) ,
] ) ,
h ( 'div.add-token__footers' , [
h ( 'div.add-token__add-custom' , {
onClick : ( ) => this . setState ( { isCollapsed : ! isCollapsed } ) ,
} , [
'Add custom token' ,
h ( ` i.fa.fa-angle- ${ isCollapsed ? 'down' : 'up' } ` ) ,
] ) ,
this . renderCustomForm ( ) ,
] ) ,
] ) ,
h ( 'div.add-token__buttons' , [
h ( 'button.btn-secondary' , {
onClick : this . onNext ,
} , 'Next' ) ,
h ( 'button.btn-tertiary' , {
onClick : goHome ,
} , 'Cancel' ) ,
] ) ,
] )
)
}