aboutsummaryrefslogblamecommitdiffstats
path: root/ui/app/add-token.js
blob: 3a806d34b993958cec86ad97451ca8aa2094e643 (plain) (tree)
1
2
3
4
5
6
7
8
9
10

                                            
                                        

                                              
                          

                                                    

                                                          


                                                

                                     
                    



                          



                                      
  
                                    
                                          
                                                   


                                                              
                                                                             

                                  
                                               
          

               
   

 


                                             
                                                             


   

                                   
                
                                 


                      

                      

                       
   
                                                                    
                                      


                      
                                                           
                                          




                                                                  
                 






                                              



      




















                                                                             
                                                                      
                            







                                                                        


























                                                                                           




                                                                        















                                                                                  
                                                                  

                           

                                          



      
                                                         
                                                                            
 

                                         




                                                                     
                                                              




                                                
                                                                           
         




                                                                    
                                                             




                                                
                                                                          
         




                                                                      
                                                                      




                                                
                                                                            



         
 
                                                        
                                                         




                                                                    
 

                                 
                                                              
                                                                    
                                                 
                                           
                                 
                                                                          
                                                                    
             
                                                                                     
            







                                                              
             


                                                                 



          
 












































                                                                                               
                                                  

                                                                         


                                                        




         
                                               
                                                                   
                               
 


                               












                                                                                                                                                                                                     
               
                                                                                 
             



                                                   
           


                                                                        



                                                              



                                   
                                                  

                          


                                                 



         
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 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'

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 () {
  this.tokenInfoGetter = tokenInfoGetter()
}

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 { symbol, decimals } = await this.tokenInfoGetter(address)
  if (symbol && decimals) {
    this.setState({
      customSymbol: symbol,
      customDecimals: decimals.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 fuseSearchResult = fuse.search(searchQuery)
  const addressSearchResult = contractList.filter(token => {
    return token.address.toLowerCase() === searchQuery.toLowerCase()
  })
  const results = [...addressSearchResult, ...fuseSearchResult]

  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-cancel.add-token__button', {
          onClick: () => this.setState({ isShowingConfirmation: false }),
        }, 'Back'),
        h('button.btn-clear.add-token__button', {
          onClick: () => addTokens(tokens).then(goHome),
        }, 'Add Tokens'),
      ]),
    ])
  )
}

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-cancel.add-token__button', {
          onClick: goHome,
        }, 'Cancel'),
        h('button.btn-clear.add-token__button', {
          onClick: this.onNext,
        }, 'Next'),
      ]),
    ])
  )
}