diff options
Diffstat (limited to 'app/scripts/metamask-controller.js')
-rw-r--r-- | app/scripts/metamask-controller.js | 206 |
1 files changed, 143 insertions, 63 deletions
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 98cb62bfa..493877345 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -15,6 +15,7 @@ const RpcEngine = require('json-rpc-engine') const debounce = require('debounce') const createEngineStream = require('json-rpc-middleware-stream/engineStream') const createFilterMiddleware = require('eth-json-rpc-filters') +const createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager') const createOriginMiddleware = require('./lib/createOriginMiddleware') const createLoggerMiddleware = require('./lib/createLoggerMiddleware') const createProviderMiddleware = require('./lib/createProviderMiddleware') @@ -36,7 +37,6 @@ const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') const TokenRatesController = require('./controllers/token-rates') const DetectTokensController = require('./controllers/detect-tokens') -const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') @@ -50,6 +50,8 @@ const log = require('loglevel') const TrezorKeyring = require('eth-trezor-keyring') const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring') const EthQuery = require('eth-query') +const ethUtil = require('ethereumjs-util') +const sigUtil = require('eth-sig-util') module.exports = class MetamaskController extends EventEmitter { @@ -83,11 +85,6 @@ module.exports = class MetamaskController extends EventEmitter { // network store this.networkController = new NetworkController(initState.NetworkController) - // config manager - this.configManager = new ConfigManager({ - store: this.store, - }) - // preferences controller this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, @@ -177,7 +174,7 @@ module.exports = class MetamaskController extends EventEmitter { blockTracker: this.blockTracker, getGasPrice: this.getGasPrice.bind(this), }) - this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts)) + this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx()) this.txController.on(`tx:status-update`, (txId, status) => { if (status === 'confirmed' || status === 'failed') { @@ -211,7 +208,7 @@ module.exports = class MetamaskController extends EventEmitter { this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() - this.typedMessageManager = new TypedMessageManager() + this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) this.publicConfigStore = this.initPublicConfigStore() this.store.updateStructure({ @@ -256,6 +253,7 @@ module.exports = class MetamaskController extends EventEmitter { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, + version, // account mgmt getAccounts: async () => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked @@ -272,7 +270,7 @@ module.exports = class MetamaskController extends EventEmitter { // msg signing processEthSignMessage: this.newUnsignedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), - processTypedMessage: this.newUnsignedTypedMessage.bind(this), + getPendingNonce: this.getPendingNonce.bind(this), } const providerProxy = this.networkController.initializeProvider(providerOpts) return providerProxy @@ -314,18 +312,15 @@ module.exports = class MetamaskController extends EventEmitter { * @returns {Object} status */ getState () { - const wallet = this.configManager.getWallet() const vault = this.keyringController.store.getState().vault - const isInitialized = (!!wallet || !!vault) + const isInitialized = !!vault return { ...{ isInitialized }, ...this.memStore.getFlatState(), - ...this.configManager.getConfig(), ...{ - lostAccounts: this.configManager.getLostAccounts(), - seedWords: this.configManager.getSeedWords(), - forgottenPassword: this.configManager.getPasswordForgotten(), + // TODO: Remove usages of lost accounts + lostAccounts: [], }, } } @@ -382,6 +377,7 @@ module.exports = class MetamaskController extends EventEmitter { // network management setProviderType: nodeify(networkController.setProviderType, networkController), setCustomRpc: nodeify(this.setCustomRpc, this), + delCustomRpc: nodeify(this.delCustomRpc, this), // PreferencesController setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), @@ -392,6 +388,9 @@ module.exports = class MetamaskController extends EventEmitter { setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), + // BlacklistController + whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this), + // AddressController setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController), @@ -407,6 +406,7 @@ module.exports = class MetamaskController extends EventEmitter { updateTransaction: nodeify(txController.updateTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), retryTransaction: nodeify(this.retryTransaction, this), + createCancelTransaction: nodeify(this.createCancelTransaction, this), getFilteredTxList: nodeify(txController.getFilteredTxList, txController), isNonceTaken: nodeify(txController.isNonceTaken, txController), estimateGas: nodeify(this.estimateGas, this), @@ -556,6 +556,8 @@ module.exports = class MetamaskController extends EventEmitter { } await this.preferencesController.syncAddresses(accounts) + await this.balancesController.updateAllBalances() + await this.txController.pendingTxTracker.updatePendingTxs() return this.keyringController.fullUpdate() } @@ -726,7 +728,7 @@ module.exports = class MetamaskController extends EventEmitter { this.verifySeedPhrase() .then((seedWords) => { - this.configManager.setSeedWords(seedWords) + this.preferencesController.setSeedWords(seedWords) return cb(null, seedWords) }) .catch((err) => { @@ -775,7 +777,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {function} cb Callback function called with the current address. */ clearSeedWordCache (cb) { - this.configManager.setSeedWords(null) + this.preferencesController.setSeedWords(null) cb(null, this.preferencesController.getSelectedAddress()) } @@ -983,22 +985,31 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_signTypedData. * @returns {Object} Full state update. */ - signTypedMessage (msgParams) { - log.info('MetaMaskController - signTypedMessage') + async signTypedMessage (msgParams) { + log.info('MetaMaskController - eth_signTypedData') 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() - }) + const version = msgParams.version + try { + const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams) + const address = sigUtil.normalize(cleanMsgParams.from) + const keyring = await this.keyringController.getKeyringForAccount(address) + const wallet = keyring._getWalletForAccount(address) + const privKey = ethUtil.toBuffer(wallet.getPrivateKey()) + let signature + switch (version) { + case 'V1': + signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data }) + break + case 'V3': + signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) }) + break + } + this.typedMessageManager.setMsgStatusSigned(msgId, signature) + return this.getState() + } catch (error) { + log.info('MetaMaskController - eth_signTypedData failed.', error) + this.typedMessageManager.errorMessage(msgId, error) + } } /** @@ -1036,36 +1047,17 @@ module.exports = class MetamaskController extends EventEmitter { /** * A legacy method used to record user confirmation that they understand * that some of their accounts have been recovered but should be backed up. + * This function no longer does anything and will be removed. * * @deprecated * @param {Function} cb - A callback function called with a full state update. */ markAccountsFound (cb) { - this.configManager.setLostAccounts([]) - this.sendUpdate() + // TODO Remove me cb(null, this.getState()) } /** - * A legacy method (probably dead code) that was used when we swapped out our - * key management library that we depended on. - * - * Described in: - * https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd - * - * @deprecated - * @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) - } - - /** * An account object * @typedef Account * @property string privateKey - The private key of the account. @@ -1109,6 +1101,19 @@ module.exports = class MetamaskController extends EventEmitter { return state } + /** + * Allows a user to attempt to cancel a previously submitted transaction by creating a new + * transaction. + * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel + * @param {string=} customGasPrice - the hex value to use for the cancel transaction + * @returns {object} MetaMask state + */ + async createCancelTransaction (originalTxId, customGasPrice, cb) { + await this.txController.createCancelTransaction(originalTxId, customGasPrice) + const state = await this.getState() + return state + } + estimateGas (estimateGasParams) { return new Promise((resolve, reject) => { return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => { @@ -1130,7 +1135,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function called when complete. */ markPasswordForgotten (cb) { - this.configManager.setPasswordForgotten(true) + this.preferencesController.setPasswordForgotten(true) this.sendUpdate() cb() } @@ -1140,7 +1145,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function called when complete. */ unMarkPasswordForgotten (cb) { - this.configManager.setPasswordForgotten(false) + this.preferencesController.setPasswordForgotten(false) this.sendUpdate() cb() } @@ -1229,8 +1234,10 @@ module.exports = class MetamaskController extends EventEmitter { ) dnode.on('remote', (remote) => { // push updates to popup - const sendUpdate = remote.sendUpdate.bind(remote) + const sendUpdate = (update) => remote.sendUpdate(update) this.on('update', sendUpdate) + // remove update listener once the connection ends + dnode.on('end', () => this.removeListener('update', sendUpdate)) }) } @@ -1242,21 +1249,33 @@ module.exports = class MetamaskController extends EventEmitter { setupProviderConnection (outStream, origin) { // setup json rpc engine stack const engine = new RpcEngine() + const provider = this.provider + const blockTracker = this.blockTracker // create filter polyfill middleware - const filterMiddleware = createFilterMiddleware({ - provider: this.provider, - blockTracker: this.blockTracker, - }) + const filterMiddleware = createFilterMiddleware({ provider, blockTracker }) + // create subscription polyfill middleware + const subscriptionManager = createSubscriptionManager({ provider, blockTracker }) + subscriptionManager.events.on('notification', (message) => engine.emit('notification', message)) + // metadata engine.push(createOriginMiddleware({ origin })) engine.push(createLoggerMiddleware({ origin })) + // filter and subscription polyfills engine.push(filterMiddleware) + engine.push(subscriptionManager.middleware) + // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) - engine.push(createProviderMiddleware({ provider: this.provider })) + // sign typed data middleware + engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this)) + engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this)) + engine.push(this.createTypedDataMiddleware('eth_signTypedData_v3', 'V3', true).bind(this)) + // forward to metamask primary provider + engine.push(createProviderMiddleware({ provider })) // setup connection const providerStream = createEngineStream({ engine }) + pump( outStream, providerStream, @@ -1280,10 +1299,12 @@ module.exports = class MetamaskController extends EventEmitter { * @param {*} outStream - The stream to provide public config over. */ setupPublicConfig (outStream) { + const configStream = asStream(this.publicConfigStore) pump( - asStream(this.publicConfigStore), + configStream, outStream, (err) => { + configStream.destroy() if (err) log.error(err) } ) @@ -1354,11 +1375,24 @@ module.exports = class MetamaskController extends EventEmitter { }) .map(number => number.div(GWEI_BN).toNumber()) - const percentileNum = percentile(50, lowestPrices) + const percentileNum = percentile(65, lowestPrices) const percentileNumBn = new BN(percentileNum) return '0x' + percentileNumBn.mul(GWEI_BN).toString(16) } + /** + * Returns the nonce that will be associated with a transaction once approved + * @param address {string} - The hex string address for the transaction + * @returns Promise<number> + */ + async getPendingNonce (address) { + const { nonceDetails, releaseLock} = await this.txController.nonceTracker.getNonceLock(address) + const pendingNonce = nonceDetails.params.highestSuggested + + releaseLock() + return pendingNonce + } + //============================================================================= // CONFIG //============================================================================= @@ -1422,6 +1456,14 @@ module.exports = class MetamaskController extends EventEmitter { } /** + * A method for deleting a selected custom URL. + * @param {string} rpcTarget - A RPC URL to delete. + */ + async delCustomRpc (rpcTarget) { + await this.preferencesController.updateFrequentRpcList(rpcTarget, true) + } + + /** * Sets whether or not to use the blockie identicon format. * @param {boolean} val - True for bockie, false for jazzicon. * @param {Function} cb - A callback function called when complete. @@ -1484,4 +1526,42 @@ module.exports = class MetamaskController extends EventEmitter { set isClientOpenAndUnlocked (active) { this.tokenRatesController.isActive = active } + + /** + * Creates RPC engine middleware for processing eth_signTypedData requests + * + * @param {Object} req - request object + * @param {Object} res - response object + * @param {Function} - next + * @param {Function} - end + */ + createTypedDataMiddleware (methodName, version, reverse) { + return async (req, res, next, end) => { + const { method, params } = req + if (method === methodName) { + const promise = this.typedMessageManager.addUnapprovedMessageAsync({ + data: reverse ? params[1] : params[0], + from: reverse ? params[0] : params[1], + }, req, version) + this.sendUpdate() + this.opts.showUnconfirmedMessage() + try { + res.result = await promise + end() + } catch (error) { + end(error) + } + } else { + next() + } + } + } + + /** + * Adds a domain to the {@link BlacklistController} whitelist + * @param {string} hostname the domain to whitelist + */ + whitelistPhishingDomain (hostname) { + return this.blacklistController.whitelistDomain(hostname) + } } |