aboutsummaryrefslogblamecommitdiffstats
path: root/app/scripts/lib/idStore.js
blob: b8d825d8bf8103b97b1ebd1a0ca18b206b5b1bd3 (plain) (tree)
1
2
3
4
5
6
7
8
9


                                                   

                                                               



                                                               
                                           
                                                           
                                                   
                                                  





                                     
                                   
                         

                                                  
                                
                          
                       
                        


                                      
 
                        

                          
   
 
                                                               
                        
                         





         

                                                                         
                             
                                                        
                           
 

                          
                      


                                          

                       

 
                                                                       
                                                     
                           


                          
                             

    
 
                                                   
                        

 
                                                           
                                       
                                              
 
 
                                              
                                          
                                        
                                           
                                                             
                                   
                         
                                              
                                            

                                                 
                                                        


     







                                                        
                                                        
                                           


                                                               




                                        
                                           
                   

 









                                                      
                                                 

                       
      


                                                                
                                        
                           
                                          

                                                


    




                                                               



               

                                                                           
 


                                                    
                            
                                                         





                          
                             
                                                   
 
                                                                      
                                                            
                              
 
                  
                   

             

 

                                                                
                                        
                                                  
 
              
      

                        
                               

                                

 
                         
                                                           
                                        
                                                  



                         
                              

                                

 

                                                                 
       
                                          
                                             
                   




                 
  














                                                                        
                                













                                                                      
                                            





                                                    
                                  





                                                        
                                           




                                                    
                                 








                                                                     







                                    





                 


          
                                                
                                      


                                                 
                                                               


               
                                  
                                                     
                                                          


                                      
                         
                                      

                        
                              

                                                            
                                              
     
                                                     
    
                   

 














                                                       




                                                              
                                              

 
                                                                              
                     
                                                                            
                           
                                                      
 
               




                                                                    
 
                                              
                                    
                                                                    
                                                              
                                                                              

                     
            
                                                             
     




                                     
                                      

      

        

 
                                                                                 
                                                                             



                                                                                                     

                                                      
                 

 

                                                                            
                                                                                   



                                                                                                     

                                               


                 









                                                                                                 


                             

                                   
                                      

                                  
                                                                                                 










                                                                               
 
              
                                                         

                                              
 
                                                      
                                               
                                                         
                        

                                  


                                                   

   









                                                                                


                                                 

                                             

                                                                                                                      
   


 

       
                 









                                                             
 
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
const Transaction = require('ethereumjs-tx')
const LightwalletKeyStore = require('eth-lightwallet').keystore
const LightwalletSigner = require('eth-lightwallet').signing
const async = require('async')
const clone = require('clone')
const extend = require('xtend')
const createId = require('web3-provider-engine/util/random-id')
const autoFaucet = require('./auto-faucet')
const configManager = require('./config-manager-singleton')
const messageManager = require('./message-manager')
const DEFAULT_RPC = 'https://testrpc.metamask.io/'


module.exports = IdentityStore


inherits(IdentityStore, EventEmitter)
function IdentityStore(opts = {}) {
  EventEmitter.call(this)

  // we just use the ethStore to auto-add accounts
  this._ethStore = opts.ethStore
  // lightwallet key store
  this._keyStore = null
  // lightwallet wrapper
  this._idmgmt = null

  this.hdPathString = "m/44'/60'/0'/0"

  this._currentState = {
    selectedAddress: null,
    identities: {},
  }

  // not part of serilized metamask state - only kept in memory
  this._unconfTxCbs = {}
  this._unconfMsgCbs = {}
}

//
// public
//

IdentityStore.prototype.createNewVault = function(password, entropy, cb){
  delete this._keyStore
  configManager.clearWallet()
  this._createIdmgmt(password, null, entropy, (err) => {
    if (err) return cb(err)

    this._loadIdentities()
    this._didUpdate()
    this._autoFaucet()

    configManager.setShowSeedWords(true)
    var seedWords = this._idmgmt.getSeed()
    cb(null, seedWords)
  })
}

IdentityStore.prototype.recoverFromSeed = function(password, seed, cb){
  this._createIdmgmt(password, seed, null, (err) => {
    if (err) return cb(err)

    this._loadIdentities()
    this._didUpdate()
    cb(null, this.getState())
  })
}

IdentityStore.prototype.setStore = function(store){
  this._ethStore = store
}

IdentityStore.prototype.clearSeedWordCache = function(cb) {
  configManager.setShowSeedWords(false)
  cb(null, configManager.getSelectedAccount())
}

IdentityStore.prototype.getState = function(){
  var seedWords = this.getSeedIfUnlocked()
  var wallet = configManager.getWallet()
  return clone(extend(this._currentState, {
    isInitialized: !!configManager.getWallet() && !seedWords,
    isUnlocked: this._isUnlocked(),
    seedWords: seedWords,
    unconfTxs: configManager.unconfirmedTxs(),
    transactions: configManager.getTxList(),
    unconfMsgs: messageManager.unconfirmedMsgs(),
    messages: messageManager.getMsgList(),
    selectedAddress: configManager.getSelectedAccount(),
  }))
}

IdentityStore.prototype.getSeedIfUnlocked = function() {
  var showSeed = configManager.getShouldShowSeedWords()
  var idmgmt = this._idmgmt
  var shouldShow = showSeed && !!idmgmt
  var seedWords = shouldShow ? idmgmt.getSeed() : null
  return seedWords
}

IdentityStore.prototype.getSelectedAddress = function(){
  return configManager.getSelectedAccount()
}

IdentityStore.prototype.setSelectedAddress = function(address){
  if (!address) {
    var addresses = this._getAddresses()
    address = addresses[0]
  }

  configManager.setSelectedAccount(address)
  this._didUpdate()
}

IdentityStore.prototype.getNetwork = function(tries) {
  if (tries === 0) return
  this.web3.version.getNetwork((err, network) => {
    if (err) {
      return this.getNetwork(tries - 1, cb)
    }
    this._currentState.network = network
  })
}

IdentityStore.prototype.setLocked = function(cb){
  delete this._keyStore
  delete this._idmgmt
  cb()
}

IdentityStore.prototype.submitPassword = function(password, cb){
  this._tryPassword(password, (err) => {
    if (err) return cb(err)
    // load identities before returning...
    this._loadIdentities()
    cb(null, configManager.getSelectedAccount())
  })
}

IdentityStore.prototype.exportAccount = function(address, cb) {
  var privateKey = this._idmgmt.exportPrivateKey(address)
  cb(null, privateKey)
}

//
// Transactions
//

// comes from dapp via zero-client hooked-wallet provider
IdentityStore.prototype.addUnconfirmedTransaction = function(txParams, cb){

  // create txData obj with parameters and meta data
  var time = (new Date()).getTime()
  var txId = createId()
  txParams.metamaskId = txId
  txParams.metamaskNetworkId = this._currentState.network
  var txData = {
    id: txId,
    txParams: txParams,
    time: time,
    status: 'unconfirmed',
  }
  configManager.addTx(txData)
  console.log('addUnconfirmedTransaction:', txData)

  // keep the cb around for after approval (requires user interaction)
  // This cb fires completion to the Dapp's write operation.
  this._unconfTxCbs[txId] = cb

  // signal update
  this._didUpdate()

  return txId
}

// comes from metamask ui
IdentityStore.prototype.approveTransaction = function(txId, cb){
  var txData = configManager.getTx(txId)
  var approvalCb = this._unconfTxCbs[txId] || noop

  // accept tx
  cb()
  approvalCb(null, true)
  // clean up
  configManager.confirmTx(txId)
  delete this._unconfTxCbs[txId]
  this._didUpdate()
}

// comes from metamask ui
IdentityStore.prototype.cancelTransaction = function(txId){
  var txData = configManager.getTx(txId)
  var approvalCb = this._unconfTxCbs[txId] || noop

  // reject tx
  approvalCb(null, false)
  // clean up
  configManager.rejectTx(txId)
  delete this._unconfTxCbs[txId]
  this._didUpdate()
}

// performs the actual signing, no autofill of params
IdentityStore.prototype.signTransaction = function(txParams, cb){
  try {
    console.log('signing tx...', txParams)
    var rawTx = this._idmgmt.signTx(txParams)
    cb(null, rawTx)
  } catch (err) {
    cb(err)
  }
}

//
// Messages
//

// comes from dapp via zero-client hooked-wallet provider
IdentityStore.prototype.addUnconfirmedMessage = function(msgParams, cb){

  // create txData obj with parameters and meta data
  var time = (new Date()).getTime()
  var msgId = createId()
  var msgData = {
    id: msgId,
    msgParams: msgParams,
    time: time,
    status: 'unconfirmed',
  }
  messageManager.addMsg(msgData)
  console.log('addUnconfirmedMessage:', msgData)

  // keep the cb around for after approval (requires user interaction)
  // This cb fires completion to the Dapp's write operation.
  this._unconfMsgCbs[msgId] = cb

  // signal update
  this._didUpdate()

  return msgId
}

// comes from metamask ui
IdentityStore.prototype.approveMessage = function(msgId, cb){
  var msgData = messageManager.getMsg(msgId)
  var approvalCb = this._unconfMsgCbs[msgId] || noop

  // accept msg
  cb()
  approvalCb(null, true)
  // clean up
  messageManager.confirmMsg(msgId)
  delete this._unconfMsgCbs[msgId]
  this._didUpdate()
}

// comes from metamask ui
IdentityStore.prototype.cancelMessage = function(msgId){
  var txData = messageManager.getMsg(msgId)
  var approvalCb = this._unconfMsgCbs[msgId] || noop

  // reject tx
  approvalCb(null, false)
  // clean up
  messageManager.rejectMsg(msgId)
  delete this._unconfTxCbs[msgId]
  this._didUpdate()
}

// performs the actual signing, no autofill of params
IdentityStore.prototype.signMessage = function(msgParams, cb){
  try {
    console.log('signing msg...', msgParams.data)
    var rawMsg = this._idmgmt.signMsg(msgParams.from, msgParams.data)
    if ('metamaskId' in msgParams) {
      var id = msgParams.metamaskId
      delete msgParams.metamaskId

      this.approveMessage(id, cb)
    } else {
      cb(null, rawMsg)
    }
  } catch (err) {
    cb(err)
  }
}

//
// private
//

IdentityStore.prototype._didUpdate = function(){
  this.emit('update', this.getState())
}

IdentityStore.prototype._isUnlocked = function(){
  var result = Boolean(this._keyStore) && Boolean(this._idmgmt)
  return result
}

// load identities from keyStoreet
IdentityStore.prototype._loadIdentities = function(){
  if (!this._isUnlocked()) throw new Error('not unlocked')

  var addresses = this._getAddresses()
  addresses.forEach((address, i) => {
    // // add to ethStore
    this._ethStore.addAccount(address)
    // add to identities
    var identity = {
      name: 'Wallet ' + (i+1),
      img: 'QmW6hcwYzXrNkuHrpvo58YeZvbZxUddv69ATSHY3BHpPdd',
      address: address,
      mayBeFauceting: this._mayBeFauceting(i),
    }
    this._currentState.identities[address] = identity
  })
  this._didUpdate()
}

// mayBeFauceting
// If on testnet, index 0 may be fauceting.
// The UI will have to check the balance to know.
// If there is no balance and it mayBeFauceting,
// then it is in fact fauceting.
IdentityStore.prototype._mayBeFauceting = function(i) {
  var config = configManager.getProvider()
  if (i === 0 &&
      config.type === 'rpc' &&
      config.rpcTarget === DEFAULT_RPC) {
    return true
  }
  return false
}

//
// keyStore managment - unlocking + deserialization
//

IdentityStore.prototype._tryPassword = function(password, cb){
  this._createIdmgmt(password, null, null, cb)
}

IdentityStore.prototype._createIdmgmt = function(password, seed, entropy, cb){
  var keyStore = null
  LightwalletKeyStore.deriveKeyFromPassword(password, (err, derivedKey) => {
    if (err) return cb(err)
    var serializedKeystore = configManager.getWallet()

    if (seed) {
      try {
        keyStore = this._restoreFromSeed(password, seed, derivedKey)
      } catch (e) {
        return cb(e)
      }

    // returning user, recovering from storage
    } else if (serializedKeystore) {
      keyStore = LightwalletKeyStore.deserialize(serializedKeystore)
      var isCorrect = keyStore.isDerivedKeyCorrect(derivedKey)
      if (!isCorrect) return cb(new Error('Lightwallet - password incorrect'))

   // first time here
    } else {
      keyStore = this._createFirstWallet(entropy, derivedKey)
    }

    this._keyStore = keyStore
    this._idmgmt = new IdManagement({
      keyStore: keyStore,
      derivedKey: derivedKey,
      hdPathSTring: this.hdPathString,
    })

    cb()
  })
}

IdentityStore.prototype._restoreFromSeed = function(password, seed, derivedKey) {
  var keyStore = new LightwalletKeyStore(seed, derivedKey, this.hdPathString)
  keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'});
  keyStore.setDefaultHdDerivationPath(this.hdPathString)

  keyStore.generateNewAddress(derivedKey, 3)
  configManager.setWallet(keyStore.serialize())
  console.log('restored from seed. saved to keystore')
  return keyStore
}

IdentityStore.prototype._createFirstWallet = function(entropy, derivedKey) {
  var secretSeed = LightwalletKeyStore.generateRandomSeed(entropy)
  var keyStore = new LightwalletKeyStore(secretSeed, derivedKey, this.hdPathString)
  keyStore.addHdDerivationPath(this.hdPathString, derivedKey, {curve: 'secp256k1', purpose: 'sign'});
  keyStore.setDefaultHdDerivationPath(this.hdPathString)

  keyStore.generateNewAddress(derivedKey, 3)
  configManager.setWallet(keyStore.serialize())
  console.log('saved to keystore')
  return keyStore
}

// get addresses and normalize address hexString
IdentityStore.prototype._getAddresses = function() {
  return this._keyStore.getAddresses(this.hdPathString).map((address) => { return '0x'+address })
}

IdentityStore.prototype._autoFaucet = function() {
  var addresses = this._getAddresses()
  autoFaucet(addresses[0])
}

function IdManagement(opts) {
  if (!opts) opts = {}

  this.keyStore = opts.keyStore
  this.derivedKey = opts.derivedKey
  this.hdPathString = "m/44'/60'/0'/0"

  this.getAddresses =  function(){
    return keyStore.getAddresses(this.hdPathString).map(function(address){ return '0x'+address })
  }

  this.signTx = function(txParams){
    // normalize values
    txParams.to = ethUtil.addHexPrefix(txParams.to)
    txParams.from = ethUtil.addHexPrefix(txParams.from)
    txParams.value = ethUtil.addHexPrefix(txParams.value)
    txParams.data = ethUtil.addHexPrefix(txParams.data)
    txParams.gasLimit = ethUtil.addHexPrefix(txParams.gasLimit || txParams.gas)
    txParams.nonce = ethUtil.addHexPrefix(txParams.nonce)
    var tx = new Transaction(txParams)

    // sign tx
    var privKeyHex = this.exportPrivateKey(txParams.from)
    var privKey = ethUtil.toBuffer(privKeyHex)
    tx.sign(privKey)

    // Add the tx hash to the persisted meta-tx object
    var txHash = ethUtil.bufferToHex(tx.hash())
    var metaTx = configManager.getTx(txParams.metamaskId)
    metaTx.hash = txHash
    configManager.updateTx(metaTx)

    // return raw serialized tx
    var rawTx = ethUtil.bufferToHex(tx.serialize())
    return rawTx
  }

  this.signMsg = function(address, message){
    // sign message
    var privKeyHex = this.exportPrivateKey(address)
    var privKey = ethUtil.toBuffer(privKeyHex)
    var msgHash = ethUtil.sha3(message)
    var msgSig = ethUtil.ecsign(msgHash, privKey)
    var rawMsgSig = ethUtil.bufferToHex(concatSig(msgSig.v, msgSig.r, msgSig.s))
    return rawMsgSig
  }

  this.getSeed = function(){
    return this.keyStore.getSeed(this.derivedKey)
  }

  this.exportPrivateKey = function(address) {
    var privKeyHex = ethUtil.addHexPrefix(this.keyStore.exportPrivateKey(address, this.derivedKey, this.hdPathString))
    return privKeyHex
  }
}


// util

function noop(){}


function concatSig(v, r, s) {
  r = ethUtil.fromSigned(r)
  s = ethUtil.fromSigned(s)
  v = ethUtil.bufferToInt(v)
  r = ethUtil.toUnsigned(r).toString('hex')
  s = ethUtil.toUnsigned(s).toString('hex')
  v = ethUtil.stripHexPrefix(ethUtil.intToHex(v))
  return ethUtil.addHexPrefix(r.concat(s, v).toString("hex"))
}