aboutsummaryrefslogblamecommitdiffstats
path: root/ui/app/components/send-token/index.js
blob: a40664977a134b43526b6a65667fb0e68351e2e5 (plain) (tree)
1
2
3
4
5
6
7
8
9


                                              
                                        
                                     


                                            
                                                         







                                                          




                                                                              
                                    



                                                                  
                                          
                                                                  
                                                                                    



                                                                        
 
          
                    



                         
                      
                         
                  
            




                                        
                                                                                   







                                                                            
                                                                                       
                                                                 
                                                       







                                    

                        

                            

                   



               



                                                            

                
                    


                                 
 





                                                                    

                  





                            

                                            
                                               
                   

                              
      
    

 


                                                  
                         





                                                         
                                     
 
                  



                                                       

   
                                  
                                   


                                                                                
          

                                  


   
                                                           



                                               
                      


                                                        
                                      












                                                             
                  










                                                                              
                                                                    


                   
                                                













                         
                  


                                                 










                                
                                                                

                                                             









                                                              
                                 

                




                                                        
                                


                                              
                                
                



                                    

                               
        



                                     

















                                                                    
                                                                        





                                                           

                                     


                
                      


                            




                                                        


                                        

                                                                      





                                                                           


                                    
         

                                   
        
                                                   
       
                                                                        








                                                        



                                     



                   
                      


                         




                                                                                       

                                       

                                  

                                                                
                                                         
        







                                       


                                         

                                                           



                                                
                          
                                    
                                         
                               




                                                                                  
                        

        


                                                       



                                                         
                                             
                                         






                                                                

                                                             
                                     


                                                       

                                                                  
                    

                                                          
                      


    



                                                
            


                              




                                      


                                                                  







                                                                     
                                                              



                                                           
       
                         

    
const Component = require('react').Component
const connect = require('react-redux').connect
const h = require('react-hyperscript')
const classnames = require('classnames')
const abi = require('ethereumjs-abi')
const inherits = require('util').inherits
const actions = require('../../actions')
const selectors = require('../../selectors')
const { isValidAddress, allNull } = require('../../util')

// const BalanceComponent = require('./balance-component')
const Identicon = require('../identicon')
const TokenBalance = require('../token-balance')
const CurrencyToggle = require('../send/currency-toggle')
const GasTooltip = require('../send/gas-tooltip')
const GasFeeDisplay = require('../send/gas-fee-display')

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

function mapStateToProps (state) {
  // const sidebarOpen = state.appState.sidebarOpen

  const { warning } = state.appState
  const identities = state.metamask.identities
  const addressBook = state.metamask.addressBook
  const conversionRate = state.metamask.conversionRate
  const currentBlockGasLimit = state.metamask.currentBlockGasLimit
  const accounts = state.metamask.accounts
  const selectedTokenAddress = state.metamask.selectedTokenAddress
  const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
  const selectedToken = selectors.getSelectedToken(state)
  const tokenExchangeRates = state.metamask.tokenExchangeRates
  const pair = `${selectedToken.symbol.toLowerCase()}_eth`
  const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {}

  return {
    selectedAddress,
    selectedTokenAddress,
    identities,
    addressBook,
    conversionRate,
    tokenExchangeRate,
    currentBlockGasLimit,
    selectedToken,
    warning,
  }
}

function mapDispatchToProps (dispatch) {
  return {
    backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
    hideWarning: () => dispatch(actions.hideWarning()),
    addToAddressBook: (recipient, nickname) => dispatch(
      actions.addToAddressBook(recipient, nickname)
    ),
    signTx: txParams => dispatch(actions.signTx(txParams)),
    signTokenTx: (tokenAddress, toAddress, amount, txData) => (
      dispatch(actions.signTokenTx(tokenAddress, toAddress, amount, txData))
    ),
    updateTokenExchangeRate: token => dispatch(actions.updateTokenExchangeRate(token)),
    estimateGas: params => dispatch(actions.estimateGas(params)),
    getGasPrice: () => dispatch(actions.getGasPrice()),
  }
}

inherits(SendTokenScreen, Component)
function SendTokenScreen () {
  Component.call(this)
  this.state = {
    to: '',
    amount: '0x0',
    amountToSend: '0x0',
    selectedCurrency: 'USD',
    isGasTooltipOpen: false,
    gasPrice: null,
    gasLimit: null,
    errors: {},
  }
}

SendTokenScreen.prototype.componentWillMount = function () {
  const {
    updateTokenExchangeRate,
    selectedToken: { symbol },
    getGasPrice,
    estimateGas,
    selectedAddress,
  } = this.props

  updateTokenExchangeRate(symbol)

  const data = Array.prototype.map.call(
    abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
    x => ('00' + x.toString(16)).slice(-2)
  ).join('')

  console.log(data)
  Promise.all([
    getGasPrice(),
    estimateGas({
      from: selectedAddress,
      value: '0x0',
      gas: '746a528800',
      data,
    }),
  ])
  .then(([blockGasPrice, estimatedGas]) => {
    console.log({ blockGasPrice, estimatedGas})
    this.setState({
      gasPrice: blockGasPrice,
      gasLimit: estimatedGas,
    })
  })
}

SendTokenScreen.prototype.validate = function () {
  const {
    to,
    amount: stringAmount,
    gasPrice: hexGasPrice,
    gasLimit: hexGasLimit,
  } = this.state

  const gasPrice = parseInt(hexGasPrice, 16)
  const gasLimit = parseInt(hexGasLimit, 16) / 1000000000
  const amount = Number(stringAmount)

  const errors = {
    to: !to ? t('required') : null,
    amount: !amount ? t('required') : null,
    gasPrice: !gasPrice ? t('gasPriceRequired') : null,
    gasLimit: !gasLimit ? t('gasLimitRequired') : null,
  }

  if (to && !isValidAddress(to)) {
    errors.to = t('invalidAddress')
  }

  const isValid = Object.entries(errors).every(([key, value]) => value === null)
  return {
    isValid,
    errors: isValid ? {} : errors,
  }
}

SendTokenScreen.prototype.setErrorsFor = function (field) {
  const { errors: previousErrors } = this.state

  const {
    isValid,
    errors: newErrors,
  } = this.validate()

  const nextErrors = Object.assign({}, previousErrors, {
    [field]: newErrors[field] || null,
  })

  if (!isValid) {
    this.setState({
      errors: nextErrors,
      isValid,
    })
  }
}

SendTokenScreen.prototype.clearErrorsFor = function (field) {
  const { errors: previousErrors } = this.state
  const nextErrors = Object.assign({}, previousErrors, {
    [field]: null,
  })

  this.setState({
    errors: nextErrors,
    isValid: allNull(nextErrors),
  })
}

SendTokenScreen.prototype.getAmountToSend = function (amount, selectedToken) {
  const { decimals } = selectedToken || {}
  const multiplier = Math.pow(10, Number(decimals || 0))
  const sendAmount = '0x' + Number(amount * multiplier).toString(16)
  return sendAmount
}

SendTokenScreen.prototype.submit = function () {
  const {
    to,
    amount,
    gasPrice,
    gasLimit,
  } = this.state

  const {
    identities,
    selectedAddress,
    selectedTokenAddress,
    hideWarning,
    addToAddressBook,
    signTokenTx,
    selectedToken,
  } = this.props

  const { nickname = ' ' } = identities[to] || {}

  hideWarning()
  addToAddressBook(to, nickname)

  const txParams = {
    from: selectedAddress,
    value: '0',
    gas: gasLimit,
    gasPrice: gasPrice,
  }

  const sendAmount = this.getAmountToSend(amount, selectedToken)

  signTokenTx(selectedTokenAddress, to, sendAmount, txParams)
}

SendTokenScreen.prototype.renderToAddressInput = function () {
  const {
    identities,
    addressBook,
  } = this.props

  const {
    to,
    errors: { to: errorMessage },
  } = this.state

  return h('div', {
    className: classnames('send-screen-input-wrapper', {
      'send-screen-input-wrapper--error': errorMessage,
    }),
  }, [
    h('div', [t('toSpecific')]),
    h('input.large-input.send-screen-input', {
      name: 'address',
      list: 'addresses',
      placeholder: t('address'),
      value: to,
      onChange: e => this.setState({
        to: e.target.value,
        errors: {},
      }),
      onBlur: () => {
        this.setErrorsFor('to')
      },
      onFocus: event => {
        if (to) event.target.select()
        this.clearErrorsFor('to')
      },
    }),
    h('datalist#addresses', [
      // Corresponds to the addresses owned.
      Object.entries(identities).map(([key, { address, name }]) => {
        return h('option', {
          value: address,
          label: name,
          key: address,
        })
      }),
      addressBook.map(({ address, name }) => {
        return h('option', {
          value: address,
          label: name,
          key: address,
        })
      }),
    ]),
    h('div.send-screen-input-wrapper__error-message', [ errorMessage ]),
  ])
}

SendTokenScreen.prototype.renderAmountInput = function () {
  const {
    selectedCurrency,
    amount,
    errors: { amount: errorMessage },
  } = this.state

  const {
    tokenExchangeRate,
    selectedToken: {symbol},
  } = this.props

  return h('div.send-screen-input-wrapper', {
    className: classnames('send-screen-input-wrapper', {
      'send-screen-input-wrapper--error': errorMessage,
    }),
  }, [
    h('div.send-screen-amount-labels', [
      h('span', ['Amount']),
      h(CurrencyToggle, {
        currentCurrency: tokenExchangeRate ? selectedCurrency : 'USD',
        currencies: tokenExchangeRate ? [ symbol, 'USD' ] : [],
        onClick: currency => this.setState({ selectedCurrency: currency }),
      }),
    ]),
    h('input.large-input.send-screen-input', {
      placeholder: `0 ${symbol}`,
      type: 'number',
      value: amount,
      onChange: e => this.setState({
        amount: e.target.value,
      }),
      onBlur: () => {
        this.setErrorsFor('amount')
      },
      onFocus: () => this.clearErrorsFor('amount'),
    }),
    h('div.send-screen-input-wrapper__error-message', [ errorMessage ]),
  ])
}

SendTokenScreen.prototype.renderGasInput = function () {
  const {
    isGasTooltipOpen,
    gasPrice,
    gasLimit,
    selectedCurrency,
    errors: {
      gasPrice: gasPriceErrorMessage,
      gasLimit: gasLimitErrorMessage,
    },
  } = this.state

  const {
    conversionRate,
    tokenExchangeRate,
    currentBlockGasLimit,
  } = this.props

  return h('div.send-screen-input-wrapper', {
    className: classnames('send-screen-input-wrapper', {
      'send-screen-input-wrapper--error': gasPriceErrorMessage || gasLimitErrorMessage,
    }),
  }, [
    isGasTooltipOpen && h(GasTooltip, {
      className: 'send-tooltip',
      gasPrice: gasPrice || '0x0',
      gasLimit: gasLimit || '0x0',
      onClose: () => this.setState({ isGasTooltipOpen: false }),
      onFeeChange: ({ gasLimit, gasPrice }) => {
        this.setState({ gasLimit, gasPrice, errors: {} })
      },
      onBlur: () => {
        this.setErrorsFor('gasLimit')
        this.setErrorsFor('gasPrice')
      },
      onFocus: () => {
        this.clearErrorsFor('gasLimit')
        this.clearErrorsFor('gasPrice')
      },
    }),

    h('div.send-screen-gas-labels', {}, [
      h('span', [ h('i.fa.fa-bolt'), t('gasFeeSpecific')]),
      h('span', [t('whatsThis')]),
    ]),
    h('div.large-input.send-screen-gas-input', [
      h(GasFeeDisplay, {
        conversionRate,
        tokenExchangeRate,
        gasPrice: gasPrice || '0x0',
        activeCurrency: selectedCurrency,
        gas: gasLimit || '0x0',
        blockGasLimit: currentBlockGasLimit,
      }),
      h(
        'div.send-screen-gas-input-customize',
        { onClick: () => this.setState({ isGasTooltipOpen: !isGasTooltipOpen }) },
        [t('customize')]
      ),
    ]),
    h('div.send-screen-input-wrapper__error-message', [
      gasPriceErrorMessage || gasLimitErrorMessage,
    ]),
  ])
}

SendTokenScreen.prototype.renderMemoInput = function () {
  return h('div.send-screen-input-wrapper', [
    h('div', {}, [t('transactionMemo')]),
    h(
      'input.large-input.send-screen-input',
      { onChange: e => this.setState({ memo: e.target.value }) }
    ),
  ])
}

SendTokenScreen.prototype.renderButtons = function () {
  const { selectedAddress, backToAccountDetail } = this.props
  const { isValid } = this.validate()

  return h('div.send-token__button-group', [
    h('button.send-token__button-next.btn-secondary', {
      className: !isValid && 'send-screen__send-button__disabled',
      onClick: () => isValid && this.submit(),
    }, [t('next')]),
    h('button.send-token__button-cancel.btn-tertiary', {
      onClick: () => backToAccountDetail(selectedAddress),
    }, [t('cancel')]),
  ])
}

SendTokenScreen.prototype.render = function () {
  const {
    selectedTokenAddress,
    selectedToken,
    warning,
  } = this.props

  return h('div.send-token', [
    h('div.send-token__content', [
      h(Identicon, {
        diameter: 75,
        address: selectedTokenAddress,
      }),
      h('div.send-token__title', [t('sendTokens')]),
      h('div.send-token__description', [t('sendTokensAnywhere')]),
      h('div.send-token__balance-text', [t('tokenBalance')]),
      h('div.send-token__token-balance', [
        h(TokenBalance, { token: selectedToken, balanceOnly: true }),
      ]),
      h('div.send-token__token-symbol', [selectedToken.symbol]),
      this.renderToAddressInput(),
      this.renderAmountInput(),
      this.renderGasInput(),
      this.renderMemoInput(),
      warning && h('div.send-screen-input-wrapper--error', {},
        h('div.send-screen-input-wrapper__error-message', [
          warning,
        ])
      ),
    ]),
    this.renderButtons(),
  ])
}