aboutsummaryrefslogblamecommitdiffstats
path: root/ui/app/add-token.js
blob: 622cf2bc227532b3e6fa9d7e99c1f535212f56ee (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 Fuse = require('fuse.js')
const contractMap = require('eth-contract-metadata')
const contractList = Object.entries(contractMap).map(([ _, tokenData]) => tokenData)
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 Tooltip = require('./components/tooltip.js')


const ethUtil = require('ethereumjs-util')
const abi = require('human-standard-token-abi')
const Eth = require('ethjs-query')
const EthContract = require('ethjs-contract')

const emptyAddr = '0x0000000000000000000000000000000000000000'

module.exports = connect(mapStateToProps, mapDispatchToProps)(AddTokenScreen)

function mapStateToProps (state) {
  return {
    identities: state.metamask.identities,
  }
}

function mapDispatchToProps (dispatch) {
  return {
    goHome: () => dispatch(actions.goHome()),
  }
}

inherits(AddTokenScreen, Component)
function AddTokenScreen () {
  this.state = {
    // warning: null,
    // address: null,
    // symbol: 'TOKEN',
    // decimals: 18,
    customAddress: '',
    customSymbol: '',
    customDecimals: 0,
    searchQuery: '',
    isCollapsed: true,
    selectedToken: {},
  }
  this.tokenAddressDidChange = this.tokenAddressDidChange.bind(this)
  Component.call(this)
}

AddTokenScreen.prototype.toggleToken = function (symbol) {
  const { selectedToken } = this.state
  const { [symbol]: isSelected } = selectedToken
  this.setState({
    selectedToken: {
      ...selectedToken,
      [symbol]: !isSelected,
    },
  })
}

AddTokenScreen.prototype.renderCustomForm = function () {
  const { customAddress, customSymbol, customDecimals } = this.state

  return !this.state.isCollapsed && (
    h('div.add-token__add-custom-form', [
      h('div.add-token__add-custom-field', [
        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-field', [
        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-field', [
        h('div.add-token__add-custom-label', 'Decimals of Precision'),
        h('input.add-token__add-custom-input', {
          type: 'number',
          value: customDecimals,
          disabled: true,
        }),
      ]),
    ])
  )
}

AddTokenScreen.prototype.renderTokenList = function () {
  const { searchQuery = '', selectedToken } = this.state
  const results = searchQuery
    ? fuse.search(searchQuery) || []
    : contractList

  return Array(6).fill(undefined)
    .map((_, i) => {
      const { logo, symbol, name } = results[i] || {}
      return Boolean(logo || symbol || name) && (
        h('div.add-token__token-wrapper', {
          className: classnames('add-token__token-wrapper', {
            'add-token__token-wrapper--selected': selectedToken[symbol],
          }),
          onClick: () => this.toggleToken(symbol),
        }, [
          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),
          ]),
        ])
      )
    })
}

AddTokenScreen.prototype.render = function () {
  const { isCollapsed } = this.state
  const { goHome } = this.props

  return (
    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__token-icons-container',
            this.renderTokenList(),
          ),
        ]),
        h('div.add-token__footers', [
          h('div.add-token__add-custom', {
            onClick: () => this.setState({ isCollapsed: !isCollapsed }),
          }, 'Add custom token'),
          this.renderCustomForm(),
        ]),
      ]),
      h('div.add-token__buttons', [
        h('button.btn-secondary', 'Next'),
        h('button.btn-tertiary', {
          onClick: goHome,
        }, 'Cancel'),
      ]),
    ])
  )
}

// AddTokenScreen.prototype.render = function () {
//   const state = this.state
//   const props = this.props
//   const { warning, symbol, decimals } = state

//   return (
//     h('.flex-column.flex-grow', [

//       // subtitle and nav
//       h('.section-title.flex-row.flex-center', [
//         h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
//           onClick: (event) => {
//             props.dispatch(actions.goHome())
//           },
//         }),
//         h('h2.page-subtitle', 'Add Token'),
//       ]),

//       h('.error', {
//         style: {
//           display: warning ? 'block' : 'none',
//           padding: '0 20px',
//           textAlign: 'center',
//         },
//       }, warning),

//       // conf view
//       h('.flex-column.flex-justify-center.flex-grow.select-none', [
//         h('.flex-space-around', {
//           style: {
//             padding: '20px',
//           },
//         }, [

//           h('div', [
//             h(Tooltip, {
//               position: 'top',
//               title: 'The contract of the actual token contract. Click for more info.',
//             }, [
//               h('a', {
//                 style: { fontWeight: 'bold', paddingRight: '10px'},
//                 href: 'https://consensyssupport.happyfox.com/staff/kb/article/24-what-is-a-token-contract-address',
//                 target: '_blank',
//               }, [
//                 h('span', 'Token Contract Address  '),
//                 h('i.fa.fa-question-circle'),
//               ]),
//             ]),
//           ]),

//           h('section.flex-row.flex-center', [
//             h('input#token-address', {
//               name: 'address',
//               placeholder: 'Token Contract Address',
//               onChange: this.tokenAddressDidChange.bind(this),
//               style: {
//                 width: 'inherit',
//                 flex: '1 0 auto',
//                 height: '30px',
//                 margin: '8px',
//               },
//             }),
//           ]),

//           h('div', [
//             h('span', {
//               style: { fontWeight: 'bold', paddingRight: '10px'},
//             }, 'Token Symbol'),
//           ]),

//           h('div', { style: {display: 'flex'} }, [
//             h('input#token_symbol', {
//               placeholder: `Like "ETH"`,
//               value: symbol,
//               style: {
//                 width: 'inherit',
//                 flex: '1 0 auto',
//                 height: '30px',
//                 margin: '8px',
//               },
//               onChange: (event) => {
//                 var element = event.target
//                 var symbol = element.value
//                 this.setState({ symbol })
//               },
//             }),
//           ]),

//           h('div', [
//             h('span', {
//               style: { fontWeight: 'bold', paddingRight: '10px'},
//             }, 'Decimals of Precision'),
//           ]),

//           h('div', { style: {display: 'flex'} }, [
//             h('input#token_decimals', {
//               value: decimals,
//               type: 'number',
//               min: 0,
//               max: 36,
//               style: {
//                 width: 'inherit',
//                 flex: '1 0 auto',
//                 height: '30px',
//                 margin: '8px',
//               },
//               onChange: (event) => {
//                 var element = event.target
//                 var decimals = element.value.trim()
//                 this.setState({ decimals })
//               },
//             }),
//           ]),

//           h('button', {
//             style: {
//               alignSelf: 'center',
//             },
//             onClick: (event) => {
//               const valid = this.validateInputs()
//               if (!valid) return

//               const { address, symbol, decimals } = this.state
//               this.props.dispatch(actions.addToken(address.trim(), symbol.trim(), decimals))
//             },
//           }, 'Add'),
//         ]),
//       ]),
//     ])
//   )
// }

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.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.validateInputs = function () {
  let msg = ''
  const state = this.state
  const identitiesList = Object.keys(this.props.identities)
  const { address, symbol, 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.'
  }

  const ownAddress = identitiesList.includes(standardAddress)
  if (ownAddress) {
    msg = 'Personal address detected. Input the token contract address.'
  }

  const isValid = validAddress && validDecimals && !ownAddress

  if (!isValid) {
    this.setState({
      warning: msg,
    })
  } else {
    this.setState({ warning: null })
  }

  return isValid
}

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(),
    })
  }
}