diff options
Diffstat (limited to 'app/scripts')
23 files changed, 478 insertions, 436 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js index e13d2842a..b91021c6e 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -38,7 +38,6 @@ const { const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG) const STORAGE_KEY = 'metamask-config' -const METAMASK_DEBUG = process.env.METAMASK_DEBUG log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') @@ -414,6 +413,7 @@ function setupController (initState, initLangCode) { controller.txController.on('update:badge', updateBadge) controller.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) + controller.typedMessageManager.on('updateBadge', updateBadge) /** * Updates the Web Extension's "badge" number, on the little fox in the toolbar. @@ -469,11 +469,3 @@ function showWatchAssetUi () { } ) } - -// On first install, open a window to MetaMask website to how-it-works. -extension.runtime.onInstalled.addListener(function (details) { - if ((details.reason === 'install') && (!METAMASK_DEBUG)) { - extension.tabs.create({url: 'https://metamask.io/#how-it-works'}) - } -}) - diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 6eee1987a..33523eb46 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -1,6 +1,7 @@ const fs = require('fs') const path = require('path') const pump = require('pump') +const querystring = require('querystring') const LocalMessageDuplexStream = require('post-message-stream') const PongStream = require('ping-pong-stream/pong') const ObjectMultiplex = require('obj-multiplex') @@ -134,17 +135,22 @@ function doctypeCheck () { } /** - * Checks the current document extension + * Returns whether or not the extension (suffix) of the current document is prohibited * - * @returns {boolean} {@code true} if the current extension is not prohibited + * This checks {@code window.location.pathname} against a set of file extensions + * that should not have web3 injected into them. This check is indifferent of query parameters + * in the location. + * + * @returns {boolean} whether or not the extension of the current document is prohibited */ function suffixCheck () { - var prohibitedTypes = ['xml', 'pdf'] - var currentUrl = window.location.href - var currentRegex + const prohibitedTypes = [ + /\.xml$/, + /\.pdf$/, + ] + const currentUrl = window.location.pathname for (let i = 0; i < prohibitedTypes.length; i++) { - currentRegex = new RegExp(`\\.${prohibitedTypes[i]}$`) - if (currentRegex.test(currentUrl)) { + if (prohibitedTypes[i].test(currentUrl)) { return false } } @@ -199,5 +205,8 @@ function blacklistedDomainCheck () { function redirectToPhishingWarning () { console.log('MetaMask - routing to Phishing Warning component') const extensionURL = extension.runtime.getURL('phishing.html') - window.location.href = extensionURL + window.location.href = `${extensionURL}#${querystring.stringify({ + hostname: window.location.hostname, + href: window.location.href, + })}` } diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index 1d2191433..89c7cc888 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -29,6 +29,7 @@ class BlacklistController { constructor (opts = {}) { const initState = extend({ phishing: PHISHING_DETECTION_CONFIG, + whitelist: [], }, opts.initState) this.store = new ObservableStore(initState) // phishing detector @@ -39,6 +40,21 @@ class BlacklistController { } /** + * Adds the given hostname to the runtime whitelist + * @param {string} hostname the hostname to whitelist + */ + whitelistDomain (hostname) { + if (!hostname) { + return + } + + const { whitelist } = this.store.getState() + this.store.updateState({ + whitelist: [...new Set([hostname, ...whitelist])], + }) + } + + /** * Given a url, returns the result of checking if that url is in the store.phishing blacklist * * @param {string} hostname The hostname portion of a url; the one that will be checked against the white and @@ -48,6 +64,12 @@ class BlacklistController { */ checkForPhishing (hostname) { if (!hostname) return false + + const { whitelist } = this.store.getState() + if (whitelist.some((e) => e === hostname)) { + return false + } + const { result } = this._phishingDetector.check(hostname) return result } diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 41af4d9f9..326bcb355 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -1,5 +1,6 @@ const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') -const createBlockReEmitMiddleware = require('eth-json-rpc-middleware/block-reemit') +const createBlockReRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createRetryOnEmptyMiddleware = require('eth-json-rpc-middleware/retryOnEmpty') const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') @@ -11,13 +12,14 @@ module.exports = createInfuraClient function createInfuraClient ({ network }) { const infuraMiddleware = createInfuraMiddleware({ network }) - const blockProvider = providerFromMiddleware(infuraMiddleware) - const blockTracker = new BlockTracker({ provider: blockProvider }) + const infuraProvider = providerFromMiddleware(infuraMiddleware) + const blockTracker = new BlockTracker({ provider: infuraProvider }) const networkMiddleware = mergeMiddleware([ createBlockCacheMiddleware({ blockTracker }), createInflightMiddleware(), - createBlockReEmitMiddleware({ blockTracker, provider: blockProvider }), + createBlockReRefMiddleware({ blockTracker, provider: infuraProvider }), + createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }), createBlockTrackerInspectorMiddleware({ blockTracker }), infuraMiddleware, ]) diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index 40c353f7f..a8cbf2aaf 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -1,6 +1,6 @@ const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') -const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache') const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') @@ -15,7 +15,7 @@ function createJsonRpcClient ({ rpcUrl }) { const blockTracker = new BlockTracker({ provider: blockProvider }) const networkMiddleware = mergeMiddleware([ - createBlockRefMiddleware({ blockTracker }), + createBlockRefRewriteMiddleware({ blockTracker }), createBlockCacheMiddleware({ blockTracker }), createInflightMiddleware(), createBlockTrackerInspectorMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js index fecc512e8..09b1d3c1c 100644 --- a/app/scripts/controllers/network/createLocalhostClient.js +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -1,6 +1,6 @@ const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') -const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const BlockTracker = require('eth-block-tracker') @@ -13,7 +13,7 @@ function createLocalhostClient () { const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) const networkMiddleware = mergeMiddleware([ - createBlockRefMiddleware({ blockTracker }), + createBlockRefRewriteMiddleware({ blockTracker }), createBlockTrackerInspectorMiddleware({ blockTracker }), fetchMiddleware, ]) diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js index 8b17829b7..9e6a45888 100644 --- a/app/scripts/controllers/network/createMetamaskMiddleware.js +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -38,6 +38,6 @@ function createPendingNonceMiddleware ({ getPendingNonce }) { const address = req.params[0] const blockRef = req.params[1] if (blockRef !== 'pending') return next() - req.result = await getPendingNonce(address) + res.result = await getPendingNonce(address) }) } diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 464a37017..fd6a4866d 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -36,6 +36,8 @@ class PreferencesController { currentLocale: opts.initLangCode, identities: {}, lostIdentities: {}, + seedWords: null, + forgottenPassword: false, }, opts.initState) this.diagnostics = opts.diagnostics @@ -47,6 +49,22 @@ class PreferencesController { // PUBLIC METHODS /** + * Sets the {@code forgottenPassword} state property + * @param {boolean} forgottenPassword whether or not the user has forgotten their password + */ + setPasswordForgotten (forgottenPassword) { + this.store.updateState({ forgottenPassword }) + } + + /** + * Sets the {@code seedWords} seed words + * @param {string|null} seedWords the seed words + */ + setSeedWords (seedWords) { + this.store.updateState({ seedWords }) + } + + /** * Setter for the `useBlockie` property * * @param {boolean} val Whether or not the user prefers blockie indicators @@ -357,11 +375,12 @@ class PreferencesController { * Gets an updated rpc list from this.addToFrequentRpcList() and sets the `frequentRpcList` to this update list. * * @param {string} _url The the new rpc url to add to the updated list + * @param {bool} remove Remove selected url * @returns {Promise<void>} Promise resolves with undefined * */ - updateFrequentRpcList (_url) { - return this.addToFrequentRpcList(_url) + updateFrequentRpcList (_url, remove = false) { + return this.addToFrequentRpcList(_url, remove) .then((rpcList) => { this.store.updateState({ frequentRpcList: rpcList }) return Promise.resolve() @@ -388,21 +407,19 @@ class PreferencesController { * end of the list. The current list is modified and returned as a promise. * * @param {string} _url The rpc url to add to the frequentRpcList. + * @param {bool} remove Remove selected url * @returns {Promise<array>} The updated frequentRpcList. * */ - addToFrequentRpcList (_url) { + addToFrequentRpcList (_url, remove = false) { const rpcList = this.getFrequentRpcList() const index = rpcList.findIndex((element) => { return element === _url }) if (index !== -1) { rpcList.splice(index, 1) } - if (_url !== 'http://localhost:8545') { + if (!remove && _url !== 'http://localhost:8545') { rpcList.push(_url) } - if (rpcList.length > 3) { - rpcList.shift() - } return Promise.resolve(rpcList) } diff --git a/app/scripts/controllers/transactions/enums.js b/app/scripts/controllers/transactions/enums.js new file mode 100644 index 000000000..be6f16e0d --- /dev/null +++ b/app/scripts/controllers/transactions/enums.js @@ -0,0 +1,12 @@ +const TRANSACTION_TYPE_CANCEL = 'cancel' +const TRANSACTION_TYPE_RETRY = 'retry' +const TRANSACTION_TYPE_STANDARD = 'standard' + +const TRANSACTION_STATUS_APPROVED = 'approved' + +module.exports = { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, + TRANSACTION_TYPE_STANDARD, + TRANSACTION_STATUS_APPROVED, +} diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 5d7d6d6da..9f2290924 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -11,6 +11,14 @@ const txUtils = require('./lib/util') const cleanErrorStack = require('../../lib/cleanErrorStack') const log = require('loglevel') const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker') +const { + TRANSACTION_TYPE_CANCEL, + TRANSACTION_TYPE_RETRY, + TRANSACTION_TYPE_STANDARD, + TRANSACTION_STATUS_APPROVED, +} = require('./enums') + +const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util') /** Transaction Controller is an aggregate of sub-controllers and trackers @@ -158,9 +166,16 @@ class TransactionController extends EventEmitter { async addUnapprovedTransaction (txParams) { // validate const normalizedTxParams = txUtils.normalizeTxParams(txParams) + // Assert the from address is the selected address + if (normalizedTxParams.from !== this.getSelectedAddress()) { + throw new Error(`Transaction from address isn't valid for this account`) + } txUtils.validateTxParams(normalizedTxParams) // construct txMeta - let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams }) + let txMeta = this.txStateManager.generateTxMeta({ + txParams: normalizedTxParams, + type: TRANSACTION_TYPE_STANDARD, + }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) @@ -214,6 +229,7 @@ class TransactionController extends EventEmitter { txParams: originalTxMeta.txParams, lastGasPrice, loadingDefaults: false, + type: TRANSACTION_TYPE_RETRY, }) this.addTx(txMeta) this.emit('newUnapprovedTx', txMeta) @@ -221,6 +237,40 @@ class TransactionController extends EventEmitter { } /** + * Creates a new approved transaction to attempt to cancel a previously submitted transaction. The + * new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to + * the sender's address, and has a higher gasPrice than that of the previous 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 {txMeta} + */ + async createCancelTransaction (originalTxId, customGasPrice) { + const originalTxMeta = this.txStateManager.getTx(originalTxId) + const { txParams } = originalTxMeta + const { gasPrice: lastGasPrice, from, nonce } = txParams + + const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10)) + const newTxMeta = this.txStateManager.generateTxMeta({ + txParams: { + from, + to: from, + nonce, + gas: '0x5208', + value: '0x0', + gasPrice: newGasPrice, + }, + lastGasPrice, + loadingDefaults: false, + status: TRANSACTION_STATUS_APPROVED, + type: TRANSACTION_TYPE_CANCEL, + }) + + this.addTx(newTxMeta) + await this.approveTransaction(newTxMeta.id) + return newTxMeta + } + + /** updates the txMeta in the txStateManager @param txMeta {Object} - the updated txMeta */ @@ -316,7 +366,40 @@ class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } - confirmTransaction (txId) { + /** + * Sets the status of the transaction to confirmed and sets the status of nonce duplicates as + * dropped if the txParams have data it will fetch the txReceipt + * @param {number} txId - The tx's ID + * @returns {Promise<void>} + */ + async confirmTransaction (txId) { + // get the txReceipt before marking the transaction confirmed + // to ensure the receipt is gotten before the ui revives the tx + const txMeta = this.txStateManager.getTx(txId) + + if (!txMeta) { + return + } + + try { + const txReceipt = await this.query.getTransactionReceipt(txMeta.hash) + + // It seems that sometimes the numerical values being returned from + // this.query.getTransactionReceipt are BN instances and not strings. + const gasUsed = typeof txReceipt.gasUsed !== 'string' + ? txReceipt.gasUsed.toString(16) + : txReceipt.gasUsed + + txMeta.txReceipt = { + ...txReceipt, + gasUsed, + } + + this.txStateManager.updateTx(txMeta, 'transactions#confirmTransaction - add txReceipt') + } catch (err) { + log.error(err) + } + this.txStateManager.setTxStatusConfirmed(txId) this._markNonceDuplicatesDropped(txId) } @@ -393,7 +476,7 @@ class TransactionController extends EventEmitter { }) this.txStateManager.getFilteredTxList({ - status: 'approved', + status: TRANSACTION_STATUS_APPROVED, }).forEach((txMeta) => { const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing') this.txStateManager.setTxStatusFailed(txMeta.id, txSignError) @@ -484,6 +567,7 @@ class TransactionController extends EventEmitter { Updates the memStore in transaction controller */ _updateMemstore () { + this.pendingTxTracker.updatePendingTxs() const unapprovedTxs = this.txStateManager.getUnapprovedTxList() const selectedAddressTxList = this.txStateManager.getFilteredTxList({ from: this.getSelectedAddress(), diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 28a18ca2e..58c48e34e 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -353,6 +353,7 @@ class TransactionStateManager extends EventEmitter { const txMeta = this.getTx(txId) txMeta.err = { message: err.toString(), + rpc: err.value, stack: err.stack, } this.updateTx(txMeta) @@ -399,6 +400,11 @@ class TransactionStateManager extends EventEmitter { */ _setTxStatus (txId, status) { const txMeta = this.getTx(txId) + + if (!txMeta) { + return + } + txMeta.status = status setTimeout(() => { try { diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 1a170c617..431702d63 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -5,10 +5,16 @@ const log = require('loglevel') const LocalMessageDuplexStream = require('post-message-stream') const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('metamask-inpage-provider') + restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') +console.warn('ATTENTION: In an effort to improve user privacy, MetaMask will ' + +'stop exposing user accounts to dapps by default beginning November 2nd, 2018. ' + +'Dapps should call provider.enable() in order to view and use accounts. Please see ' + +'https://bit.ly/2QQHXvF for complete information and up-to-date example code.') + // // setup plugin communication // @@ -22,6 +28,33 @@ var metamaskStream = new LocalMessageDuplexStream({ // compose the inpage provider var inpageProvider = new MetamaskInpageProvider(metamaskStream) +// Augment the provider with its enable method +inpageProvider.enable = function (options = {}) { + return new Promise((resolve, reject) => { + if (options.mockRejection) { + reject('User rejected account access') + } else { + inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { + if (error) { + reject(error) + } else { + resolve(response.result) + } + }) + } + }) +} + +// Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound +// `sendAsync` method on the prototype, causing `this` reference issues with drizzle +const proxiedInpageProvider = new Proxy(inpageProvider, { + // straight up lie that we deleted the property so that it doesnt + // throw an error in strict mode + deleteProperty: () => true, +}) + +window.ethereum = proxiedInpageProvider + // // setup web3 // @@ -33,7 +66,8 @@ if (typeof window.web3 !== 'undefined') { or MetaMask and another web3 extension. Please remove one and try again.`) } -var web3 = new Web3(inpageProvider) + +var web3 = new Web3(proxiedInpageProvider) web3.setProvider = function () { log.debug('MetaMask - overrode web3.setProvider') } diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index 3a52d5e8d..2e9340018 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -45,6 +45,9 @@ class AccountTracker { this._blockTracker = opts.blockTracker // blockTracker.currentBlock may be null this._currentBlockNumber = this._blockTracker.getCurrentBlock() + this._blockTracker.once('latest', blockNumber => { + this._currentBlockNumber = blockNumber + }) // bind function for easier listener syntax this._updateForBlock = this._updateForBlock.bind(this) } diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js index cce31c3d2..558391a06 100644 --- a/app/scripts/lib/auto-reload.js +++ b/app/scripts/lib/auto-reload.js @@ -2,18 +2,12 @@ module.exports = setupDappAutoReload function setupDappAutoReload (web3, observable) { // export web3 as a global, checking for usage - let hasBeenWarned = false let reloadInProgress = false let lastTimeUsed let lastSeenNetwork global.web3 = new Proxy(web3, { get: (_web3, key) => { - // show warning once on web3 access - if (!hasBeenWarned && key !== 'currentProvider') { - console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation') - hasBeenWarned = true - } // get the time of use lastTimeUsed = Date.now() // return value normally diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js deleted file mode 100644 index 221746467..000000000 --- a/app/scripts/lib/config-manager.js +++ /dev/null @@ -1,254 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const normalize = require('eth-sig-util').normalize -const { - MAINNET_RPC_URL, - ROPSTEN_RPC_URL, - KOVAN_RPC_URL, - RINKEBY_RPC_URL, -} = require('../controllers/network/enums') - -/* The config-manager is a convenience object - * wrapping a pojo-migrator. - * - * It exists mostly to allow the creation of - * convenience methods to access and persist - * particular portions of the state. - */ -module.exports = ConfigManager -function ConfigManager (opts) { - // ConfigManager is observable and will emit updates - this._subs = [] - this.store = opts.store -} - -ConfigManager.prototype.setConfig = function (config) { - var data = this.getData() - data.config = config - this.setData(data) - this._emitUpdates(config) -} - -ConfigManager.prototype.getConfig = function () { - var data = this.getData() - return data.config -} - -ConfigManager.prototype.setData = function (data) { - this.store.putState(data) -} - -ConfigManager.prototype.getData = function () { - return this.store.getState() -} - -ConfigManager.prototype.setPasswordForgotten = function (passwordForgottenState) { - const data = this.getData() - data.forgottenPassword = passwordForgottenState - this.setData(data) -} - -ConfigManager.prototype.getPasswordForgotten = function (passwordForgottenState) { - const data = this.getData() - return data.forgottenPassword -} - -ConfigManager.prototype.setWallet = function (wallet) { - var data = this.getData() - data.wallet = wallet - this.setData(data) -} - -ConfigManager.prototype.setVault = function (encryptedString) { - var data = this.getData() - data.vault = encryptedString - this.setData(data) -} - -ConfigManager.prototype.getVault = function () { - var data = this.getData() - return data.vault -} - -ConfigManager.prototype.getKeychains = function () { - return this.getData().keychains || [] -} - -ConfigManager.prototype.setKeychains = function (keychains) { - var data = this.getData() - data.keychains = keychains - this.setData(data) -} - -ConfigManager.prototype.getSelectedAccount = function () { - var config = this.getConfig() - return config.selectedAccount -} - -ConfigManager.prototype.setSelectedAccount = function (address) { - var config = this.getConfig() - config.selectedAccount = ethUtil.addHexPrefix(address) - this.setConfig(config) -} - -ConfigManager.prototype.getWallet = function () { - return this.getData().wallet -} - -// Takes a boolean -ConfigManager.prototype.setShowSeedWords = function (should) { - var data = this.getData() - data.showSeedWords = should - this.setData(data) -} - - -ConfigManager.prototype.getShouldShowSeedWords = function () { - var data = this.getData() - return data.showSeedWords -} - -ConfigManager.prototype.setSeedWords = function (words) { - var data = this.getData() - data.seedWords = words - this.setData(data) -} - -ConfigManager.prototype.getSeedWords = function () { - var data = this.getData() - return data.seedWords -} -ConfigManager.prototype.setRpcTarget = function (rpcUrl) { - var config = this.getConfig() - config.provider = { - type: 'rpc', - rpcTarget: rpcUrl, - } - this.setConfig(config) -} - -ConfigManager.prototype.setProviderType = function (type) { - var config = this.getConfig() - config.provider = { - type: type, - } - this.setConfig(config) -} - -ConfigManager.prototype.useEtherscanProvider = function () { - var config = this.getConfig() - config.provider = { - type: 'etherscan', - } - this.setConfig(config) -} - -ConfigManager.prototype.getProvider = function () { - var config = this.getConfig() - return config.provider -} - -ConfigManager.prototype.getCurrentRpcAddress = function () { - var provider = this.getProvider() - if (!provider) return null - switch (provider.type) { - - case 'mainnet': - return MAINNET_RPC_URL - - case 'ropsten': - return ROPSTEN_RPC_URL - - case 'kovan': - return KOVAN_RPC_URL - - case 'rinkeby': - return RINKEBY_RPC_URL - - default: - return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL - } -} - -// -// Tx -// - -ConfigManager.prototype.getTxList = function () { - var data = this.getData() - if (data.transactions !== undefined) { - return data.transactions - } else { - return [] - } -} - -ConfigManager.prototype.setTxList = function (txList) { - var data = this.getData() - data.transactions = txList - this.setData(data) -} - - -// wallet nickname methods - -ConfigManager.prototype.getWalletNicknames = function () { - var data = this.getData() - const nicknames = ('walletNicknames' in data) ? data.walletNicknames : {} - return nicknames -} - -ConfigManager.prototype.nicknameForWallet = function (account) { - const address = normalize(account) - const nicknames = this.getWalletNicknames() - return nicknames[address] -} - -ConfigManager.prototype.setNicknameForWallet = function (account, nickname) { - const address = normalize(account) - const nicknames = this.getWalletNicknames() - nicknames[address] = nickname - var data = this.getData() - data.walletNicknames = nicknames - this.setData(data) -} - -// observable - -ConfigManager.prototype.getSalt = function () { - var data = this.getData() - return data.salt -} - -ConfigManager.prototype.setSalt = function (salt) { - var data = this.getData() - data.salt = salt - this.setData(data) -} - -ConfigManager.prototype.subscribe = function (fn) { - this._subs.push(fn) - var unsubscribe = this.unsubscribe.bind(this, fn) - return unsubscribe -} - -ConfigManager.prototype.unsubscribe = function (fn) { - var index = this._subs.indexOf(fn) - if (index !== -1) this._subs.splice(index, 1) -} - -ConfigManager.prototype._emitUpdates = function (state) { - this._subs.forEach(function (handler) { - handler(state) - }) -} - -ConfigManager.prototype.setLostAccounts = function (lostAccounts) { - var data = this.getData() - data.lostAccounts = lostAccounts - this.setData(data) -} - -ConfigManager.prototype.getLostAccounts = function () { - var data = this.getData() - return data.lostAccounts || [] -} diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js index 38682b916..8b08453c4 100644 --- a/app/scripts/lib/ipfsContent.js +++ b/app/scripts/lib/ipfsContent.js @@ -5,6 +5,8 @@ module.exports = function (provider) { function ipfsContent (details) { const name = details.url.substring(7, details.url.length - 1) let clearTime = null + if (/^.+\.eth$/.test(name) === false) return + extension.tabs.query({active: true}, tab => { extension.tabs.update(tab.id, { url: 'loading.html' }) @@ -34,7 +36,7 @@ module.exports = function (provider) { return { cancel: true } } - extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']}) + extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/'], types: ['main_frame']}) return { remove () { diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js index 47925b94b..e86629590 100644 --- a/app/scripts/lib/message-manager.js +++ b/app/scripts/lib/message-manager.js @@ -272,6 +272,6 @@ function normalizeMsgData (data) { return data } else { // data is unicode, convert to hex - return ethUtil.bufferToHex(new Buffer(data, 'utf8')) + return ethUtil.bufferToHex(Buffer.from(data, 'utf8')) } } diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js index fc2cccdf1..fdb94f5ec 100644 --- a/app/scripts/lib/personal-message-manager.js +++ b/app/scripts/lib/personal-message-manager.js @@ -285,7 +285,7 @@ module.exports = class PersonalMessageManager extends EventEmitter { log.debug(`Message was not hex encoded, interpreting as utf8.`) } - return ethUtil.bufferToHex(new Buffer(data, 'utf8')) + return ethUtil.bufferToHex(Buffer.from(data, 'utf8')) } } diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js index e5e1c94b3..b10145f3b 100644 --- a/app/scripts/lib/typed-message-manager.js +++ b/app/scripts/lib/typed-message-manager.js @@ -4,6 +4,7 @@ const createId = require('./random-id') const assert = require('assert') const sigUtil = require('eth-sig-util') const log = require('loglevel') +const jsonschema = require('jsonschema') /** * Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a @@ -17,7 +18,7 @@ const log = require('loglevel') * @property {Object} msgParams.from The address that is making the signature request. * @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request * @property {number} time The epoch time at which the this message was created - * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected' + * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored' * @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will * always have a 'eth_signTypedData' type. * @@ -26,17 +27,10 @@ const log = require('loglevel') module.exports = class TypedMessageManager extends EventEmitter { /** * Controller in charge of managing - storing, adding, removing, updating - TypedMessage. - * - * @typedef {Object} TypedMessage - * @param {Object} opts @deprecated - * @property {Object} memStore The observable store where TypedMessage are saved. - * @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state - * @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs - * @property {array} messages Holds all messages that have been created by this TypedMessage - * */ - constructor (opts) { + constructor ({ networkController }) { super() + this.networkController = networkController this.memStore = new ObservableStore({ unapprovedTypedMessages: {}, unapprovedTypedMessagesCount: 0, @@ -76,15 +70,17 @@ module.exports = class TypedMessageManager extends EventEmitter { * @returns {promise} When the message has been signed or rejected * */ - addUnapprovedMessageAsync (msgParams, req) { + addUnapprovedMessageAsync (msgParams, req, version) { return new Promise((resolve, reject) => { - const msgId = this.addUnapprovedMessage(msgParams, req) + const msgId = this.addUnapprovedMessage(msgParams, req, version) this.once(`${msgId}:finished`, (data) => { switch (data.status) { case 'signed': return resolve(data.rawSig) case 'rejected': return reject(new Error('MetaMask Message Signature: User denied message signature.')) + case 'errored': + return reject(new Error(`MetaMask Message Signature: ${data.error}`)) default: return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)) } @@ -102,7 +98,8 @@ module.exports = class TypedMessageManager extends EventEmitter { * @returns {number} The id of the newly created TypedMessage. * */ - addUnapprovedMessage (msgParams, req) { + addUnapprovedMessage (msgParams, req, version) { + msgParams.version = version this.validateParams(msgParams) // add origin from request if (req) msgParams.origin = req.origin @@ -132,14 +129,33 @@ module.exports = class TypedMessageManager extends EventEmitter { * */ validateParams (params) { - assert.equal(typeof params, 'object', 'Params should ben an object.') - assert.ok('data' in params, 'Params must include a data field.') - assert.ok('from' in params, 'Params must include a from field.') - assert.ok(Array.isArray(params.data), 'Data should be an array.') - assert.equal(typeof params.from, 'string', 'From field must be a string.') - assert.doesNotThrow(() => { - sigUtil.typedSignatureHash(params.data) - }, 'Expected EIP712 typed data') + switch (params.version) { + case 'V1': + assert.equal(typeof params, 'object', 'Params should ben an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.ok(Array.isArray(params.data), 'Data should be an array.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.doesNotThrow(() => { + sigUtil.typedSignatureHash(params.data) + }, 'Expected EIP712 typed data') + break + case 'V3': + let data + assert.equal(typeof params, 'object', 'Params should be an object.') + assert.ok('data' in params, 'Params must include a data field.') + assert.ok('from' in params, 'Params must include a from field.') + assert.equal(typeof params.from, 'string', 'From field must be a string.') + assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.') + assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.') + const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA) + assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`) + assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.') + const chainId = data.domain.chainId + const activeChainId = parseInt(this.networkController.getNetworkState()) + chainId && assert.equal(chainId, activeChainId, `Provided chainId (${chainId}) must match the active chainId (${activeChainId})`) + break + } } /** @@ -214,6 +230,7 @@ module.exports = class TypedMessageManager extends EventEmitter { */ prepMsgForSigning (msgParams) { delete msgParams.metamaskId + delete msgParams.version return Promise.resolve(msgParams) } @@ -227,6 +244,19 @@ module.exports = class TypedMessageManager extends EventEmitter { this._setMsgStatus(msgId, 'rejected') } + /** + * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus. + * + * @param {number} msgId The id of the TypedMessage to error + * + */ + errorMessage (msgId, error) { + const msg = this.getMsg(msgId) + msg.error = error + this._updateMsg(msg) + this._setMsgStatus(msgId, 'errored') + } + // // PRIVATE METHODS // @@ -250,7 +280,7 @@ module.exports = class TypedMessageManager extends EventEmitter { msg.status = status this._updateMsg(msg) this.emit(`${msgId}:${status}`, msg) - if (status === 'rejected' || status === 'signed') { + if (status === 'rejected' || status === 'signed' || status === 'errored') { this.emit(`${msgId}:finished`, msg) } } 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) + } } diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js deleted file mode 100644 index 7a4578ea7..000000000 --- a/app/scripts/migrations/_multi-keyring.js +++ /dev/null @@ -1,50 +0,0 @@ -const version = 5 - -/* - -This is an incomplete migration bc it requires post-decrypted data -which we dont have access to at the time of this writing. - -*/ - -const ObservableStore = require('obs-store') -const ConfigManager = require('../../app/scripts/lib/config-manager') -const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator') -const KeyringController = require('eth-keyring-controller') - -const password = 'obviously not correct' - -module.exports = { - version, - - migrate: function (versionedData) { - versionedData.meta.version = version - - const store = new ObservableStore(versionedData.data) - const configManager = new ConfigManager({ store }) - const idStoreMigrator = new IdentityStoreMigrator({ configManager }) - const keyringController = new KeyringController({ - configManager: configManager, - }) - - // attempt to migrate to multiVault - return idStoreMigrator.migratedVaultForPassword(password) - .then((result) => { - // skip if nothing to migrate - if (!result) return Promise.resolve(versionedData) - delete versionedData.data.wallet - // create new keyrings - const privKeys = result.lostAccounts.map(acct => acct.privateKey) - return Promise.all([ - keyringController.restoreKeyring(result.serialized), - keyringController.restoreKeyring({ type: 'Simple Key Pair', data: privKeys }), - ]).then(() => { - return keyringController.persistAllKeyrings(password) - }).then(() => { - // copy result on to state object - versionedData.data = store.get() - return Promise.resolve(versionedData) - }) - }) - }, -} diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js index 2def4371e..ce686d9d1 100644 --- a/app/scripts/notice-controller.js +++ b/app/scripts/notice-controller.js @@ -7,7 +7,7 @@ const uniqBy = require('lodash.uniqby') module.exports = class NoticeController extends EventEmitter { - constructor (opts) { + constructor (opts = {}) { super() this.noticePoller = null this.firstVersion = opts.firstVersion diff --git a/app/scripts/phishing-detect.js b/app/scripts/phishing-detect.js new file mode 100644 index 000000000..0889c831e --- /dev/null +++ b/app/scripts/phishing-detect.js @@ -0,0 +1,59 @@ +window.onload = function () { + if (window.location.pathname === '/phishing.html') { + const {hostname} = parseHash() + document.getElementById('esdbLink').innerHTML = '<b>To read more about this scam, navigate to: <a href="https://etherscamdb.info/domain/' + hostname + '"> https://etherscamdb.info/domain/' + hostname + '</a></b>' + } +} + +const querystring = require('querystring') +const dnode = require('dnode') +const { EventEmitter } = require('events') +const PortStream = require('extension-port-stream') +const extension = require('extensionizer') +const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex +const { getEnvironmentType } = require('./lib/util') +const ExtensionPlatform = require('./platforms/extension') + +document.addEventListener('DOMContentLoaded', start) + +function start () { + const windowType = getEnvironmentType(window.location.href) + + global.platform = new ExtensionPlatform() + global.METAMASK_UI_TYPE = windowType + + const extensionPort = extension.runtime.connect({ name: windowType }) + const connectionStream = new PortStream(extensionPort) + const mx = setupMultiplex(connectionStream) + setupControllerConnection(mx.createStream('controller'), (err, metaMaskController) => { + if (err) { + return + } + + const suspect = parseHash() + const unsafeContinue = () => { + window.location.href = suspect.href + } + const continueLink = document.getElementById('unsafe-continue') + continueLink.addEventListener('click', () => { + metaMaskController.whitelistPhishingDomain(suspect.hostname) + unsafeContinue() + }) + }) +} + +function setupControllerConnection (connectionStream, cb) { + const eventEmitter = new EventEmitter() + const accountManagerDnode = dnode({ + sendUpdate (state) { + eventEmitter.emit('update', state) + }, + }) + connectionStream.pipe(accountManagerDnode).pipe(connectionStream) + accountManagerDnode.once('remote', (accountManager) => cb(null, accountManager)) +} + +function parseHash () { + const hash = window.location.hash.substring(1) + return querystring.parse(hash) +} |