From b81f00849d53ee780bf26d4d6f79aef4ebdba34c Mon Sep 17 00:00:00 2001 From: Dan Finlay Date: Tue, 29 Nov 2016 11:40:49 -0800 Subject: Annotated KeyringController --- app/scripts/keyring-controller.js | 715 ++++++++++++++++++++++++++------------ 1 file changed, 496 insertions(+), 219 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index 5e6b0acbb..7d29fb5d8 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -25,6 +25,13 @@ const createId = require('./lib/random-id') module.exports = 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() this.configManager = opts.configManager @@ -46,6 +53,46 @@ module.exports = class KeyringController extends EventEmitter { }) } + // Set Store + // + // Allows setting the ethStore after the constructor. + // This is currently required because of the initialization order + // of the ethStore and this class. + // + // Eventually would be nice to be able to add this in the constructor. + setStore (ethStore) { + this.ethStore = ethStore + } + + // 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 () { const configManager = this.configManager const address = configManager.getSelectedAccount() @@ -71,16 +118,30 @@ module.exports = class KeyringController extends EventEmitter { } } - setStore (ethStore) { - this.ethStore = ethStore - } - + // 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.createNewVault(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.') @@ -92,7 +153,7 @@ module.exports = class KeyringController extends EventEmitter { this.clearKeyrings() - return this.createNewVault(password) + return this.persistAllKeyrings(password) .then(() => { return this.addNewKeyring('HD Key Tree', { mnemonic: seed, @@ -112,65 +173,54 @@ module.exports = class KeyringController extends EventEmitter { .then(this.fullUpdate.bind(this)) } - migrateOldVaultIfAny (password) { - const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault() - return this.idStoreMigrator.migratedVaultForPassword(password) - .then((serialized) => { - this.password = password - - if (serialized && shouldMigrate) { - return this.restoreKeyring(serialized) - .then(keyring => keyring.getAccounts()) - .then((accounts) => { - this.configManager.setSelectedAccount(accounts[0]) - return this.persistAllKeyrings() - }) - } else { - return Promise.resolve() - } - }) - } - - createNewVault (password) { - return this.migrateOldVaultIfAny(password) - .then(() => { - this.password = password - return this.persistAllKeyrings() - }) - .then(() => { - return password - }) - } - - createFirstKeyTree (password) { - this.clearKeyrings() - return this.addNewKeyring('HD Key Tree', {numberOfAccounts: 1}) - .then(() => { - return this.keyrings[0].getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - const hexAccount = normalize(firstAccount) - this.configManager.setSelectedAccount(hexAccount) - this.emit('newAccount', hexAccount) - return this.setupAccounts(accounts) - }).then(() => { - return this.placeSeedWords() - }) - .then(this.persistAllKeyrings.bind(this)) - } - + // PlaceSeedWords + // returns Promise( @object state ) + // + // Adds the current vault's seed words to the UI's state tree. + // + // Used when creating a first vault, to allow confirmation. + // Also used when revealing the seed words in the confirmation view. placeSeedWords () { const firstKeyring = this.keyrings[0] return firstKeyring.serialize() .then((serialized) => { const seedWords = serialized.mnemonic this.configManager.setSeedWords(seedWords) - this.emit('update') - return + return this.fullUpdate() }) } + // ClearSeedWordCache + // + // returns Promise( @string currentSelectedAccount ) + // + // Removes the current vault's seed words from the UI's state tree, + // ensuring they are only ever available in the background process. + clearSeedWordCache () { + this.configManager.setSeedWords(null) + return Promise.resolve(this.configManager.getSelectedAccount()) + } + + // 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.migrateOldVaultIfAny(password) .then(() => { @@ -183,11 +233,17 @@ module.exports = class KeyringController extends EventEmitter { .then(this.fullUpdate.bind(this)) } - fullUpdate() { - this.emit('update') - return Promise.resolve(this.getState()) - } - + // 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) @@ -202,43 +258,42 @@ module.exports = class KeyringController extends EventEmitter { }) } + // 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 (keyRingNum = 0) { const ring = this.keyrings[keyRingNum] return ring.addAccounts(1) .then(this.setupAccounts.bind(this)) .then(this.persistAllKeyrings.bind(this)) + .then(this.fullUpdate.bind(this)) } - setupAccounts (accounts) { - return this.getAccounts() - .then((loadedAccounts) => { - const arr = accounts || loadedAccounts - return Promise.all(arr.map((account) => { - return this.getBalanceAndNickname(account) - })) - }) - } - - // Takes an account address and an iterator representing - // the current number of named accounts. - getBalanceAndNickname (account) { - const address = normalize(account) - this.ethStore.addAccount(address) - return this.createNickname(address) - } - - createNickname (address) { - const hexAddress = normalize(address) - var i = Object.keys(this.identities).length - const oldNickname = this.configManager.nicknameForWallet(address) - const name = oldNickname || `Account ${++i}` - this.identities[hexAddress] = { - address: hexAddress, - name, - } - return this.saveAccountLabel(hexAddress, name) + // Set Selected Account + // @string address + // + // returns Promise( @string address ) + // + // Sets the state's `selectedAccount` value + // to the specified address. + setSelectedAccount (address) { + var addr = normalize(address) + this.configManager.setSelectedAccount(addr) + Promise.resolve(addr) } + // Save Account Label + // @string account + // @string label + // + // returns Promise( @string label ) + // + // Persists a nickname equal to `label` for the specified account. saveAccountLabel (account, label) { const address = normalize(account) const configManager = this.configManager @@ -247,80 +302,45 @@ module.exports = class KeyringController extends EventEmitter { return Promise.resolve(label) } - persistAllKeyrings () { - 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], - } + // 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(normalize(address)) }) - })) - .then((serializedKeyrings) => { - return this.encryptor.encrypt(this.password, serializedKeyrings) - }) - .then((encryptedString) => { - this.configManager.setVault(encryptedString) - return true - }) - } - - unlockKeyrings (password) { - const encryptedVault = this.configManager.getVault() - return this.encryptor.decrypt(password, encryptedVault) - .then((vault) => { - this.password = password - vault.forEach(this.restoreKeyring.bind(this)) - return this.keyrings - }) - } - - 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 - }) + } catch (e) { + return Promise.reject(e) + } } - getKeyringClassForType (type) { - const Keyring = this.keyringTypes.reduce((res, kr) => { - if (kr.type === type) { - return kr - } else { - return res - } - }) - return Keyring - } - getAccounts () { - const keyrings = this.keyrings || [] - return Promise.all(keyrings.map(kr => kr.getAccounts())) - .then((keyringArrays) => { - return keyringArrays.reduce((res, arr) => { - return res.concat(arr) - }, []) - }) - } + // SIGNING RELATED METHODS + // + // SIGN, SUBMIT TX, CANCEL, AND APPROVE. + // THIS SECTION INVOLVES THE REQUEST, STORING, AND SIGNING OF DATA + // WITH THE KEYS STORED IN THIS CONTROLLER. - setSelectedAccount (address) { - var addr = normalize(address) - this.configManager.setSelectedAccount(addr) - Promise.resolve(addr) - } + // Add Unconfirmed Transaction + // @object txParams + // @function onTxDoneCb + // @function cb + // + // Calls back `cb` with @object txData = { txParams } + // Calls back `onTxDoneCb` with `true` or an `error` depending on result. + // + // Prepares the given `txParams` for final confirmation and approval. + // Estimates gas and other preparatory steps. + // Caches the requesting Dapp's callback, `onTxDoneCb`, for resolution later. addUnconfirmedTransaction (txParams, onTxDoneCb, cb) { var self = this const configManager = this.configManager @@ -446,28 +466,38 @@ module.exports = class KeyringController extends EventEmitter { } } - 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) + // Cancel Transaction + // @string txId + // @function cb + // + // Calls back `cb` with no error if provided. + // + // Forgets any tx matching `txId`. + cancelTransaction (txId, cb) { + const configManager = this.configManager + var approvalCb = this._unconfTxCbs[txId] || noop - // keep the cb around for after approval (requires user interaction) - // This cb fires completion to the Dapp's write operation. - this._unconfMsgCbs[msgId] = cb + // reject tx + approvalCb(null, false) + // clean up + configManager.rejectTx(txId) + delete this._unconfTxCbs[txId] - // signal update - this.emit('update') - return msgId + if (cb && typeof cb === 'function') { + cb() + } } + // Approve Transaction + // @string txId + // @function cb + // + // Calls back `cb` with no error always. + // + // Attempts to sign a Transaction with `txId` + // and submit it to the blockchain. + // + // Calls back the cached Dapp's confirmation callback, also. approveTransaction (txId, cb) { const configManager = this.configManager var approvalCb = this._unconfTxCbs[txId] || noop @@ -480,22 +510,6 @@ module.exports = class KeyringController extends EventEmitter { delete this._unconfTxCbs[txId] this.emit('update') } - - cancelTransaction (txId, cb) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // reject tx - approvalCb(null, false) - // clean up - configManager.rejectTx(txId) - delete this._unconfTxCbs[txId] - - if (cb && typeof cb === 'function') { - cb() - } - } - signTransaction (txParams, cb) { try { const address = normalize(txParams.from) @@ -534,14 +548,77 @@ module.exports = class KeyringController extends EventEmitter { } } + // 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 { + var approvalCb = this._unconfMsgCbs[msgId] || noop const address = normalize(msgParams.from) return this.getKeyringForAccount(address) .then((keyring) => { return keyring.signMessage(address, msgParams.data) }).then((rawSig) => { cb(null, rawSig) + approvalCb(null, true) return rawSig }) } catch (e) { @@ -549,6 +626,224 @@ module.exports = class KeyringController extends EventEmitter { } } + // PRIVATE METHODS + // + // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER + // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS. + + // Migrate Old Vault If Any + // @string password + // + // returns Promise() + // + // Temporary step used when logging in. + // Checks if old style (pre-3.0.0) Metamask Vault exists. + // If so, persists that vault in the new vault format + // with the provided password, so the other unlock steps + // may be completed without interruption. + migrateOldVaultIfAny (password) { + const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault() + return this.idStoreMigrator.migratedVaultForPassword(password) + .then((serialized) => { + this.password = password + + if (serialized && shouldMigrate) { + return this.restoreKeyring(serialized) + .then(keyring => keyring.getAccounts()) + .then((accounts) => { + this.configManager.setSelectedAccount(accounts[0]) + return this.persistAllKeyrings() + }) + } else { + return Promise.resolve() + } + }) + } + + // 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(() => { + return this.keyrings[0].getAccounts() + }) + .then((accounts) => { + const firstAccount = accounts[0] + const hexAccount = normalize(firstAccount) + this.configManager.setSelectedAccount(hexAccount) + this.emit('newAccount', hexAccount) + return this.setupAccounts(accounts) + }).then(() => { + return this.placeSeedWords() + }) + .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) { + const address = normalize(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 = normalize(address) + var i = Object.keys(this.identities).length + const oldNickname = this.configManager.nicknameForWallet(address) + const name = oldNickname || `Account ${++i}` + 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) { + 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.configManager.setVault(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.configManager.getVault() + 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) + } + + // 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 = normalize(address) return new Promise((resolve, reject) => { @@ -576,29 +871,12 @@ module.exports = class KeyringController extends EventEmitter { }) } - cancelMessage (msgId, cb) { - if (cb && typeof cb === 'function') { - cb() - } - } - - setLocked () { - this.password = null - this.keyrings = [] - return this.fullUpdate() - } - - exportAccount (address) { - try { - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.exportAccount(normalize(address)) - }) - } catch (e) { - return Promise.reject(e) - } - } - + // 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) @@ -606,11 +884,10 @@ module.exports = class KeyringController extends EventEmitter { return ethUtil.addHexPrefix(correct.toString(16)) } - clearSeedWordCache () { - this.configManager.setSeedWords(null) - return Promise.resolve(this.configManager.getSelectedAccount()) - } - + // Clear Keyrings + // + // Deallocates all currently managed keyrings and accounts. + // Used before initializing a new vault. clearKeyrings () { let accounts try { -- cgit v1.2.3