aboutsummaryrefslogblamecommitdiffstats
path: root/app/scripts/keyring-controller.js
blob: f7a4e4010d46f94b10840dd494ee5cc551f9fb44 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                          
                     
                              
                                                   
                                            
                                        
                                               
                                           
                                                            
                                                       
                   
 

                                                  
                                          

                      
            
 
 
 
                                              
 






                                                          
                      
           

                                               
                                           
                                 
                              
                                    
                      
                                                   
 

                           
                                     

   










                                                                  
                 
















                                                                         
               
                                                                 
                                


                                                   
              







                                                             
                        
                                                     
                                                                           


                                                               


                                                     

       

   








                                                              
                                        
                                            

                                             

   








                                                                
                                             
                                       
                                                     


                                        
                                                      



                        
                                            

                                                
                       
                            
        

                             
                                       
      

                                      
                                                                                        
                                                       
                                         

                                         
                                                       
                                     

   



















                                                                         
                             
                                        

                              

                              

   










                                                                   
                              

                                                     



                                    


                                         
      

                                          

                    


      







                                                  

                                         

                                             
                                     

   






                                                                    
                                     












                                                         

   












                                                                    
                                                               
        


                              

   
 
                    
    

                                                   
 
                                         
                                                      
                                                 
                        
                                                        

      




























































                                                                        
                               
         



                                                          
                                                      



                                                           
                        
                              
                                        

                     




                 




                                                                     










                                                     
                                                                     
                        


                                  
                                      
                                                                                             
                                                       

                                         





























                                                                


                                                 
                                             










                                                                             




                                                                        
















                                                      


                                       













                                                                      
                                                        











                                                        
                                                      



                                                                


















                                                           
 



























                                                         



                                                                   





















                                                            
                                  
                                           
 






                                                       
                                                         







                                                                      
      

   















                                                                 





                                                             
                      



                                                         

   



                                                             
                    

                
                                                      








                                          

   

 
                                  
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const bip39 = require('bip39')
const EventEmitter = require('events').EventEmitter
const ObservableStore = require('obs-store')
const filter = require('promise-filter')
const encryptor = require('browser-passworder')
const createId = require('./lib/random-id')
const normalizeAddress = require('./lib/sig-util').normalize
const messageManager = require('./lib/message-manager')
function noop () {}

// Keyrings:
const SimpleKeyring = require('./keyrings/simple')
const HdKeyring = require('./keyrings/hd')
const keyringTypes = [
  SimpleKeyring,
  HdKeyring,
]


class KeyringController extends EventEmitter {

  // PUBLIC METHODS
  //
  // THE FIRST SECTION OF METHODS ARE PUBLIC-FACING,
  // MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS.
  //
  // THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE.

  constructor (opts) {
    super()
    const initState = opts.initState || {}
    this.store = new ObservableStore(initState)
    this.configManager = opts.configManager
    this.ethStore = opts.ethStore
    this.encryptor = encryptor
    this.keyringTypes = keyringTypes
    this.keyrings = []
    this.identities = {} // Essentially a name hash

    this._unconfMsgCbs = {}

    this.getNetwork = opts.getNetwork
  }

  // Full Update
  // returns Promise( @object state )
  //
  // Emits the `update` event and
  // returns a Promise that resolves to the current state.
  //
  // Frequently used to end asynchronous chains in this class,
  // indicating consumers can often either listen for updates,
  // or accept a state-resolving promise to consume their results.
  //
  // Not all methods end with this, that might be a nice refactor.
  fullUpdate () {
    this.emit('update')
    return Promise.resolve(this.getState())
  }

  // Get State
  // returns @object state
  //
  // This method returns a hash representing the current state
  // that the keyringController manages.
  //
  // It is extended in the MetamaskController along with the EthStore
  // state, and its own state, to create the metamask state branch
  // that is passed to the UI.
  //
  // This is currently a rare example of a synchronously resolving method
  // in this class, but will need to be Promisified when we move our
  // persistence to an async model.
  getState () {
    return Promise.all(this.keyrings.map(this.displayForKeyring))
    .then((displayKeyrings) => {
      const state = this.store.getState()
      // old wallet
      const wallet = this.configManager.getWallet()
      return {
        // computed
        isInitialized: (!!wallet || !!state.vault),
        isUnlocked: (!!this.password),
        keyrings: displayKeyrings,
        // hard coded
        keyringTypes: this.keyringTypes.map(krt => krt.type),
        // memStore
        identities: this.identities,
        // configManager
        seedWords: this.configManager.getSeedWords(),
        isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(),
        currentFiat: this.configManager.getCurrentFiat(),
        conversionRate: this.configManager.getConversionRate(),
        conversionDate: this.configManager.getConversionDate(),
        // messageManager
        unconfMsgs: messageManager.unconfirmedMsgs(),
        messages: messageManager.getMsgList(),
      }
    })
  }

  // Create New Vault And Keychain
  // @string password - The password to encrypt the vault with
  //
  // returns Promise( @object state )
  //
  // Destroys any old encrypted storage,
  // creates a new encrypted store with the given password,
  // randomly creates a new HD wallet with 1 account,
  // faucets that account on the testnet.
  createNewVaultAndKeychain (password) {
    return this.persistAllKeyrings(password)
    .then(this.createFirstKeyTree.bind(this))
    .then(this.fullUpdate.bind(this))
  }

  // CreateNewVaultAndRestore
  // @string password - The password to encrypt the vault with
  // @string seed - The BIP44-compliant seed phrase.
  //
  // returns Promise( @object state )
  //
  // Destroys any old encrypted storage,
  // creates a new encrypted store with the given password,
  // creates a new HD wallet from the given seed with 1 account.
  createNewVaultAndRestore (password, seed) {
    if (typeof password !== 'string') {
      return Promise.reject('Password must be text.')
    }

    if (!bip39.validateMnemonic(seed)) {
      return Promise.reject('Seed phrase is invalid.')
    }

    this.clearKeyrings()

    return this.persistAllKeyrings(password)
    .then(() => {
      return this.addNewKeyring('HD Key Tree', {
        mnemonic: seed,
        numberOfAccounts: 1,
      })
    })
    .then((firstKeyring) => {
      return firstKeyring.getAccounts()
    })
    .then((accounts) => {
      const firstAccount = accounts[0]
      if (!firstAccount) throw new Error('KeyringController - First Account not found.')
      const hexAccount = normalizeAddress(firstAccount)
      this.emit('newAccount', hexAccount)
      return this.setupAccounts(accounts)
    })
    .then(this.persistAllKeyrings.bind(this, password))
    .then(this.fullUpdate.bind(this))
  }

  // Set Locked
  // returns Promise( @object state )
  //
  // This method deallocates all secrets, and effectively locks metamask.
  setLocked () {
    this.password = null
    this.keyrings = []
    return this.fullUpdate()
  }

  // Submit Password
  // @string password
  //
  // returns Promise( @object state )
  //
  // Attempts to decrypt the current vault and load its keyrings
  // into memory.
  //
  // Temporarily also migrates any old-style vaults first, as well.
  // (Pre MetaMask 3.0.0)
  submitPassword (password) {
    return this.unlockKeyrings(password)
    .then((keyrings) => {
      this.keyrings = keyrings
      return this.fullUpdate()
    })
  }

  // Add New Keyring
  // @string type
  // @object opts
  //
  // returns Promise( @Keyring keyring )
  //
  // Adds a new Keyring of the given `type` to the vault
  // and the current decrypted Keyrings array.
  //
  // All Keyring classes implement a unique `type` string,
  // and this is used to retrieve them from the keyringTypes array.
  addNewKeyring (type, opts) {
    const Keyring = this.getKeyringClassForType(type)
    const keyring = new Keyring(opts)
    return keyring.deserialize(opts)
    .then(() => {
      return keyring.getAccounts()
    })
    .then((accounts) => {
      this.keyrings.push(keyring)
      return this.setupAccounts(accounts)
    })
    .then(() => this.persistAllKeyrings())
    .then(() => this.fullUpdate())
    .then(() => {
      return keyring
    })
  }

  // Add New Account
  // @number keyRingNum
  //
  // returns Promise( @object state )
  //
  // Calls the `addAccounts` method on the Keyring
  // in the kryings array at index `keyringNum`,
  // and then saves those changes.
  addNewAccount (selectedKeyring) {
    return selectedKeyring.addAccounts(1)
    .then(this.setupAccounts.bind(this))
    .then(this.persistAllKeyrings.bind(this))
    .then(this.fullUpdate.bind(this))
  }

  // Save Account Label
  // @string account
  // @string label
  //
  // returns Promise( @string label )
  //
  // Persists a nickname equal to `label` for the specified account.
  saveAccountLabel (account, label) {
    try {
      const hexAddress = normalizeAddress(account)
      // update state on diskStore
      const state = this.store.getState()
      const walletNicknames = state.walletNicknames || {}
      walletNicknames[hexAddress] = label
      this.store.updateState({ walletNicknames })
      // update state on memStore
      this.identities[hexAddress].name = label
      return Promise.resolve(label)
    } catch (err) {
      return Promise.reject(err)
    }
  }

  // Export Account
  // @string address
  //
  // returns Promise( @string privateKey )
  //
  // Requests the private key from the keyring controlling
  // the specified address.
  //
  // Returns a Promise that may resolve with the private key string.
  exportAccount (address) {
    try {
      return this.getKeyringForAccount(address)
      .then((keyring) => {
        return keyring.exportAccount(normalizeAddress(address))
      })
    } catch (e) {
      return Promise.reject(e)
    }
  }


  // SIGNING METHODS
  //
  // This method signs tx and returns a promise for
  // TX Manager to update the state after signing

  signTransaction (ethTx, _fromAddress) {
    const fromAddress = normalizeAddress(_fromAddress)
    return this.getKeyringForAccount(fromAddress)
    .then((keyring) => {
      return keyring.signTransaction(fromAddress, ethTx)
    })
  }
  // Add Unconfirmed Message
  // @object msgParams
  // @function cb
  //
  // Does not call back, only emits an `update` event.
  //
  // Adds the given `msgParams` and `cb` to a local cache,
  // for displaying to a user for approval before signing or canceling.
  addUnconfirmedMessage (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.emit('update')
    return msgId
  }

  // Cancel Message
  // @string msgId
  // @function cb (optional)
  //
  // Calls back to cached `unconfMsgCb`.
  // Calls back to `cb` if provided.
  //
  // Forgets any messages matching `msgId`.
  cancelMessage (msgId, cb) {
    var approvalCb = this._unconfMsgCbs[msgId] || noop

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

    if (cb && typeof cb === 'function') {
      cb()
    }
  }

  // Sign Message
  // @object msgParams
  // @function cb
  //
  // returns Promise(@buffer rawSig)
  // calls back @function cb with @buffer rawSig
  // calls back cached Dapp's @function unconfMsgCb.
  //
  // Attempts to sign the provided @object msgParams.
  signMessage (msgParams, cb) {
    try {
      const msgId = msgParams.metamaskId
      delete msgParams.metamaskId
      const approvalCb = this._unconfMsgCbs[msgId] || noop

      const address = normalizeAddress(msgParams.from)
      return this.getKeyringForAccount(address)
      .then((keyring) => {
        return keyring.signMessage(address, msgParams.data)
      }).then((rawSig) => {
        cb(null, rawSig)
        approvalCb(null, true)
        messageManager.confirmMsg(msgId)
        return rawSig
      })
    } catch (e) {
      cb(e)
    }
  }

  // PRIVATE METHODS
  //
  // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER
  // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS.

  // Create First Key Tree
  // returns @Promise
  //
  // Clears the vault,
  // creates a new one,
  // creates a random new HD Keyring with 1 account,
  // makes that account the selected account,
  // faucets that account on testnet,
  // puts the current seed words into the state tree.
  createFirstKeyTree () {
    this.clearKeyrings()
    return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
    .then((keyring) => {
      return keyring.getAccounts()
    })
    .then((accounts) => {
      const firstAccount = accounts[0]
      if (!firstAccount) throw new Error('KeyringController - No account found on keychain.')
      const hexAccount = normalizeAddress(firstAccount)
      this.emit('newAccount', hexAccount)
      return this.setupAccounts(accounts)
    })
    .then(this.persistAllKeyrings.bind(this))
  }

  // Setup Accounts
  // @array accounts
  //
  // returns @Promise(@object account)
  //
  // Initializes the provided account array
  // Gives them numerically incremented nicknames,
  // and adds them to the ethStore for regular balance checking.
  setupAccounts (accounts) {
    return this.getAccounts()
    .then((loadedAccounts) => {
      const arr = accounts || loadedAccounts
      return Promise.all(arr.map((account) => {
        return this.getBalanceAndNickname(account)
      }))
    })
  }

  // Get Balance And Nickname
  // @string account
  //
  // returns Promise( @string label )
  //
  // Takes an account address and an iterator representing
  // the current number of named accounts.
  getBalanceAndNickname (account) {
    if (!account) {
      throw new Error('Problem loading account.')
    }
    const address = normalizeAddress(account)
    this.ethStore.addAccount(address)
    return this.createNickname(address)
  }

  // Create Nickname
  // @string address
  //
  // returns Promise( @string label )
  //
  // Takes an address, and assigns it an incremented nickname, persisting it.
  createNickname (address) {
    const hexAddress = normalizeAddress(address)
    const currentIdentityCount = Object.keys(this.identities).length + 1
    const nicknames = this.store.getState().walletNicknames || {}
    const existingNickname = nicknames[hexAddress]
    const name = existingNickname || `Account ${currentIdentityCount}`
    this.identities[hexAddress] = {
      address: hexAddress,
      name,
    }
    return this.saveAccountLabel(hexAddress, name)
  }

  // Persist All Keyrings
  // @password string
  //
  // returns Promise
  //
  // Iterates the current `keyrings` array,
  // serializes each one into a serialized array,
  // encrypts that array with the provided `password`,
  // and persists that encrypted string to storage.
  persistAllKeyrings (password = this.password) {
    if (typeof password === 'string') {
      this.password = password
    }
    return Promise.all(this.keyrings.map((keyring) => {
      return Promise.all([keyring.type, keyring.serialize()])
      .then((serializedKeyringArray) => {
        // Label the output values on each serialized Keyring:
        return {
          type: serializedKeyringArray[0],
          data: serializedKeyringArray[1],
        }
      })
    }))
    .then((serializedKeyrings) => {
      return this.encryptor.encrypt(this.password, serializedKeyrings)
    })
    .then((encryptedString) => {
      this.store.updateState({ vault: encryptedString })
      return true
    })
  }

  // Unlock Keyrings
  // @string password
  //
  // returns Promise( @array keyrings )
  //
  // Attempts to unlock the persisted encrypted storage,
  // initializing the persisted keyrings to RAM.
  unlockKeyrings (password) {
    const encryptedVault = this.store.getState().vault
    if (!encryptedVault) {
      throw new Error('Cannot unlock without a previous vault.')
    }

    return this.encryptor.decrypt(password, encryptedVault)
    .then((vault) => {
      this.password = password
      vault.forEach(this.restoreKeyring.bind(this))
      return this.keyrings
    })
  }

  // Restore Keyring
  // @object serialized
  //
  // returns Promise( @Keyring deserialized )
  //
  // Attempts to initialize a new keyring from the provided
  // serialized payload.
  //
  // On success, returns the resulting @Keyring instance.
  restoreKeyring (serialized) {
    const { type, data } = serialized

    const Keyring = this.getKeyringClassForType(type)
    const keyring = new Keyring()
    return keyring.deserialize(data)
    .then(() => {
      return keyring.getAccounts()
    })
    .then((accounts) => {
      return this.setupAccounts(accounts)
    })
    .then(() => {
      this.keyrings.push(keyring)
      return keyring
    })
  }

  // Get Keyring Class For Type
  // @string type
  //
  // Returns @class Keyring
  //
  // Searches the current `keyringTypes` array
  // for a Keyring class whose unique `type` property
  // matches the provided `type`,
  // returning it if it exists.
  getKeyringClassForType (type) {
    return this.keyringTypes.find(kr => kr.type === type)
  }

  getKeyringsByType (type) {
    return this.keyrings.filter((keyring) => keyring.type === type)
  }

  // Get Accounts
  // returns Promise( @Array[ @string accounts ] )
  //
  // Returns the public addresses of all current accounts
  // managed by all currently unlocked keyrings.
  getAccounts () {
    const keyrings = this.keyrings || []
    return Promise.all(keyrings.map(kr => kr.getAccounts()))
    .then((keyringArrays) => {
      return keyringArrays.reduce((res, arr) => {
        return res.concat(arr)
      }, [])
    })
  }

  // Get Keyring For Account
  // @string address
  //
  // returns Promise(@Keyring keyring)
  //
  // Returns the currently initialized keyring that manages
  // the specified `address` if one exists.
  getKeyringForAccount (address) {
    const hexed = normalizeAddress(address)

    return Promise.all(this.keyrings.map((keyring) => {
      return Promise.all([
        keyring,
        keyring.getAccounts(),
      ])
    }))
    .then(filter((candidate) => {
      const accounts = candidate[1].map(normalizeAddress)
      return accounts.includes(hexed)
    }))
    .then((winners) => {
      if (winners && winners.length > 0) {
        return winners[0][0]
      } else {
        throw new Error('No keyring found for the requested account.')
      }
    })
  }

  // Display For Keyring
  // @Keyring keyring
  //
  // returns Promise( @Object { type:String, accounts:Array } )
  //
  // Is used for adding the current keyrings to the state object.
  displayForKeyring (keyring) {
    return keyring.getAccounts()
    .then((accounts) => {
      return {
        type: keyring.type,
        accounts: accounts,
      }
    })
  }

  // Add Gas Buffer
  // @string gas (as hexadecimal value)
  //
  // returns @string bufferedGas (as hexadecimal value)
  //
  // Adds a healthy buffer of gas to an initial gas estimate.
  addGasBuffer (gas) {
    const gasBuffer = new BN('100000', 10)
    const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16)
    const correct = bnGas.add(gasBuffer)
    return ethUtil.addHexPrefix(correct.toString(16))
  }

  // Clear Keyrings
  //
  // Deallocates all currently managed keyrings and accounts.
  // Used before initializing a new vault.
  clearKeyrings () {
    let accounts
    try {
      accounts = Object.keys(this.ethStore.getState())
    } catch (e) {
      accounts = []
    }
    accounts.forEach((address) => {
      this.ethStore.removeAccount(address)
    })

    this.keyrings = []
    this.identities = {}
  }

}

module.exports = KeyringController