2017-06-15 05:42:48 +02:00
const inherits = require ( 'util' ) . inherits
const Component = require ( 'react' ) . Component
2017-09-21 07:57:36 +02:00
const classnames = require ( 'classnames' )
2017-06-15 05:42:48 +02:00
const h = require ( 'react-hyperscript' )
const connect = require ( 'react-redux' ) . connect
2018-01-18 03:50:18 +01:00
const R = require ( 'ramda' )
2017-09-20 06:18:36 +02:00
const Fuse = require ( 'fuse.js' )
const contractMap = require ( 'eth-contract-metadata' )
2017-09-22 03:44:52 +02:00
const TokenBalance = require ( './components/token-balance' )
const Identicon = require ( './components/identicon' )
2017-10-20 16:31:34 +02:00
const contractList = Object . entries ( contractMap )
. map ( ( [ _ , tokenData ] ) => tokenData )
. filter ( tokenData => Boolean ( tokenData . erc20 ) )
2017-09-20 06:18:36 +02:00
const fuse = new Fuse ( contractList , {
shouldSort : true ,
2017-09-21 07:57:36 +02:00
threshold : 0.45 ,
2017-09-20 06:18:36 +02:00
location : 0 ,
distance : 100 ,
maxPatternLength : 32 ,
minMatchCharLength : 1 ,
2018-01-17 21:59:25 +01:00
keys : [
{ name : 'name' , weight : 0.5 } ,
{ name : 'symbol' , weight : 0.5 } ,
] ,
2017-09-20 06:18:36 +02:00
} )
2017-09-21 07:57:36 +02:00
const actions = require ( './actions' )
2017-06-16 00:38:23 +02:00
const ethUtil = require ( 'ethereumjs-util' )
2017-10-27 19:39:40 +02:00
const { tokenInfoGetter } = require ( './token-util' )
2017-06-16 00:38:23 +02:00
const emptyAddr = '0x0000000000000000000000000000000000000000'
2017-09-21 07:57:36 +02:00
module . exports = connect ( mapStateToProps , mapDispatchToProps ) ( AddTokenScreen )
2017-06-15 05:42:48 +02:00
function mapStateToProps ( state ) {
2017-10-13 23:14:48 +02:00
const { identities , tokens } = state . metamask
2017-06-16 00:38:23 +02:00
return {
2017-10-13 23:14:48 +02:00
identities ,
tokens ,
2017-06-16 00:38:23 +02:00
}
2017-06-15 05:42:48 +02:00
}
2017-09-21 07:57:36 +02:00
function mapDispatchToProps ( dispatch ) {
return {
goHome : ( ) => dispatch ( actions . goHome ( ) ) ,
2017-09-22 03:44:52 +02:00
addTokens : tokens => dispatch ( actions . addTokens ( tokens ) ) ,
2017-09-21 07:57:36 +02:00
}
}
2017-06-15 05:42:48 +02:00
inherits ( AddTokenScreen , Component )
function AddTokenScreen ( ) {
2017-06-16 00:38:23 +02:00
this . state = {
2017-09-22 03:44:52 +02:00
isShowingConfirmation : false ,
2017-09-21 07:57:36 +02:00
customAddress : '' ,
customSymbol : '' ,
customDecimals : 0 ,
2017-09-20 06:18:36 +02:00
searchQuery : '' ,
isCollapsed : true ,
2017-09-22 03:44:52 +02:00
selectedTokens : { } ,
errors : { } ,
2017-06-16 00:38:23 +02:00
}
2017-09-21 07:57:36 +02:00
this . tokenAddressDidChange = this . tokenAddressDidChange . bind ( this )
2017-09-22 03:44:52 +02:00
this . onNext = this . onNext . bind ( this )
2017-06-15 05:42:48 +02:00
Component . call ( this )
}
2017-09-22 03:44:52 +02:00
AddTokenScreen . prototype . componentWillMount = function ( ) {
2017-10-27 19:39:40 +02:00
this . tokenInfoGetter = tokenInfoGetter ( )
2017-09-22 03:44:52 +02:00
}
AddTokenScreen . prototype . toggleToken = function ( address , token ) {
const { selectedTokens , errors } = this . state
const { [ address ] : selectedToken } = selectedTokens
2017-09-21 07:57:36 +02:00
this . setState ( {
2017-09-22 03:44:52 +02:00
selectedTokens : {
... selectedTokens ,
[ address ] : selectedToken ? null : token ,
} ,
errors : {
... errors ,
tokenSelector : null ,
2017-09-21 07:57:36 +02:00
} ,
} )
}
2017-09-22 03:44:52 +02:00
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 ,
} )
}
}
2017-10-13 23:14:48 +02:00
AddTokenScreen . prototype . checkExistingAddresses = function ( address ) {
2017-10-20 16:18:01 +02:00
if ( ! address ) return false
2017-10-13 23:14:48 +02:00
const tokensList = this . props . tokens
const matchesAddress = existingToken => {
return existingToken . address . toLowerCase ( ) === address . toLowerCase ( )
}
return R . any ( matchesAddress ) ( tokensList )
}
2017-09-22 03:44:52 +02:00
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.'
}
2017-10-13 23:14:48 +02:00
const tokenAlreadyAdded = this . checkExistingAddresses ( customAddress )
if ( tokenAlreadyAdded ) {
errors . customAddress = 'Token has already been added.'
}
2017-09-22 03:44:52 +02:00
} 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 ) {
2017-10-27 19:39:40 +02:00
const { symbol , decimals } = await this . tokenInfoGetter ( address )
2017-09-22 03:44:52 +02:00
if ( symbol && decimals ) {
this . setState ( {
2017-10-27 19:39:40 +02:00
customSymbol : symbol ,
customDecimals : decimals . toString ( ) ,
2017-09-22 03:44:52 +02:00
} )
}
}
2017-09-20 06:18:36 +02:00
AddTokenScreen . prototype . renderCustomForm = function ( ) {
2017-09-22 03:44:52 +02:00
const { customAddress , customSymbol , customDecimals , errors } = this . state
2017-09-21 07:57:36 +02:00
2017-09-20 06:18:36 +02:00
return ! this . state . isCollapsed && (
h ( 'div.add-token__add-custom-form' , [
2017-09-22 03:44:52 +02:00
h ( 'div' , {
className : classnames ( 'add-token__add-custom-field' , {
'add-token__add-custom-field--error' : errors . customAddress ,
} ) ,
} , [
2017-09-20 06:18:36 +02:00
h ( 'div.add-token__add-custom-label' , 'Token Address' ) ,
2017-09-21 07:57:36 +02:00
h ( 'input.add-token__add-custom-input' , {
type : 'text' ,
onChange : this . tokenAddressDidChange ,
value : customAddress ,
} ) ,
2017-09-22 03:44:52 +02:00
h ( 'div.add-token__add-custom-error-message' , errors . customAddress ) ,
2017-06-15 05:42:48 +02:00
] ) ,
2017-09-22 03:44:52 +02:00
h ( 'div' , {
className : classnames ( 'add-token__add-custom-field' , {
'add-token__add-custom-field--error' : errors . customSymbol ,
} ) ,
} , [
2017-09-20 06:18:36 +02:00
h ( 'div.add-token__add-custom-label' , 'Token Symbol' ) ,
2017-09-21 07:57:36 +02:00
h ( 'input.add-token__add-custom-input' , {
type : 'text' ,
value : customSymbol ,
disabled : true ,
} ) ,
2017-09-22 03:44:52 +02:00
h ( 'div.add-token__add-custom-error-message' , errors . customSymbol ) ,
2017-09-20 06:18:36 +02:00
] ) ,
2017-09-22 03:44:52 +02:00
h ( 'div' , {
className : classnames ( 'add-token__add-custom-field' , {
'add-token__add-custom-field--error' : errors . customDecimals ,
} ) ,
} , [
2017-09-20 06:18:36 +02:00
h ( 'div.add-token__add-custom-label' , 'Decimals of Precision' ) ,
2017-09-21 07:57:36 +02:00
h ( 'input.add-token__add-custom-input' , {
type : 'number' ,
value : customDecimals ,
disabled : true ,
} ) ,
2017-09-22 03:44:52 +02:00
h ( 'div.add-token__add-custom-error-message' , errors . customDecimals ) ,
2017-09-20 06:18:36 +02:00
] ) ,
] )
)
}
2017-06-15 05:42:48 +02:00
2017-09-20 06:18:36 +02:00
AddTokenScreen . prototype . renderTokenList = function ( ) {
2017-09-22 03:44:52 +02:00
const { searchQuery = '' , selectedTokens } = this . state
2018-01-17 21:59:25 +01:00
const fuseSearchResult = fuse . search ( searchQuery )
const addressSearchResult = contractList . filter ( token => {
return token . address . toLowerCase ( ) === searchQuery . toLowerCase ( )
} )
const results = [ ... addressSearchResult , ... fuseSearchResult ]
2017-06-15 05:42:48 +02:00
2017-09-20 06:18:36 +02:00
return Array ( 6 ) . fill ( undefined )
. map ( ( _ , i ) => {
2017-09-22 03:44:52 +02:00
const { logo , symbol , name , address } = results [ i ] || { }
2017-10-13 23:14:48 +02:00
const tokenAlreadyAdded = this . checkExistingAddresses ( address )
2017-09-20 06:18:36 +02:00
return Boolean ( logo || symbol || name ) && (
2017-09-21 07:57:36 +02:00
h ( 'div.add-token__token-wrapper' , {
2017-10-13 23:14:48 +02:00
className : classnames ( {
2017-09-22 03:44:52 +02:00
'add-token__token-wrapper--selected' : selectedTokens [ address ] ,
2017-10-13 23:14:48 +02:00
'add-token__token-wrapper--disabled' : tokenAlreadyAdded ,
2017-09-21 07:57:36 +02:00
} ) ,
2017-10-13 23:14:48 +02:00
onClick : ( ) => ! tokenAlreadyAdded && this . toggleToken ( address , results [ i ] ) ,
2017-09-21 07:57:36 +02:00
} , [
2017-09-20 06:18:36 +02:00
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 ) ,
2017-06-16 00:38:23 +02:00
] ) ,
2017-10-26 03:47:28 +02:00
// tokenAlreadyAdded && (
// h('div.add-token__token-message', 'Already added')
// ),
2017-09-20 06:18:36 +02:00
] )
)
} )
}
2017-06-16 00:38:23 +02:00
2017-09-22 03:44:52 +02:00
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' , [
2018-01-13 01:41:29 +01:00
h ( 'button.btn-cancel.add-token__button' , {
2017-09-22 03:44:52 +02:00
onClick : ( ) => this . setState ( { isShowingConfirmation : false } ) ,
} , 'Back' ) ,
2018-01-13 01:41:29 +01:00
h ( 'button.btn-clear.add-token__button' , {
onClick : ( ) => addTokens ( tokens ) . then ( goHome ) ,
} , 'Add Tokens' ) ,
2017-09-22 03:44:52 +02:00
] ) ,
] )
)
}
2017-09-20 06:18:36 +02:00
AddTokenScreen . prototype . render = function ( ) {
2017-09-22 03:44:52 +02:00
const { isCollapsed , errors , isShowingConfirmation } = this . state
2017-09-21 07:57:36 +02:00
const { goHome } = this . props
2017-06-16 00:38:23 +02:00
2017-09-22 03:44:52 +02:00
return isShowingConfirmation
? this . renderConfirmation ( )
: (
2017-09-20 06:18:36 +02:00
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 } ) ,
2017-06-15 05:42:48 +02:00
} ) ,
2017-09-22 03:44:52 +02:00
h ( 'div.add-token__search-input-error-message' , errors . tokenSelector ) ,
2017-06-15 05:42:48 +02:00
] ) ,
2017-09-20 06:18:36 +02:00
h (
'div.add-token__token-icons-container' ,
this . renderTokenList ( ) ,
) ,
2017-06-15 05:42:48 +02:00
] ) ,
2017-09-20 06:18:36 +02:00
h ( 'div.add-token__footers' , [
h ( 'div.add-token__add-custom' , {
onClick : ( ) => this . setState ( { isCollapsed : ! isCollapsed } ) ,
2017-10-24 09:38:39 +02:00
} , [
'Add custom token' ,
h ( ` i.fa.fa-angle- ${ isCollapsed ? 'down' : 'up' } ` ) ,
] ) ,
2017-09-20 06:18:36 +02:00
this . renderCustomForm ( ) ,
] ) ,
] ) ,
h ( 'div.add-token__buttons' , [
2018-01-13 01:16:28 +01:00
h ( 'button.btn-cancel.add-token__button' , {
2017-09-21 07:57:36 +02:00
onClick : goHome ,
} , 'Cancel' ) ,
2018-01-13 01:16:28 +01:00
h ( 'button.btn-clear.add-token__button' , {
onClick : this . onNext ,
} , 'Next' ) ,
2017-06-15 05:42:48 +02:00
] ) ,
] )
)
}