aboutsummaryrefslogblamecommitdiffstats
path: root/ui/app/components/pending-tx.js
blob: a8fac4c283f16b37177327fd44587e218178b438 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                            
                                          
                                      


                                               
                                         
                                     
                              

                                         
 

                                          
                                                             
                               
                                                                       
 
                                       
                               
                                                               
 






                                                







                                                                        

                                                                                    


                   
                    




                                        

                                                                                   
                                                                      


   







                              
                                          
 
 



                                                      
                                             
                                       

                                        



                                                                             

                    
 









                                                        
                              






                                          
                              













                                          
                              






                                          
                              







                          
   
 
 
 



                                             
 
        
                          
                            





                                                                        


                                                                     
                                      
 
                                       
 
                                       



                            
                      
                        

                   
                                       



                            
                      
                        


                   













                                                                             









                                                                 













                                                                           

                  













                                                                                       

                  

























                                                                            

                


                    
 







                                                                                                             

                                                         
 
                  
 
          
                                                             
                                   
        
                             





                                                                
           

                                                                 


                        

                                     

                

                                                                                               
             
                                         
                                                   



                                     
                              

                


                                                                                           
           
 
                                                               

                                
                             
            
            
                                                                                    
           
 
                                                                            


                                                                           
           
 





                                                                                                    
               

             




                                                                                                
               

             



                                                                                        
 
                                                                       
               

             
 



                                                                              

               


                                                                      
               
             

           














































































                                                                                                       
         
 
                                                         
                                









                                     
                                                               
 
                        


                                                                
         
      
   

 

































                                                                  
 



                                                 
                                            
 



                                                                         
                                        


   




                                                       
                                                 
                               



                                    
                                             


                                                             
                                               



             
                                                     
                                                
                          
                          
                                                           
 
                                                                       
               




                                                              



                                                   





                                                            




                                                                                        
const Component = require('react').Component
const { connect } = require('react-redux')
const h = require('react-hyperscript')
const abi = require('human-standard-token-abi')
const abiDecoder = require('abi-decoder')
abiDecoder.addABI(abi)
const inherits = require('util').inherits
const actions = require('../actions')
const clone = require('clone')
const FiatValue = require('./fiat-value')
const Identicon = require('./identicon')

const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const hexToBn = require('../../../app/scripts/lib/hex-to-bn')
const util = require('../util')
const { conversionUtil, addCurrencies } = require('../conversion-util')

const MIN_GAS_PRICE_GWEI_BN = new BN(1)
const GWEI_FACTOR = new BN(1e9)
const MIN_GAS_PRICE_BN = MIN_GAS_PRICE_GWEI_BN.mul(GWEI_FACTOR)

// Next: create separate react components
// roughly 5 components:
//   heroIcon
//   numericDisplay (contains symbol + currency)
//   divider
//   contentBox
//   actionButtons

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

function mapStateToProps (state) {
  const {
    conversionRate,
    identities,
  } = state.metamask
  const accounts = state.metamask.accounts
  const selectedAddress = state.metamask.selectedAddress || Object.keys(accounts)[0]
  return {
    conversionRate,
    identities,
    selectedAddress,
  }
}

function mapDispatchToProps (dispatch) {
  return {
    setCurrentCurrencyToUSD: () => dispatch(actions.setCurrentCurrency('USD')),
    backToAccountDetail: address => dispatch(actions.backToAccountDetail(address)),
    cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
  }
}

inherits(PendingTx, Component)
function PendingTx () {
  Component.call(this)
  this.state = {
    valid: true,
    txData: null,
    submitting: false,
  }
  this.onSubmit = this.onSubmit.bind(this)
}

PendingTx.prototype.componentWillMount = function () {
  this.props.setCurrentCurrencyToUSD()
}

PendingTx.prototype.getAmount = function () {
  const { conversionRate } = this.props
  const txMeta = this.gatherTxMeta()
  const txParams = txMeta.txParams || {}
  const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
  const { params = [] } = decodedData || {}
  const { name, value } = params[1] || {}
  const amountBn = name === '_value'
    ? value
    : txParams.value

  if (name === '_value') {
    const token = util.getContractAtAddress(txParams.to)
    token.symbol().then(symbol => console.log({symbol}))
    console.log({txParams, txMeta, decodedData, token})
    const USD = conversionUtil(amountBn, {
      fromNumericBase: 'hex',
      toNumericBase: 'dec',
      fromCurrency: 'ETH',
      toCurrency: 'USD',
      numberOfDecimals: 2,
      fromDenomination: 'WEI',
      conversionRate,
    })
    const ETH = conversionUtil(amountBn, {
      fromNumericBase: 'hex',
      toNumericBase: 'dec',
      fromCurrency: 'ETH',
      toCurrency: 'ETH',
      fromDenomination: 'WEI',
      conversionRate,
      numberOfDecimals: 6,
    })
    return {
      USD,
      ETH,
    }
  } else {
    const USD = conversionUtil(amountBn, {
      fromNumericBase: 'hex',
      toNumericBase: 'dec',
      fromCurrency: 'ETH',
      toCurrency: 'USD',
      numberOfDecimals: 2,
      fromDenomination: 'WEI',
      conversionRate,
    })
    const ETH = conversionUtil(amountBn, {
      fromNumericBase: 'hex',
      toNumericBase: 'dec',
      fromCurrency: 'ETH',
      toCurrency: 'ETH',
      fromDenomination: 'WEI',
      conversionRate,
      numberOfDecimals: 6,
    })

    return {
      USD,
      ETH,
    }
  }

}

PendingTx.prototype.getGasFee = function () {
  const { conversionRate } = this.props
  const txMeta = this.gatherTxMeta()
  const txParams = txMeta.txParams || {}

  // Gas
  const gas = txParams.gas
  const gasBn = hexToBn(gas)
  
  // From latest master
//   const gasLimit = new BN(parseInt(blockGasLimit))
//   const safeGasLimitBN = this.bnMultiplyByFraction(gasLimit, 19, 20)
//   const saferGasLimitBN = this.bnMultiplyByFraction(gasLimit, 18, 20)
//   const safeGasLimit = safeGasLimitBN.toString(10)

  // Gas Price
  const gasPrice = txParams.gasPrice || MIN_GAS_PRICE_BN.toString(16)
  const gasPriceBn = hexToBn(gasPrice)

  const txFeeBn = gasBn.mul(gasPriceBn)

  const USD = conversionUtil(txFeeBn, {
    fromNumericBase: 'BN',
    toNumericBase: 'dec',
    fromDenomination: 'WEI',
    fromCurrency: 'ETH',
    toCurrency: 'USD',
    numberOfDecimals: 2,
    conversionRate,
  })
  const ETH = conversionUtil(txFeeBn, {
    fromNumericBase: 'BN',
    toNumericBase: 'dec',
    fromDenomination: 'WEI',
    fromCurrency: 'ETH',
    toCurrency: 'ETH',
    numberOfDecimals: 6,
    conversionRate,
  })

  return {
    USD,
    ETH,
  }
}

PendingTx.prototype.getData = function () {
  const { identities } = this.props
  const txMeta = this.gatherTxMeta()
  const txParams = txMeta.txParams || {}
  const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
  const { name, params = [] } = decodedData || {}
  const { type, value } = params[0] || {}
  const { USD: gasFeeInUSD, ETH: gasFeeInETH } = this.getGasFee()
  const { USD: amountInUSD, ETH: amountInETH } = this.getAmount()

  const totalInUSD = addCurrencies(gasFeeInUSD, amountInUSD, {
    toNumericBase: 'dec',
    numberOfDecimals: 2,
  })
  const totalInETH = addCurrencies(gasFeeInETH, amountInETH, {
    toNumericBase: 'dec',
    numberOfDecimals: 6,
  })

  if (name === 'transfer' && type === 'address') {
    return {
      from: {
        address: txParams.from,
        name: identities[txParams.from].name,
      },
      to: {
        address: value,
        name: identities[value] ? identities[value].name : 'New Recipient',
      },
      memo: txParams.memo || '',
      gasFeeInUSD,
      gasFeeInETH,
      amountInUSD,
      amountInETH,
    }
  } else {
    return {
      from: {
        address: txParams.from,
        name: identities[txParams.from].name,
      },
      to: {
        address: txParams.to,
        name: identities[txParams.to] ? identities[txParams.to].name : 'New Recipient',
      },
      memo: txParams.memo || '',
      gasFeeInUSD,
      gasFeeInETH,
      amountInUSD,
      amountInETH,
      totalInUSD,
      totalInETH,
    }
  }
}

PendingTx.prototype.render = function () {
  const { backToAccountDetail, selectedAddress } = this.props
  const txMeta = this.gatherTxMeta()
  const txParams = txMeta.txParams || {}

  // recipient check
  // const isValidAddress = !txParams.to || util.isValidAddress(txParams.to)

  const {
    from: {
      address: fromAddress,
      name: fromName,
    },
    to: {
      address: toAddress,
      name: toName,
    },
    memo,
    gasFeeInUSD,
    gasFeeInETH,
    amountInUSD,
    amountInETH,
    totalInUSD,
    totalInETH,
  } = this.getData()

  // This is from the latest master
  // It handles some of the errors that we are not currently handling
  // Leaving as comments fo reference

  // const balanceBn = hexToBn(balance)
  // const insufficientBalance = balanceBn.lt(maxCost)
  // const buyDisabled = insufficientBalance || !this.state.valid || !isValidAddress || this.state.submitting
  // const showRejectAll = props.unconfTxListLength > 1
//   const dangerousGasLimit = gasBn.gte(saferGasLimitBN)
//   const gasLimitSpecified = txMeta.gasLimitSpecified

  this.inputs = []

  return (
    h('div.flex-column.flex-grow.confirm-screen-container', {
      style: { minWidth: '355px' },
    }, [
      // Main Send token Card
      h('div.confirm-screen-wrapper.flex-column.flex-grow', [
        h('h3.flex-center.confirm-screen-header', [
          h('button.confirm-screen-back-button', {
            onClick: () => backToAccountDetail(selectedAddress),
          }, 'BACK'),
          h('div.confirm-screen-title', 'Confirm Transaction'),
        ]),
        h('div.flex-row.flex-center.confirm-screen-identicons', [
          h('div.confirm-screen-account-wrapper', [
            h(
              Identicon,
              {
                address: fromAddress,
                diameter: 100,
              },
            ),
            h('span.confirm-screen-account-name', fromName),
            h('span.confirm-screen-account-number', fromAddress.slice(fromAddress.length - 4)),
          ]),
          h('i.fa.fa-arrow-right.fa-lg'),
          h('div.confirm-screen-account-wrapper', [
            h(
              Identicon,
              {
                address: txParams.to,
                diameter: 100,
              },
            ),
            h('span.confirm-screen-account-name', toName),
            h('span.confirm-screen-account-number', toAddress.slice(toAddress.length - 4)),
          ]),
        ]),

        h('h3.flex-center.confirm-screen-sending-to-message', {
          style: {
            textAlign: 'center',
            fontSize: '16px',
          },
        }, [
          `You're sending to Recipient ...${toAddress.slice(toAddress.length - 4)}`,
        ]),

        h('h3.flex-center.confirm-screen-send-amount', [`$${amountInUSD}`]),
        h('h3.flex-center.confirm-screen-send-amount-currency', [ 'USD' ]),
        h('div.flex-center.confirm-memo-wrapper', [
          h('h3.confirm-screen-send-memo', [ memo ]),
        ]),

        h('div.confirm-screen-rows', [
          h('section.flex-row.flex-center.confirm-screen-row', [
            h('span.confirm-screen-label.confirm-screen-section-column', [ 'From' ]),
            h('div.confirm-screen-section-column', [
              h('div.confirm-screen-row-info', fromName),
              h('div.confirm-screen-row-detail', `...${fromAddress.slice(fromAddress.length - 4)}`),
            ]),
          ]),

          h('section.flex-row.flex-center.confirm-screen-row', [
            h('span.confirm-screen-label.confirm-screen-section-column', [ 'To' ]),
            h('div.confirm-screen-section-column', [
              h('div.confirm-screen-row-info', toName),
              h('div.confirm-screen-row-detail', `...${toAddress.slice(toAddress.length - 4)}`),
            ]),
          ]),

          h('section.flex-row.flex-center.confirm-screen-row', [
            h('span.confirm-screen-label.confirm-screen-section-column', [ 'Gas Fee' ]),
            h('div.confirm-screen-section-column', [
              h('div.confirm-screen-row-info', `$${gasFeeInUSD} USD`),

              h('div.confirm-screen-row-detail', `${gasFeeInETH} ETH`),
            ]),
          ]),


          h('section.flex-row.flex-center.confirm-screen-total-box ', [
            h('div.confirm-screen-section-column', [
              h('span.confirm-screen-label', [ 'Total ' ]),
              h('div.confirm-screen-total-box__subtitle', [ 'Amount + Gas' ]),
            ]),

            h('div.confirm-screen-section-column', [
              h('div.confirm-screen-row-info', `$${totalInUSD} USD`),
              h('div.confirm-screen-row-detail', `${totalInETH} ETH`),
            ]),
          ]),
        ]),

// These are latest errors handling from master
// Leaving as comments as reference when we start implementing error handling
//         h('style', `
//           .conf-buttons button {
//             margin-left: 10px;
//             text-transform: uppercase;
//           }
//         `),

//         txMeta.simulationFails ?
//           h('.error', {
//             style: {
//               marginLeft: 50,
//               fontSize: '0.9em',
//             },
//           }, 'Transaction Error. Exception thrown in contract code.')
//         : null,

//         !isValidAddress ?
//           h('.error', {
//             style: {
//               marginLeft: 50,
//               fontSize: '0.9em',
//             },
//           }, 'Recipient address is invalid. Sending this transaction will result in a loss of ETH.')
//         : null,

//         insufficientBalance ?
//           h('span.error', {
//             style: {
//               marginLeft: 50,
//               fontSize: '0.9em',
//             },
//           }, 'Insufficient balance for transaction')
//         : null,

//         // send + cancel
//         h('.flex-row.flex-space-around.conf-buttons', {
//           style: {
//             display: 'flex',
//             justifyContent: 'flex-end',
//             margin: '14px 25px',
//           },
//         }, [
//           h('button', {
//             onClick: (event) => {
//               this.resetGasFields()
//               event.preventDefault()
//             },
//           }, 'Reset'),

//           // Accept Button or Buy Button
//           insufficientBalance ? h('button.btn-green', { onClick: props.buyEth }, 'Buy Ether') :
//             h('input.confirm.btn-green', {
//               type: 'submit',
//               value: 'SUBMIT',
//               style: { marginLeft: '10px' },
//               disabled: buyDisabled,
//             }),

//           h('button.cancel.btn-red', {
//             onClick: props.cancelTransaction,
//           }, 'Reject'),
//         ]),
//         showRejectAll ? h('.flex-row.flex-space-around.conf-buttons', {
//           style: {
//             display: 'flex',
//             justifyContent: 'flex-end',
//             margin: '14px 25px',
//           },
//         }, [
//           h('button.cancel.btn-red', {
//             onClick: props.cancelAllTransactions,
//           }, 'Reject All'),
//         ]) : null,
//       ]),
//     ])
//   )
// }
      ]),

      h('form#pending-tx-form.flex-column.flex-center', {
        onSubmit: this.onSubmit,
      }, [
        // Reset Button
        // h('button', {
        //   onClick: (event) => {
        //     this.resetGasFields()
        //     event.preventDefault()
        //   },
        // }, 'Reset'),

        // Accept Button
        h('button.confirm-screen-confirm-button', ['CONFIRM']),

        // Cancel Button
        h('div.cancel.btn-light.confirm-screen-cancel-button', {
          onClick: (event) => this.cancel(event, txMeta),
        }, 'CANCEL'),
      ]),
    ])
  )
}

// PendingTx.prototype.gasPriceChanged = function (newBN, valid) {
//   log.info(`Gas price changed to: ${newBN.toString(10)}`)
//   const txMeta = this.gatherTxMeta()
//   txMeta.txParams.gasPrice = '0x' + newBN.toString('hex')
//   this.setState({
//     txData: clone(txMeta),
//     valid,
//   })
// }

// PendingTx.prototype.gasLimitChanged = function (newBN, valid) {
//   log.info(`Gas limit changed to ${newBN.toString(10)}`)
//   const txMeta = this.gatherTxMeta()
//   txMeta.txParams.gas = '0x' + newBN.toString('hex')
//   this.setState({
//     txData: clone(txMeta),
//     valid,
//   })
// }

// PendingTx.prototype.resetGasFields = function () {
//   log.debug(`pending-tx resetGasFields`)

//   this.inputs.forEach((hexInput) => {
//     if (hexInput) {
//       hexInput.setValid()
//     }
//   })

//   this.setState({
//     txData: null,
//     valid: true,
//   })
// }

PendingTx.prototype.onSubmit = function (event) {
  event.preventDefault()
  const txMeta = this.gatherTxMeta()
  const valid = this.checkValidity()
  this.setState({ valid, submitting: true })

  if (valid && this.verifyGasParams()) {
    this.props.sendTransaction(txMeta, event)
  } else {
    this.props.dispatch(actions.displayWarning('Invalid Gas Parameters'))
    this.setState({ submitting: false })
  }
}

PendingTx.prototype.cancel = function (event, txMeta) {
  event.preventDefault()
  this.props.cancelTransaction(txMeta)
}

PendingTx.prototype.checkValidity = function () {
  const form = this.getFormEl()
  const valid = form.checkValidity()
  return valid
}

PendingTx.prototype.getFormEl = function () {
  const form = document.querySelector('form#pending-tx-form')
  // Stub out form for unit tests:
  if (!form) {
    return { checkValidity () { return true } }
  }
  return form
}

// After a customizable state value has been updated,
PendingTx.prototype.gatherTxMeta = function () {
  const props = this.props
  const state = this.state
  const txData = clone(state.txData) || clone(props.txData)

  // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
  return txData
}

PendingTx.prototype.verifyGasParams = function () {
  // We call this in case the gas has not been modified at all
  if (!this.state) { return true }
  return (
    this._notZeroOrEmptyString(this.state.gas) &&
    this._notZeroOrEmptyString(this.state.gasPrice)
  )
}

PendingTx.prototype._notZeroOrEmptyString = function (obj) {
  return obj !== '' && obj !== '0x0'
}

PendingTx.prototype.bnMultiplyByFraction = function (targetBN, numerator, denominator) {
  const numBN = new BN(numerator)
  const denomBN = new BN(denominator)
  return targetBN.mul(numBN).div(denomBN)
}