diff --git a/.eslintrc b/.eslintrc index 20a2a7a00..4fa7c2d7f 100644 --- a/.eslintrc +++ b/.eslintrc @@ -148,7 +148,7 @@ "space-in-parens": [1, "never"], "space-infix-ops": 2, "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], + "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","], "exceptions": ["=", "-"] } ], "strict": 0, "template-curly-spacing": [2, "never"], "use-isnan": 2, diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0a5c1d36f..18d71874a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1,3 +1,9 @@ +/** + * @file The central metamask controller. Aggregates other controllers and exports an api. + * @copyright Copyright (c) 2018 MetaMask + * @license MIT + */ + const EventEmitter = require('events') const extend = require('xtend') const pump = require('pump') @@ -41,7 +47,11 @@ const seedPhraseVerifier = require('./lib/seed-phrase-verifier') module.exports = class MetamaskController extends EventEmitter { - constructor (opts) { + /** + * @constructor + * @param {Object} opts + */ + constructor (opts) { super() this.defaultMaxListeners = 20 @@ -223,10 +233,9 @@ module.exports = class MetamaskController extends EventEmitter { this.infuraController.store.subscribe(sendUpdate) } - // - // Constructor helpers - // - + /** + * Constructor helper: initialize a provider. + */ initializeProvider () { const providerOpts = { static: { @@ -257,6 +266,9 @@ module.exports = class MetamaskController extends EventEmitter { return providerProxy } + /** + * Constructor helper: initialize a public config store. + */ initPublicConfigStore () { // get init state const publicConfigStore = new ObservableStore() @@ -278,10 +290,15 @@ module.exports = class MetamaskController extends EventEmitter { return publicConfigStore } - // - // State Management - // +//============================================================================= +// EXPOSED TO THE UI SUBSYSTEM +//============================================================================= + /** + * The metamask-state of the various controllers, made available to the UI + * + * @returns {Object} status + */ getState () { const wallet = this.configManager.getWallet() const vault = this.keyringController.store.getState().vault @@ -316,10 +333,11 @@ module.exports = class MetamaskController extends EventEmitter { ) } - // - // Remote Features - // - + /** + * Returns an api-object which is consumed by the UI + * + * @returns {Object} + */ getApi () { const keyringController = this.keyringController const preferencesController = this.preferencesController @@ -400,6 +418,451 @@ module.exports = class MetamaskController extends EventEmitter { } } + + +//============================================================================= +// VAULT / KEYRING RELATED METHODS +//============================================================================= + + /** + * Creates a new Vault(?) and create a new keychain(?) + * + * A vault is ... + * + * A keychain is ... + * + * + * @param {} password + * + * @returns {} vault + */ + async createNewVaultAndKeychain (password) { + const release = await this.createVaultMutex.acquire() + let vault + + try { + const accounts = await this.keyringController.getAccounts() + + if (accounts.length > 0) { + vault = await this.keyringController.fullUpdate() + + } else { + vault = await this.keyringController.createNewVaultAndKeychain(password) + this.selectFirstIdentity(vault) + } + release() + } catch (err) { + release() + throw err + } + + return vault + } + + /** + * Create a new Vault and restore an existent keychain + * @param {} password + * @param {} seed + */ + async createNewVaultAndRestore (password, seed) { + const release = await this.createVaultMutex.acquire() + try { + const vault = await this.keyringController.createNewVaultAndRestore(password, seed) + this.selectFirstIdentity(vault) + release() + return vault + } catch (err) { + release() + throw err + } + } + + /** + * Retrieves the first Identiy from the passed Vault and selects the related address + * + * An Identity is ... + * + * @param {} vault + */ + selectFirstIdentity (vault) { + const { identities } = vault + const address = Object.keys(identities)[0] + this.preferencesController.setSelectedAddress(address) + } + + // ? + // Opinionated Keyring Management + // + + /** + * Adds a new account to ... + * + * @returns {} keyState + */ + async addNewAccount () { + const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] + if (!primaryKeyring) { + throw new Error('MetamaskController - No HD Key Tree found') + } + const keyringController = this.keyringController + const oldAccounts = await keyringController.getAccounts() + const keyState = await keyringController.addNewAccount(primaryKeyring) + const newAccounts = await keyringController.getAccounts() + + await this.verifySeedPhrase() + + newAccounts.forEach((address) => { + if (!oldAccounts.includes(address)) { + this.preferencesController.setSelectedAddress(address) + } + }) + + return keyState + } + + /** + * 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 (cb) { + + this.verifySeedPhrase() + .then((seedWords) => { + this.configManager.setSeedWords(seedWords) + return cb(null, seedWords) + }) + .catch((err) => { + return cb(err) + }) + } + + /** + * Verifies the validity of the current vault's seed phrase. + * + * Validity: seed phrase restores the accounts belonging to the current vault. + * + * Called when the first account is created and on unlocking the vault. + */ + async verifySeedPhrase () { + + const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] + if (!primaryKeyring) { + throw new Error('MetamaskController - No HD Key Tree found') + } + + const serialized = await primaryKeyring.serialize() + const seedWords = serialized.mnemonic + + const accounts = await primaryKeyring.getAccounts() + if (accounts.length < 1) { + throw new Error('MetamaskController - No accounts found') + } + + try { + await seedPhraseVerifier.verifyAccounts(accounts, seedWords) + return seedWords + } catch (err) { + log.error(err.message) + throw err + } + } + + /** + * Remove the primary account seed phrase from the UI's state tree. + * + * The seed phrase remains available in the background process. + * + */ + clearSeedWordCache (cb) { + this.configManager.setSeedWords(null) + cb(null, this.preferencesController.getSelectedAddress()) + } + + /** + * ? + */ + resetAccount (cb) { + const selectedAddress = this.preferencesController.getSelectedAddress() + this.txController.wipeTransactions(selectedAddress) + cb(null, selectedAddress) + } + + /** + * Imports an account ... ? + * + * @param {} strategy + * @param {} args + * @param {} cb + */ + importAccountWithStrategy (strategy, args, cb) { + accountImporter.importAccount(strategy, args) + .then((privateKey) => { + return this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ]) + }) + .then(keyring => keyring.getAccounts()) + .then((accounts) => this.preferencesController.setSelectedAddress(accounts[0])) + .then(() => { cb(null, this.keyringController.fullUpdate()) }) + .catch((reason) => { cb(reason) }) + } + + // --------------------------------------------------------------------------- + // Identity Management (sign) + + /** + * @param {} msgParams + * @param {} cb + */ + signMessage (msgParams, cb) { + log.info('MetaMaskController - signMessage') + const msgId = msgParams.metamaskId + + // sets the status op the message to 'approved' + // and removes the metamaskId for signing + return this.messageManager.approveMessage(msgParams) + .then((cleanMsgParams) => { + // signs the message + return this.keyringController.signMessage(cleanMsgParams) + }) + .then((rawSig) => { + // tells the listener that the message has been signed + // and can be returned to the dapp + this.messageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) + } + + // Prefixed Style Message Signing Methods: + + /** + * + * @param {} msgParams + * @param {} cb + */ + approvePersonalMessage (msgParams, cb) { + const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + this.personalMessageManager.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied transaction signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + + /** + * @param {} msgParams + */ + signPersonalMessage (msgParams) { + log.info('MetaMaskController - signPersonalMessage') + const msgId = msgParams.metamaskId + // sets the status op the message to 'approved' + // and removes the metamaskId for signing + return this.personalMessageManager.approveMessage(msgParams) + .then((cleanMsgParams) => { + // signs the message + return this.keyringController.signPersonalMessage(cleanMsgParams) + }) + .then((rawSig) => { + // tells the listener that the message has been signed + // and can be returned to the dapp + this.personalMessageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) + } + + /** + * @param {} msgParams + */ + signTypedMessage (msgParams) { + log.info('MetaMaskController - signTypedMessage') + const msgId = msgParams.metamaskId + // sets the status op the message to 'approved' + // and removes the metamaskId for signing + return this.typedMessageManager.approveMessage(msgParams) + .then((cleanMsgParams) => { + // signs the message + return this.keyringController.signTypedMessage(cleanMsgParams) + }) + .then((rawSig) => { + // tells the listener that the message has been signed + // and can be returned to the dapp + this.typedMessageManager.setMsgStatusSigned(msgId, rawSig) + return this.getState() + }) + } + + // --------------------------------------------------------------------------- + // Account Restauration + + /** + * ? + * + * @param {} migratorOutput + */ + restoreOldVaultAccounts (migratorOutput) { + const { serialized } = migratorOutput + return this.keyringController.restoreKeyring(serialized) + .then(() => migratorOutput) + } + + /** + * ? + * + * @param {} migratorOutput + */ + restoreOldLostAccounts (migratorOutput) { + const { lostAccounts } = migratorOutput + if (lostAccounts) { + this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address)) + return this.importLostAccounts(migratorOutput) + } + return Promise.resolve(migratorOutput) + } + + /** + * Import (lost) Accounts + * + * @param {Object} {lostAccounts} @Array accounts <{ address, privateKey }> + * + * Uses the array's private keys to create a new Simple Key Pair keychain + * and add it to the keyring controller. + */ + importLostAccounts ({ lostAccounts }) { + const privKeys = lostAccounts.map(acct => acct.privateKey) + return this.keyringController.restoreKeyring({ + type: 'Simple Key Pair', + data: privKeys, + }) + } + +//============================================================================= +// END (VAULT / KEYRING RELATED METHODS) +//============================================================================= + +// + +//============================================================================= +// MESSAGES +//============================================================================= + + async retryTransaction (txId, cb) { + await this.txController.retryTransaction(txId) + const state = await this.getState() + return state + } + + + newUnsignedMessage (msgParams, cb) { + const msgId = this.messageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + this.messageManager.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + + newUnsignedPersonalMessage (msgParams, cb) { + if (!msgParams.from) { + return cb(new Error('MetaMask Message Signature: from field is required.')) + } + + const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + this.personalMessageManager.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + + newUnsignedTypedMessage (msgParams, cb) { + let msgId + try { + msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + } catch (e) { + return cb(e) + } + + this.typedMessageManager.once(`${msgId}:finished`, (data) => { + switch (data.status) { + case 'signed': + return cb(null, data.rawSig) + case 'rejected': + return cb(new Error('MetaMask Message Signature: User denied message signature.')) + default: + return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) + } + }) + } + + cancelMessage (msgId, cb) { + const messageManager = this.messageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + + cancelPersonalMessage (msgId, cb) { + const messageManager = this.personalMessageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + + cancelTypedMessage (msgId, cb) { + const messageManager = this.typedMessageManager + messageManager.rejectMsg(msgId) + if (cb && typeof cb === 'function') { + cb(null, this.getState()) + } + } + + markAccountsFound (cb) { + this.configManager.setLostAccounts([]) + this.sendUpdate() + cb(null, this.getState()) + } + + markPasswordForgotten(cb) { + this.configManager.setPasswordForgotten(true) + this.sendUpdate() + cb() + } + + unMarkPasswordForgotten(cb) { + this.configManager.setPasswordForgotten(false) + this.sendUpdate() + cb() + } + +//============================================================================= +// SETUP +//============================================================================= + setupUntrustedCommunication (connectionStream, originDomain) { // Check if new connection is blacklisted if (this.blacklistController.checkForPhishing(originDomain)) { @@ -515,365 +978,11 @@ module.exports = class MetamaskController extends EventEmitter { const percentileNum = percentile(50, lowestPrices) const percentileNumBn = new BN(percentileNum) return '0x' + percentileNumBn.mul(GWEI_BN).toString(16) - } + } - // - // Vault Management - // - - async createNewVaultAndKeychain (password) { - const release = await this.createVaultMutex.acquire() - let vault - - try { - const accounts = await this.keyringController.getAccounts() - - if (accounts.length > 0) { - vault = await this.keyringController.fullUpdate() - - } else { - vault = await this.keyringController.createNewVaultAndKeychain(password) - this.selectFirstIdentity(vault) - } - release() - } catch (err) { - release() - throw err - } - - return vault - } - - async createNewVaultAndRestore (password, seed) { - const release = await this.createVaultMutex.acquire() - try { - const vault = await this.keyringController.createNewVaultAndRestore(password, seed) - this.selectFirstIdentity(vault) - release() - return vault - } catch (err) { - release() - throw err - } - } - - selectFirstIdentity (vault) { - const { identities } = vault - const address = Object.keys(identities)[0] - this.preferencesController.setSelectedAddress(address) - } - - // - // Opinionated Keyring Management - // - - async addNewAccount () { - const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] - if (!primaryKeyring) { - throw new Error('MetamaskController - No HD Key Tree found') - } - const keyringController = this.keyringController - const oldAccounts = await keyringController.getAccounts() - const keyState = await keyringController.addNewAccount(primaryKeyring) - const newAccounts = await keyringController.getAccounts() - - await this.verifySeedPhrase() - - newAccounts.forEach((address) => { - if (!oldAccounts.includes(address)) { - this.preferencesController.setSelectedAddress(address) - } - }) - - return keyState - } - - // 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 (cb) { - - this.verifySeedPhrase() - .then((seedWords) => { - this.configManager.setSeedWords(seedWords) - return cb(null, seedWords) - }) - .catch((err) => { - return cb(err) - }) - } - - // Verifies the current vault's seed words if they can restore the - // accounts belonging to the current vault. - // - // Called when the first account is created and on unlocking the vault. - async verifySeedPhrase () { - - const primaryKeyring = this.keyringController.getKeyringsByType('HD Key Tree')[0] - if (!primaryKeyring) { - throw new Error('MetamaskController - No HD Key Tree found') - } - - const serialized = await primaryKeyring.serialize() - const seedWords = serialized.mnemonic - - const accounts = await primaryKeyring.getAccounts() - if (accounts.length < 1) { - throw new Error('MetamaskController - No accounts found') - } - - try { - await seedPhraseVerifier.verifyAccounts(accounts, seedWords) - return seedWords - } catch (err) { - log.error(err.message) - throw err - } - } - - // ClearSeedWordCache - // - // Removes the primary account's seed words from the UI's state tree, - // ensuring they are only ever available in the background process. - clearSeedWordCache (cb) { - this.configManager.setSeedWords(null) - cb(null, this.preferencesController.getSelectedAddress()) - } - - resetAccount (cb) { - const selectedAddress = this.preferencesController.getSelectedAddress() - this.txController.wipeTransactions(selectedAddress) - cb(null, selectedAddress) - } - - - importAccountWithStrategy (strategy, args, cb) { - accountImporter.importAccount(strategy, args) - .then((privateKey) => { - return this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ]) - }) - .then(keyring => keyring.getAccounts()) - .then((accounts) => this.preferencesController.setSelectedAddress(accounts[0])) - .then(() => { cb(null, this.keyringController.fullUpdate()) }) - .catch((reason) => { cb(reason) }) - } - - - // - // Identity Management - // - // - - async retryTransaction (txId, cb) { - await this.txController.retryTransaction(txId) - const state = await this.getState() - return state - } - - - newUnsignedMessage (msgParams, cb) { - const msgId = this.messageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - this.messageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) - default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) - } - }) - } - - newUnsignedPersonalMessage (msgParams, cb) { - if (!msgParams.from) { - return cb(new Error('MetaMask Message Signature: from field is required.')) - } - - const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - this.personalMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) - default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) - } - }) - } - - newUnsignedTypedMessage (msgParams, cb) { - let msgId - try { - msgId = this.typedMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - } catch (e) { - return cb(e) - } - - this.typedMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied message signature.')) - default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) - } - }) - } - - signMessage (msgParams, cb) { - log.info('MetaMaskController - signMessage') - const msgId = msgParams.metamaskId - - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - return this.messageManager.approveMessage(msgParams) - .then((cleanMsgParams) => { - // signs the message - return this.keyringController.signMessage(cleanMsgParams) - }) - .then((rawSig) => { - // tells the listener that the message has been signed - // and can be returned to the dapp - this.messageManager.setMsgStatusSigned(msgId, rawSig) - return this.getState() - }) - } - - cancelMessage (msgId, cb) { - const messageManager = this.messageManager - messageManager.rejectMsg(msgId) - if (cb && typeof cb === 'function') { - cb(null, this.getState()) - } - } - - // Prefixed Style Message Signing Methods: - approvePersonalMessage (msgParams, cb) { - const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams) - this.sendUpdate() - this.opts.showUnconfirmedMessage() - this.personalMessageManager.once(`${msgId}:finished`, (data) => { - switch (data.status) { - case 'signed': - return cb(null, data.rawSig) - case 'rejected': - return cb(new Error('MetaMask Message Signature: User denied transaction signature.')) - default: - return cb(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) - } - }) - } - - signPersonalMessage (msgParams) { - log.info('MetaMaskController - signPersonalMessage') - const msgId = msgParams.metamaskId - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - return this.personalMessageManager.approveMessage(msgParams) - .then((cleanMsgParams) => { - // signs the message - return this.keyringController.signPersonalMessage(cleanMsgParams) - }) - .then((rawSig) => { - // tells the listener that the message has been signed - // and can be returned to the dapp - this.personalMessageManager.setMsgStatusSigned(msgId, rawSig) - return this.getState() - }) - } - - signTypedMessage (msgParams) { - log.info('MetaMaskController - signTypedMessage') - const msgId = msgParams.metamaskId - // sets the status op the message to 'approved' - // and removes the metamaskId for signing - return this.typedMessageManager.approveMessage(msgParams) - .then((cleanMsgParams) => { - // signs the message - return this.keyringController.signTypedMessage(cleanMsgParams) - }) - .then((rawSig) => { - // tells the listener that the message has been signed - // and can be returned to the dapp - this.typedMessageManager.setMsgStatusSigned(msgId, rawSig) - return this.getState() - }) - } - - cancelPersonalMessage (msgId, cb) { - const messageManager = this.personalMessageManager - messageManager.rejectMsg(msgId) - if (cb && typeof cb === 'function') { - cb(null, this.getState()) - } - } - - cancelTypedMessage (msgId, cb) { - const messageManager = this.typedMessageManager - messageManager.rejectMsg(msgId) - if (cb && typeof cb === 'function') { - cb(null, this.getState()) - } - } - - markAccountsFound (cb) { - this.configManager.setLostAccounts([]) - this.sendUpdate() - cb(null, this.getState()) - } - - markPasswordForgotten(cb) { - this.configManager.setPasswordForgotten(true) - this.sendUpdate() - cb() - } - - unMarkPasswordForgotten(cb) { - this.configManager.setPasswordForgotten(false) - this.sendUpdate() - cb() - } - - restoreOldVaultAccounts (migratorOutput) { - const { serialized } = migratorOutput - return this.keyringController.restoreKeyring(serialized) - .then(() => migratorOutput) - } - - restoreOldLostAccounts (migratorOutput) { - const { lostAccounts } = migratorOutput - if (lostAccounts) { - this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address)) - return this.importLostAccounts(migratorOutput) - } - return Promise.resolve(migratorOutput) - } - - // IMPORT LOST ACCOUNTS - // @Object with key lostAccounts: @Array accounts <{ address, privateKey }> - // Uses the array's private keys to create a new Simple Key Pair keychain - // and add it to the keyring controller. - importLostAccounts ({ lostAccounts }) { - const privKeys = lostAccounts.map(acct => acct.privateKey) - return this.keyringController.restoreKeyring({ - type: 'Simple Key Pair', - data: privKeys, - }) - } - - // - // config - // +//============================================================================= +// CONFIG +//============================================================================= // Log blocks