diff options
Diffstat (limited to 'app/scripts/keyring-controller.js')
-rw-r--r-- | app/scripts/keyring-controller.js | 946 |
1 files changed, 528 insertions, 418 deletions
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js index fb3091143..4be00a5a5 100644 --- a/app/scripts/keyring-controller.js +++ b/app/scripts/keyring-controller.js @@ -1,18 +1,12 @@ -const async = require('async') -const EventEmitter = require('events').EventEmitter -const encryptor = require('./lib/encryptor') -const messageManager = require('./lib/message-manager') const ethUtil = require('ethereumjs-util') -const ethBinToOps = require('eth-bin-to-ops') -const EthQuery = require('eth-query') -const BN = ethUtil.BN -const Transaction = require('ethereumjs-tx') -const createId = require('web3-provider-engine/util/random-id') -const autoFaucet = require('./lib/auto-faucet') const bip39 = require('bip39') +const EventEmitter = require('events').EventEmitter +const filter = require('promise-filter') +const encryptor = require('browser-passworder') -// TEMPORARY UNTIL FULL DEPRECATION: -const IdStoreMigrator = require('./lib/idStore-migrator') +const normalize = require('./lib/sig-util').normalize +const messageManager = require('./lib/message-manager') +const BN = ethUtil.BN // Keyrings: const SimpleKeyring = require('./keyrings/simple') @@ -22,390 +16,322 @@ const keyringTypes = [ HdKeyring, ] +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.web3 = opts.web3 this.configManager = opts.configManager this.ethStore = opts.ethStore this.encryptor = encryptor this.keyringTypes = keyringTypes - this.keyrings = [] this.identities = {} // Essentially a name hash - this._unconfTxCbs = {} this._unconfMsgCbs = {} - this.network = opts.network + this.getNetwork = opts.getNetwork + } - // TEMPORARY UNTIL FULL DEPRECATION: - this.idStoreMigrator = new IdStoreMigrator({ - configManager: this.configManager, - }) + // 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 } - getState() { + // 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() const wallet = configManager.getWallet() // old style vault const vault = configManager.getVault() // new style vault + const keyrings = this.keyrings - return { - seedWords: this.configManager.getSeedWords(), - isInitialized: (!!wallet || !!vault), - isUnlocked: !!this.key, - isConfirmed: true, // AUDIT this.configManager.getConfirmed(), - unconfTxs: this.configManager.unconfirmedTxs(), - transactions: this.configManager.getTxList(), - unconfMsgs: messageManager.unconfirmedMsgs(), - messages: messageManager.getMsgList(), - selectedAddress: address, - selectedAccount: address, - shapeShiftTxList: this.configManager.getShapeShiftTxList(), - currentFiat: this.configManager.getCurrentFiat(), - conversionRate: this.configManager.getConversionRate(), - conversionDate: this.configManager.getConversionDate(), - keyringTypes: this.keyringTypes.map((krt) => krt.type()), - identities: this.identities, - } - } - - setStore(ethStore) { - this.ethStore = ethStore - } - - createNewVaultAndKeychain(password, entropy, cb) { - this.createNewVault(password, entropy, (err) => { - if (err) return cb(err) - this.createFirstKeyTree(password, cb) + return Promise.all(keyrings.map(this.displayForKeyring)) + .then((displayKeyrings) => { + return { + seedWords: this.configManager.getSeedWords(), + isInitialized: (!!wallet || !!vault), + isUnlocked: Boolean(this.password), + isDisclaimerConfirmed: this.configManager.getConfirmedDisclaimer(), + unconfMsgs: messageManager.unconfirmedMsgs(), + messages: messageManager.getMsgList(), + selectedAccount: address, + shapeShiftTxList: this.configManager.getShapeShiftTxList(), + currentFiat: this.configManager.getCurrentFiat(), + conversionRate: this.configManager.getConversionRate(), + conversionDate: this.configManager.getConversionDate(), + keyringTypes: this.keyringTypes.map(krt => krt.type), + identities: this.identities, + keyrings: displayKeyrings, + } }) } - createNewVaultAndRestore(password, seed, cb) { + // 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 cb('Password must be text.') + return Promise.reject('Password must be text.') } if (!bip39.validateMnemonic(seed)) { - return cb('Seed phrase is invalid.') + return Promise.reject('Seed phrase is invalid.') } this.clearKeyrings() - this.createNewVault(password, '', (err) => { - if (err) return cb(err) - this.addNewKeyring('HD Key Tree', { + return this.persistAllKeyrings(password) + .then(() => { + return this.addNewKeyring('HD Key Tree', { mnemonic: seed, - n: 1, - }, (err) => { - if (err) return cb(err) - const firstKeyring = this.keyrings[0] - const accounts = firstKeyring.getAccounts() - const firstAccount = accounts[0] - const hexAccount = normalize(firstAccount) - this.configManager.setSelectedAccount(hexAccount) - this.setupAccounts(accounts) - - this.emit('update') - cb(null, this.getState()) + numberOfAccounts: 1, }) - }) - } - - migrateAndGetKey(password) { - let key - const shouldMigrate = !!this.configManager.getWallet() && !this.configManager.getVault() - - return this.loadKey(password) - .then((derivedKey) => { - key = derivedKey - this.key = key - return this.idStoreMigrator.oldSeedForPassword(password) - }) - .then((serialized) => { - if (serialized && shouldMigrate) { - const keyring = this.restoreKeyring(serialized) - this.keyrings.push(keyring) - this.configManager.setSelectedAccount(keyring.getAccounts()[0]) - } - return key - }) - } - - createNewVault(password, entropy, cb) { - const configManager = this.configManager - const salt = this.encryptor.generateSalt() - configManager.setSalt(salt) - - return this.migrateAndGetKey(password) - .then(() => { - return this.persistAllKeyrings() - }) - .then(() => { - cb(null) - }) - .catch((err) => { - cb(err) - }) - } - - createFirstKeyTree(password, cb) { - this.clearKeyrings() - this.addNewKeyring('HD Key Tree', {n: 1}, (err) => { + }).then(() => { const firstKeyring = this.keyrings[0] - const accounts = firstKeyring.getAccounts() + return firstKeyring.getAccounts() + }) + .then((accounts) => { const firstAccount = accounts[0] const hexAccount = normalize(firstAccount) - const seedWords = firstKeyring.serialize().mnemonic - this.configManager.setSelectedAccount(firstAccount) + this.configManager.setSelectedAccount(hexAccount) + return this.setupAccounts(accounts) + }) + .then(this.persistAllKeyrings.bind(this, password)) + .then(this.fullUpdate.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) - autoFaucet(hexAccount) - this.setupAccounts(accounts) - this.persistAllKeyrings() - .then(() => { - cb(err, this.getState()) - }) - .catch((reason) => { - cb(reason) - }) + return this.fullUpdate() }) } - placeSeedWords (cb) { - const firstKeyring = this.keyrings[0] - const seedWords = firstKeyring.serialize().mnemonic - this.configManager.setSeedWords(seedWords) + // 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()) } - submitPassword(password, cb) { - this.migrateAndGetKey(password) - .then((key) => { - return this.unlockKeyrings(key) - }) + // 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 - this.setupAccounts() - this.emit('update') - cb(null, this.getState()) - }) - .catch((err) => { - console.error(err) - cb(err) + return this.fullUpdate() }) } - loadKey(password) { - const salt = this.configManager.getSalt() || this.encryptor.generateSalt() - return this.encryptor.keyFromPassword(password + salt) - .then((key) => { - this.key = key - this.configManager.setSalt(salt) - return key - }) - } - - addNewKeyring(type, opts, cb) { + // 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) - const accounts = keyring.getAccounts() - - this.keyrings.push(keyring) - this.setupAccounts(accounts) - this.persistAllKeyrings() - .then(() => { - cb(null, this.getState()) - }) - .catch((reason) => { - cb(reason) + return keyring.getAccounts() + .then((accounts) => { + this.keyrings.push(keyring) + return this.setupAccounts(accounts) }) - } - - addNewAccount(keyRingNum = 0, cb) { - const ring = this.keyrings[keyRingNum] - const accounts = ring.addAccounts(1) - this.setupAccounts(accounts) - this.persistAllKeyrings() + .then(() => { return this.password }) + .then(this.persistAllKeyrings.bind(this)) .then(() => { - cb(null, this.getState()) - }) - .catch((reason) => { - cb(reason) - }) - } - - setupAccounts(accounts) { - var arr = accounts || this.getAccounts() - arr.forEach((account) => { - this.loadBalanceAndNickname(account) + return keyring }) } - // Takes an account address and an iterator representing - // the current number of named accounts. - loadBalanceAndNickname(account) { - const address = normalize(account) - this.ethStore.addAccount(address) - 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) + // 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)) + } + + // 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) + return this.fullUpdate() } - saveAccountLabel (account, label, cb) { + // 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 configManager.setNicknameForWallet(address, label) this.identities[address].name = label - if (cb) { - cb(null, label) - } else { - return label + return Promise.resolve(label) + } + + // 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)) + }) + } catch (e) { + return Promise.reject(e) } } - persistAllKeyrings() { - const serialized = this.keyrings.map((k) => { - return { - type: k.type, - data: k.serialize(), - } - }) - return this.encryptor.encryptWithKey(this.key, serialized) - .then((encryptedString) => { - this.configManager.setVault(encryptedString) - return true - }) - } + // SIGNING METHODS + // + // This method signs tx and returns a promise for + // TX Manager to update the state after signing - unlockKeyrings(key) { - const encryptedVault = this.configManager.getVault() - return this.encryptor.decryptWithKey(key, encryptedVault) - .then((vault) => { - vault.forEach(this.restoreKeyring.bind(this)) - return this.keyrings + signTransaction (ethTx, _fromAddress) { + const fromAddress = normalize(_fromAddress) + return this.getKeyringForAccount(fromAddress) + .then((keyring) => { + return keyring.signTransaction(fromAddress, ethTx) }) } - - restoreKeyring(serialized) { - const { type, data } = serialized - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring() - keyring.deserialize(data) - - const accounts = keyring.getAccounts() - this.setupAccounts(accounts) - - this.keyrings.push(keyring) - return keyring - } - - 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 keyrings.map(kr => kr.getAccounts()) - .reduce((res, arr) => { - return res.concat(arr) - }, []) - } - - setSelectedAddress(address, cb) { - var addr = normalize(address) - this.configManager.setSelectedAccount(addr) - cb(null, addr) - } - - addUnconfirmedTransaction(txParams, onTxDoneCb, cb) { - var self = this - const configManager = this.configManager - - // create txData obj with parameters and meta data - var time = (new Date()).getTime() - var txId = createId() - txParams.metamaskId = txId - txParams.metamaskNetworkId = this.network - var txData = { - id: txId, - txParams: txParams, - time: time, - status: 'unconfirmed', - gasMultiplier: configManager.getGasMultiplier() || 1, - } - - - // keep the onTxDoneCb around for after approval/denial (requires user interaction) - // This onTxDoneCb fires completion to the Dapp's write operation. - this._unconfTxCbs[txId] = onTxDoneCb - - var provider = this.ethStore._query.currentProvider - var query = new EthQuery(provider) - - // calculate metadata for tx - async.parallel([ - analyzeForDelegateCall, - estimateGas, - ], didComplete) - - // perform static analyis on the target contract code - function analyzeForDelegateCall(cb){ - if (txParams.to) { - query.getCode(txParams.to, function (err, result) { - if (err) return cb(err) - var code = ethUtil.toBuffer(result) - if (code !== '0x') { - var ops = ethBinToOps(code) - var containsDelegateCall = ops.some((op) => op.name === 'DELEGATECALL') - txData.containsDelegateCall = containsDelegateCall - cb() - } else { - cb() - } - }) - } else { - cb() - } - } - - function estimateGas(cb){ - query.estimateGas(txParams, function(err, result){ - if (err) return cb(err) - txData.estimatedGas = self.addGasBuffer(result) - cb() - }) - } - - function didComplete (err) { - if (err) return cb(err) - configManager.addTx(txData) - // signal update - self.emit('update') - // signal completion of add tx - cb(null, txData) - } - } - - addUnconfirmedMessage(msgParams, cb) { + // 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() @@ -427,125 +353,313 @@ module.exports = class KeyringController extends EventEmitter { return msgId } - approveTransaction(txId, cb) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop - - // accept tx - cb() - approvalCb(null, true) - // clean up - configManager.confirmTx(txId) - delete this._unconfTxCbs[txId] - this.emit('update') - } - - cancelTransaction(txId, cb) { - const configManager = this.configManager - var approvalCb = this._unconfTxCbs[txId] || noop + // 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 - configManager.rejectTx(txId) - delete this._unconfTxCbs[txId] + messageManager.rejectMsg(msgId) + delete this._unconfTxCbs[msgId] if (cb && typeof cb === 'function') { cb() } } - signTransaction(txParams, 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 address = normalize(txParams.from) - const keyring = this.getKeyringForAccount(address) - - // Handle gas pricing - var gasMultiplier = this.configManager.getGasMultiplier() || 1 - 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) - - let tx = new Transaction(txParams) - tx = keyring.signTransaction(address, tx) - - // Add the tx hash to the persisted meta-tx object - var txHash = ethUtil.bufferToHex(tx.hash()) - var metaTx = this.configManager.getTx(txParams.metamaskId) - metaTx.hash = txHash - this.configManager.updateTx(metaTx) - - // return raw serialized tx - var rawTx = ethUtil.bufferToHex(tx.serialize()) - cb(null, rawTx) - } catch (e) { - cb(e) - } - } + const msgId = msgParams.metamaskId + delete msgParams.metamaskId + const approvalCb = this._unconfMsgCbs[msgId] || noop - signMessage(msgParams, cb) { - try { - const keyring = this.getKeyringForAccount(msgParams.from) const address = normalize(msgParams.from) - const rawSig = keyring.signMessage(address, msgParams.data) - cb(null, rawSig) + 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) } } - getKeyringForAccount(address) { - const hexed = normalize(address) - return this.keyrings.find((ring) => { - return ring.getAccounts() - .map(normalize) - .includes(hexed) + // 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(() => { + 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) + })) }) } - cancelMessage(msgId, cb) { - if (cb && typeof cb === 'function') { - cb() + // 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 = 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) } - setLocked(cb) { - this.key = null - this.keyrings = [] - cb() + // 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.configManager.setVault(encryptedString) + return true + }) } - exportAccount(address, cb) { - try { - const keyring = this.getKeyringForAccount(address) - const privateKey = keyring.exportAccount(normalize(address)) - cb(null, privateKey) - } catch (e) { - cb(e) + // 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() + 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 + }) } - addGasBuffer(gasHex) { - var gas = new BN(gasHex, 16) - var buffer = new BN('100000', 10) - var result = gas.add(buffer) - return normalize(result.toString(16)) + // 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 + }) } - clearSeedWordCache(cb) { - this.configManager.setSeedWords(null) - cb(null, this.configManager.getSelectedAccount()) + // 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) + }, []) + }) } - clearKeyrings() { + // 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 Promise.all(this.keyrings.map((keyring) => { + return Promise.all([ + keyring, + keyring.getAccounts(), + ]) + })) + .then(filter((candidate) => { + const accounts = candidate[1].map(normalize) + 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._currentState.accounts) @@ -563,9 +677,5 @@ module.exports = class KeyringController extends EventEmitter { } -function normalize(address) { - if (!address) return - return ethUtil.addHexPrefix(address.toLowerCase()) -} function noop () {} |