aboutsummaryrefslogblamecommitdiffstats
path: root/app/scripts/transaction-manager.js
blob: 6becfa6d1c95e935c58648dd9d314efde58a18c8 (plain) (tree)
1
2
3
4
5
6
7
8

                                      
                                          

                                            

                                                
                                                     



                                                                

                                    
                                             
                                                     
                                 

                                                            
                                                                    




                                                 
                                                   


                                            
                                                                                                                   
     






                        
                            
                                     
                                 
                                            





                                               

                                                                                                             
                             
     
                       
 
                            











                                                                      
                            
                                                

   
                                 

                                 
                                                          
                                   

   
    

                        
                                 
                                                              

                            

   
                            






                                                           























                                                                                                       

                                 

                                                                    




                        
                                        
                                
                                            

   
                                       
                                  
        

   
                                                          
                                  
















                                                                                 
   
 

                                                        
                                                    





                                                      

                                 

   














                                                 
                            







                                                                            


                                             
              
                                    



       




                                        
                                   
                        


   
                                                    
                            
                                     
                            

   
                                                      
                              
                                       
                            


                               
                                        




                                                    
                                 
                                                       
                         

   

                                                  
                        
                                                                                 
                                    



                                  

                                                                                
                                                                     

                                                                                            
         

                                         



         
 
                    















                                                            





                                               

 
 
                                                               
const EventEmitter = require('events')
const extend = require('xtend')
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const BN = ethUtil.BN
const TxProviderUtil = require('./lib/tx-utils')
const createId = require('./lib/random-id')
const normalize = require('./lib/sig-util').normalize

module.exports = class TransactionManager extends EventEmitter {
  constructor (opts) {
    super()
    this.txList = opts.txList || []
    this._setTxList = opts.setTxList
    this.txHistoryLimit = opts.txHistoryLimit
    this.getSelectedAccount = opts.getSelectedAccount
    this.provider = opts.provider
    this.blockTracker = opts.blockTracker
    this.txProviderUtils = new TxProviderUtil(this.provider)
    this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
    this.getGasMultiplier = opts.getGasMultiplier
    this.getNetwork = opts.getNetwork
  }

  getState () {
    var selectedAccount = this.getSelectedAccount()
    return {
      transactions: this.getTxList(),
      unconfTxs: this.getUnapprovedTxList(),
      selectedAccountTxList: this.getFilteredTxList({metamaskNetworkId: this.getNetwork(), from: selectedAccount}),
    }
  }

//   Returns the tx list
  getTxList () {
    return this.txList
  }

  // Adds a tx to the txlist
  addTx (txMeta, onTxDoneCb = warn) {
    var txList = this.getTxList()
    var txHistoryLimit = this.txHistoryLimit

    // checks if the length of th tx history is
    // longer then desired persistence limit
    // and then if it is removes only confirmed
    // or rejected tx's.
    // not tx's that are pending or unapproved
    if (txList.length > txHistoryLimit - 1) {
      var index = txList.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected')
      txList.splice(index, 1)
    }
    txList.push(txMeta)

    this._saveTxList(txList)
    // keep the onTxDoneCb around in a listener
    // for after approval/denial (requires user interaction)
    // This onTxDoneCb fires completion to the Dapp's write operation.
    this.once(`${txMeta.id}:signed`, function (txId) {
      this.removeAllListeners(`${txMeta.id}:rejected`)
      onTxDoneCb(null, true)
    })
    this.once(`${txMeta.id}:rejected`, function (txId) {
      this.removeAllListeners(`${txMeta.id}:signed`)
      onTxDoneCb(null, false)
    })

    this.emit('updateBadge')
    this.emit(`${txMeta.id}:unapproved`, txMeta)
  }

  // gets tx by Id and returns it
  getTx (txId, cb) {
    var txList = this.getTxList()
    var txMeta = txList.find(txData => txData.id === txId)
    return cb ? cb(txMeta) : txMeta
  }

  //
  updateTx (txMeta) {
    var txId = txMeta.id
    var txList = this.getTxList()
    var index = txList.findIndex(txData => txData.id === txId)
    txList[index] = txMeta
    this._saveTxList(txList)
  }

  get unapprovedTxCount () {
    return Object.keys(this.getUnapprovedTxList()).length
  }

  get pendingTxCount () {
    return this.getTxsByMetaData('status', 'signed').length
  }

  addUnapprovedTransaction (txParams, onTxDoneCb, cb) {
    // create txData obj with parameters and meta data
    var time = (new Date()).getTime()
    var txId = createId()
    txParams.metamaskId = txId
    txParams.metamaskNetworkId = this.getNetwork()
    var txData = {
      id: txId,
      txParams: txParams,
      time: time,
      status: 'unapproved',
      gasMultiplier: this.getGasMultiplier() || 1,
      metamaskNetworkId: this.getNetwork(),
    }
    this.txProviderUtils.analyzeGasUsage(txData, this.txDidComplete.bind(this, txData, onTxDoneCb, cb))
    // calculate metadata for tx
  }

  txDidComplete (txMeta, onTxDoneCb, cb, err) {
    if (err) return cb(err)
    this.addTx(txMeta, onTxDoneCb)
    cb(null, txMeta)
  }

  getUnapprovedTxList () {
    var txList = this.getTxList()
    return txList.filter((txMeta) => txMeta.status === 'unapproved')
    .reduce((result, tx) => {
      result[tx.id] = tx
      return result
    }, {})
  }

  approveTransaction (txId, cb = warn) {
    this.setTxStatusSigned(txId)
    this.once(`${txId}:signingComplete`, cb)
  }

  cancelTransaction (txId, cb = warn) {
    this.setTxStatusRejected(txId)
    cb()
  }

  // formats txParams so the keyringController can sign it
  formatTxForSigining (txParams) {
    var address = txParams.from
    var metaTx = this.getTx(txParams.metamaskId)
    var gasMultiplier = metaTx.gasMultiplier
    var gasPrice = new BN(ethUtil.stripHexPrefix(txParams.gasPrice), 16)
    gasPrice = gasPrice.mul(new BN(gasMultiplier * 100, 10)).div(new BN(100, 10))
    txParams.gasPrice = ethUtil.intToHex(gasPrice.toNumber())

    // normalize values
    txParams.to = normalize(txParams.to)
    txParams.from = normalize(txParams.from)
    txParams.value = normalize(txParams.value)
    txParams.data = normalize(txParams.data)
    txParams.gasLimit = normalize(txParams.gasLimit || txParams.gas)
    txParams.nonce = normalize(txParams.nonce)
    const ethTx = new Transaction(txParams)
    var txId = txParams.metamaskId
    return Promise.resolve({ethTx, address, txId})
  }

  // receives a signed tx object and updates the tx hash
  // and pass it to the cb to be sent off
  resolveSignedTransaction ({tx, txId, cb = warn}) {
    // Add the tx hash to the persisted meta-tx object
    var txHash = ethUtil.bufferToHex(tx.hash())
    var metaTx = this.getTx(txId)
    metaTx.hash = txHash
    this.updateTx(metaTx)
    var rawTx = ethUtil.bufferToHex(tx.serialize())
    return Promise.resolve(rawTx)

  }

  /*
  Takes an object of fields to search for eg:
  var thingsToLookFor = {
    to: '0x0..',
    from: '0x0..',
    status: 'signed',
  }
  and returns a list of tx with all
  options matching

  this is for things like filtering a the tx list
  for only tx's from 1 account
  or for filltering for all txs from one account
  and that have been 'confirmed'
  */
  getFilteredTxList (opts) {
    var filteredTxList
    Object.keys(opts).forEach((key) => {
      filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList)
    })
    return filteredTxList
  }

  getTxsByMetaData (key, value, txList = this.getTxList()) {
    return txList.filter((txMeta) => {
      if (key in txMeta.txParams) {
        return txMeta.txParams[key] === value
      } else {
        return txMeta[key] === value
      }
    })
  }

  // STATUS METHODS
  // get::set status

  // should return the status of the tx.
  getTxStatus (txId) {
    const txMeta = this.getTx(txId)
    return txMeta.status
  }


  // should update the status of the tx to 'signed'.
  setTxStatusSigned (txId) {
    this._setTxStatus(txId, 'signed')
    this.emit('updateBadge')
  }

  // should update the status of the tx to 'rejected'.
  setTxStatusRejected (txId) {
    this._setTxStatus(txId, 'rejected')
    this.emit('updateBadge')
  }

  setTxStatusConfirmed (txId) {
    this._setTxStatus(txId, 'confirmed')
  }

  // merges txParams obj onto txData.txParams
  // use extend to ensure that all fields are filled
  updateTxParams (txId, txParams) {
    var txMeta = this.getTx(txId)
    txMeta.txParams = extend(txMeta.txParams, txParams)
    this.updateTx(txMeta)
  }

  //  checks if a signed tx is in a block and
  // if included sets the tx status as 'confirmed'
  checkForTxInBlock () {
    var signedTxList = this.getFilteredTxList({status: 'signed', err: undefined})
    if (!signedTxList.length) return
    signedTxList.forEach((tx) => {
      var txHash = tx.hash
      var txId = tx.id
      if (!txHash) return
      this.txProviderUtils.query.getTransactionByHash(txHash, (err, txMeta) => {
        if (err || !txMeta) {
          tx.err = err || 'Tx could possibly have not been submitted'
          this.updateTx(tx)
          return txMeta ? console.error(err) : console.debug(`txMeta is ${txMeta} for:`, tx)
        }
        if (txMeta.blockNumber) {
          this.setTxStatusConfirmed(txId)
        }
      })
    })
  }

  // PRIVATE METHODS

  //  Should find the tx in the tx list and
  //  update it.
  //  should set the status in txData
  //    - `'unapproved'` the user has not responded
  //    - `'rejected'` the user has responded no!
  //    - `'signed'` the tx is signed
  //    - `'submitted'` the tx is sent to a server
  //    - `'confirmed'` the tx has been included in a block.
  _setTxStatus (txId, status) {
    var txMeta = this.getTx(txId)
    txMeta.status = status
    this.emit(`${txMeta.id}:${status}`, txId)
    this.updateTx(txMeta)
  }

  // Saves the new/updated txList.
  // Function is intended only for internal use
  _saveTxList (txList) {
    this.txList = txList
    this._setTxList(txList)
  }
}


const warn = () => console.warn('warn was used no cb provided')