From 222e62d7f10ffe22dd606aea9c15e1547986c4ab Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Mon, 17 Sep 2018 20:04:10 -0700 Subject: Bug Fix: #1789 and #4525 eth.getCode() with no contract --- .../controllers/transactions/tx-gas-utils.js | 23 +++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 3dd45507f..5ec728085 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -7,6 +7,8 @@ const { const { addHexPrefix } = require('ethereumjs-util') const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. +import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/constants/error-keys' + /** tx-gas-utils are gas utility methods for Transaction manager its passed ethquery @@ -32,6 +34,7 @@ class TxGasUtil { } catch (err) { txMeta.simulationFails = { reason: err.message, + errorKey: err.errorKey, } return txMeta } @@ -56,16 +59,22 @@ class TxGasUtil { return txParams.gas } - // if recipient has no code, gas is 21k max: const recipient = txParams.to const hasRecipient = Boolean(recipient) - let code - if (recipient) code = await this.query.getCode(recipient) - if (hasRecipient && (!code || code === '0x')) { - txParams.gas = SIMPLE_GAS_COST - txMeta.simpleSend = true // Prevents buffer addition - return SIMPLE_GAS_COST + if (hasRecipient) { + let code = await this.query.getCode(recipient) + + // If there's data in the params, but there's no code, it's not a valid contract + // For no code, Infura will return '0x', and Ganache will return '0x0' + if (txParams.data && (!code || code === '0x' || code === '0x0')) { + throw {errorKey: TRANSACTION_NO_CONTRACT_ERROR_KEY} + } + else if (!code) { + txParams.gas = SIMPLE_GAS_COST // For a standard ETH send, gas is 21k max + txMeta.simpleSend = true // Prevents buffer addition + return SIMPLE_GAS_COST + } } // if not, fall back to block gasLimit -- cgit From 4cc0b1ef01573e1541d18bdcd89650e1db32ae9a Mon Sep 17 00:00:00 2001 From: Howard Braham Date: Fri, 28 Sep 2018 11:01:34 -0700 Subject: ganache-core merged my PR, so I changed some comments to clarify that ganache-core v2.2.1 and below will return the non-standard '0x0' --- app/scripts/controllers/transactions/tx-gas-utils.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 5ec728085..ac57dfe1d 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -63,14 +63,15 @@ class TxGasUtil { const hasRecipient = Boolean(recipient) if (hasRecipient) { - let code = await this.query.getCode(recipient) + const code = await this.query.getCode(recipient) // If there's data in the params, but there's no code, it's not a valid contract - // For no code, Infura will return '0x', and Ganache will return '0x0' + // For no code, Infura will return '0x', and ganache-core v2.2.1 will return '0x0' if (txParams.data && (!code || code === '0x' || code === '0x0')) { - throw {errorKey: TRANSACTION_NO_CONTRACT_ERROR_KEY} - } - else if (!code) { + const err = new Error() + err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY + throw err + } else if (!code) { txParams.gas = SIMPLE_GAS_COST // For a standard ETH send, gas is 21k max txMeta.simpleSend = true // Prevents buffer addition return SIMPLE_GAS_COST -- cgit From 600f755dbf8d4cfdc152e3d521b537ee9a046a35 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 9 Oct 2018 23:17:05 -0400 Subject: tx-gas-utils - improve format + comments --- .../controllers/transactions/tx-gas-utils.js | 30 +++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index ac57dfe1d..436900715 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -62,28 +62,34 @@ class TxGasUtil { const recipient = txParams.to const hasRecipient = Boolean(recipient) + // see if we can set the gas based on the recipient if (hasRecipient) { const code = await this.query.getCode(recipient) - - // If there's data in the params, but there's no code, it's not a valid contract - // For no code, Infura will return '0x', and ganache-core v2.2.1 will return '0x0' - if (txParams.data && (!code || code === '0x' || code === '0x0')) { - const err = new Error() - err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY - throw err - } else if (!code) { - txParams.gas = SIMPLE_GAS_COST // For a standard ETH send, gas is 21k max - txMeta.simpleSend = true // Prevents buffer addition + // For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0' + const codeIsEmpty = !code || code === '0x' || code === '0x0' + + if (codeIsEmpty) { + // if there's data in the params, but there's no contract code, it's not a valid transaction + if (txParams.data) { + const err = new Error() + err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY + throw err + } + + // This is a standard ether simple send, gas requirement is exactly 21k + txParams.gas = SIMPLE_GAS_COST + // prevents buffer addition + txMeta.simpleSend = true return SIMPLE_GAS_COST } } - // if not, fall back to block gasLimit + // fallback to block gasLimit const blockGasLimitBN = hexToBn(blockGasLimitHex) const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20) txParams.gas = bnToHex(saferGasLimitBN) - // run tx + // estimate tx gas requirements return await this.query.estimateGas(txParams) } -- cgit From 26b30a031dacb23cc0a8ec742ece972acdd48594 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 19 Oct 2018 04:36:36 -0400 Subject: sentry - move isBrave decoration to insides of try-catch --- app/scripts/lib/setupRaven.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js index e6e511640..e1dc4bb8b 100644 --- a/app/scripts/lib/setupRaven.js +++ b/app/scripts/lib/setupRaven.js @@ -24,10 +24,11 @@ function setupRaven (opts) { const client = Raven.config(ravenTarget, { release, transport: function (opts) { - opts.data.extra.isBrave = isBrave const report = opts.data try { + // mark browser as brave or not + report.extra.isBrave = isBrave // handle error-like non-error exceptions rewriteErrorLikeExceptions(report) // simplify certain complex error messages (e.g. Ethjs) -- cgit From 65aa0a1d14b0b18d77b537b216d03ce5d9fca725 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 19 Oct 2018 04:51:03 -0400 Subject: blacklist - throw errors on request/parse failure --- app/scripts/controllers/blacklist.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index 89c7cc888..5a5ec1c31 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -83,8 +83,23 @@ class BlacklistController { * */ async updatePhishingList () { - const response = await fetch('https://api.infura.io/v2/blacklist') - const phishing = await response.json() + // make request + let response + try { + response = await fetch('https://api.infura.io/v2/blacklist') + } catch (err) { + throw new Error(`BlacklistController - failed to fetch blacklist:\n${err.stack}`) + } + // parse response + let rawResponse + let phishing + try { + const rawResponse = await response.text() + phishing = JSON.parse(rawResponse) + } catch (err) { + throw new Error(`BlacklistController - failed to parse blacklist:\n${rawResponse}`) + } + // update current blacklist this.store.updateState({ phishing }) this._setupPhishingDetector(phishing) return phishing @@ -97,9 +112,9 @@ class BlacklistController { */ scheduleUpdates () { if (this._phishingUpdateIntervalRef) return - this.updatePhishingList().catch(log.warn) + this.updatePhishingList() this._phishingUpdateIntervalRef = setInterval(() => { - this.updatePhishingList().catch(log.warn) + this.updatePhishingList() }, POLLING_INTERVAL) } -- cgit From 2394881511928e414ca99cac2a46f30a7703ae91 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 19 Oct 2018 04:58:19 -0400 Subject: currency - throw errors on failure --- app/scripts/controllers/currency.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index d5bc5fe2b..619c515fa 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -107,14 +107,28 @@ class CurrencyController { let currentCurrency try { currentCurrency = this.getCurrentCurrency() - const response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) - const parsedResponse = await response.json() + let response + try { + response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) + } catch (err) { + throw new Error(`CurrencyController - Failed to request currency from Infura:\n${err.stack}`) + } + let rawResponse + let parsedResponse + try { + rawResponse = await response.text() + parsedResponse = JSON.parse(rawResponse) + } catch () { + throw new Error(`CurrencyController - Failed to parse response "${rawResponse}"`) + } this.setConversionRate(Number(parsedResponse.bid)) this.setConversionDate(Number(parsedResponse.timestamp)) } catch (err) { - log.warn(`MetaMask - Failed to query currency conversion:`, currentCurrency, err) + // reset current conversion rate this.setConversionRate(0) this.setConversionDate('N/A') + // throw error + throw new Error(`CurrencyController - Failed to query rate for currency "${currentCurrency}":\n${err.stack}`) } } -- cgit From 3e3d4b9ddef032fe81419a516e65eb62ed664cb9 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 19 Oct 2018 05:30:21 -0400 Subject: sentry - failed txs - namespace txMeta for better readability --- app/scripts/lib/reportFailedTxToSentry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/lib/reportFailedTxToSentry.js b/app/scripts/lib/reportFailedTxToSentry.js index df5661e59..fc6fbac24 100644 --- a/app/scripts/lib/reportFailedTxToSentry.js +++ b/app/scripts/lib/reportFailedTxToSentry.js @@ -11,6 +11,6 @@ function reportFailedTxToSentry ({ raven, txMeta }) { const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message) raven.captureMessage(errorMessage, { // "extra" key is required by Sentry - extra: txMeta, + extra: { txMeta }, }) } -- cgit From b85ae55cd59b4f8fe52ec75e1b7c6efe52a02e3f Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 19 Oct 2018 06:42:53 -0400 Subject: fetch debugger - only append source stack if no stack is present --- app/scripts/lib/setupFetchDebugging.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/lib/setupFetchDebugging.js b/app/scripts/lib/setupFetchDebugging.js index dd87b65a6..c1ef22d21 100644 --- a/app/scripts/lib/setupFetchDebugging.js +++ b/app/scripts/lib/setupFetchDebugging.js @@ -2,7 +2,7 @@ module.exports = setupFetchDebugging // // This is a utility to help resolve cases where `window.fetch` throws a -// `TypeError: Failed to Fetch` without any stack or context for the request +// `TypeError: Failed to Fetch` without any stack or context for the request // https://github.com/getsentry/sentry-javascript/pull/1293 // @@ -17,9 +17,11 @@ function setupFetchDebugging() { try { return await originalFetch.call(window, ...args) } catch (err) { - console.warn('FetchDebugger - fetch encountered an Error', err) - console.warn('FetchDebugger - overriding stack to point of original call') - err.stack = initialStack + if (!err.stack) { + console.warn('FetchDebugger - fetch encountered an Error without a stack', err) + console.warn('FetchDebugger - overriding stack to point of original call') + err.stack = initialStack + } throw err } } -- cgit From a57d267dcbfa1a75a5ce3295e51825561e42c58d Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 19 Oct 2018 07:08:04 -0400 Subject: lint fix --- app/scripts/controllers/blacklist.js | 1 - app/scripts/controllers/currency.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index 5a5ec1c31..6ee89259c 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -1,7 +1,6 @@ const ObservableStore = require('obs-store') const extend = require('xtend') const PhishingDetector = require('eth-phishing-detect/src/detector') -const log = require('loglevel') // compute phishing lists const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index 619c515fa..6cb39d39a 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -1,6 +1,5 @@ const ObservableStore = require('obs-store') const extend = require('xtend') -const log = require('loglevel') // every ten minutes const POLLING_INTERVAL = 600000 @@ -118,7 +117,7 @@ class CurrencyController { try { rawResponse = await response.text() parsedResponse = JSON.parse(rawResponse) - } catch () { + } catch (err) { throw new Error(`CurrencyController - Failed to parse response "${rawResponse}"`) } this.setConversionRate(Number(parsedResponse.bid)) -- cgit From 31175dcb24d836650469775a50289bfc131bcd18 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 19 Oct 2018 07:18:16 -0400 Subject: blacklist + currency - report error via log instead of throw --- app/scripts/controllers/blacklist.js | 7 +++++-- app/scripts/controllers/currency.js | 10 +++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index 6ee89259c..e55b09d03 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -1,6 +1,7 @@ const ObservableStore = require('obs-store') const extend = require('xtend') const PhishingDetector = require('eth-phishing-detect/src/detector') +const log = require('loglevel') // compute phishing lists const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') @@ -87,7 +88,8 @@ class BlacklistController { try { response = await fetch('https://api.infura.io/v2/blacklist') } catch (err) { - throw new Error(`BlacklistController - failed to fetch blacklist:\n${err.stack}`) + log.error(new Error(`BlacklistController - failed to fetch blacklist:\n${err.stack}`)) + return } // parse response let rawResponse @@ -96,7 +98,8 @@ class BlacklistController { const rawResponse = await response.text() phishing = JSON.parse(rawResponse) } catch (err) { - throw new Error(`BlacklistController - failed to parse blacklist:\n${rawResponse}`) + log.error(new Error(`BlacklistController - failed to parse blacklist:\n${rawResponse}`)) + return } // update current blacklist this.store.updateState({ phishing }) diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index 6cb39d39a..6b82265f6 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const extend = require('xtend') +const log = require('loglevel') // every ten minutes const POLLING_INTERVAL = 600000 @@ -110,7 +111,8 @@ class CurrencyController { try { response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) } catch (err) { - throw new Error(`CurrencyController - Failed to request currency from Infura:\n${err.stack}`) + log.error(new Error(`CurrencyController - Failed to request currency from Infura:\n${err.stack}`)) + return } let rawResponse let parsedResponse @@ -118,7 +120,8 @@ class CurrencyController { rawResponse = await response.text() parsedResponse = JSON.parse(rawResponse) } catch (err) { - throw new Error(`CurrencyController - Failed to parse response "${rawResponse}"`) + log.error(new Error(`CurrencyController - Failed to parse response "${rawResponse}"`)) + return } this.setConversionRate(Number(parsedResponse.bid)) this.setConversionDate(Number(parsedResponse.timestamp)) @@ -127,7 +130,8 @@ class CurrencyController { this.setConversionRate(0) this.setConversionDate('N/A') // throw error - throw new Error(`CurrencyController - Failed to query rate for currency "${currentCurrency}":\n${err.stack}`) + log.error(new Error(`CurrencyController - Failed to query rate for currency "${currentCurrency}":\n${err.stack}`)) + return } } -- cgit From 2f6530a4948743fe156dc3519f04bd44f7c6e2ae Mon Sep 17 00:00:00 2001 From: hackyminer Date: Sat, 20 Oct 2018 00:45:59 +0900 Subject: support both eth_chainId and net_version get the real chainId using eth_chainId and use net_version as a fallback --- app/scripts/controllers/network/network.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index c1667d9a6..b386161da 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -86,10 +86,17 @@ module.exports = class NetworkController extends EventEmitter { return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } const ethQuery = new EthQuery(this._provider) - ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { - if (err) return this.setNetworkState('loading') - log.info('web3.getNetwork returned ' + network) - this.setNetworkState(network) + ethQuery.sendAsync({ method: 'eth_chainId' }, (err, chainId) => { + if (err) { + ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { + if (err) return this.setNetworkState('loading') + log.info('web3.getNetwork returned net_version = ' + network) + this.setNetworkState(network) + }) + return + } + log.info('web3.getNetwork returned chainId = ' + parseInt(chainId)) + this.setNetworkState(parseInt(chainId)) }) } -- cgit From e3fda83ab209af7836ba93bfaba215c271d73e8a Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 20 Oct 2018 02:22:50 -0400 Subject: sentry - replace raven-js with sentry/browser --- app/scripts/background.js | 12 ++-- app/scripts/lib/reportFailedTxToSentry.js | 4 +- app/scripts/lib/setupRaven.js | 101 ------------------------------ app/scripts/lib/setupSentry.js | 91 +++++++++++++++++++++++++++ app/scripts/ui.js | 4 +- 5 files changed, 101 insertions(+), 111 deletions(-) delete mode 100644 app/scripts/lib/setupRaven.js create mode 100644 app/scripts/lib/setupSentry.js (limited to 'app/scripts') diff --git a/app/scripts/background.js b/app/scripts/background.js index 509a0001d..2455608aa 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -23,7 +23,7 @@ const createStreamSink = require('./lib/createStreamSink') const NotificationManager = require('./lib/notification-manager.js') const MetamaskController = require('./metamask-controller') const rawFirstTimeState = require('./first-time-state') -const setupRaven = require('./lib/setupRaven') +const setupSentry = require('./lib/setupSentry') const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry') const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const EdgeEncryptor = require('./edge-encryptor') @@ -50,7 +50,7 @@ global.METAMASK_NOTIFIER = notificationManager // setup sentry error reporting const release = platform.getVersion() -const raven = setupRaven({ release }) +const sentry = setupSentry({ release }) // browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser // Internet Explorer 6-11 @@ -197,14 +197,14 @@ async function loadStateFromPersistence () { // we were able to recover (though it might be old) versionedData = diskStoreState const vaultStructure = getObjStructure(versionedData) - raven.captureMessage('MetaMask - Empty vault found - recovered from diskStore', { + sentry.captureMessage('MetaMask - Empty vault found - recovered from diskStore', { // "extra" key is required by Sentry extra: { vaultStructure }, }) } else { // unable to recover, clear state versionedData = migrator.generateInitialState(firstTimeState) - raven.captureMessage('MetaMask - Empty vault found - unable to recover') + sentry.captureMessage('MetaMask - Empty vault found - unable to recover') } } @@ -212,7 +212,7 @@ async function loadStateFromPersistence () { migrator.on('error', (err) => { // get vault structure without secrets const vaultStructure = getObjStructure(versionedData) - raven.captureException(err, { + sentry.captureException(err, { // "extra" key is required by Sentry extra: { vaultStructure }, }) @@ -279,7 +279,7 @@ function setupController (initState, initLangCode) { if (status !== 'failed') return const txMeta = controller.txController.txStateManager.getTx(txId) try { - reportFailedTxToSentry({ raven, txMeta }) + reportFailedTxToSentry({ sentry, txMeta }) } catch (e) { console.error(e) } diff --git a/app/scripts/lib/reportFailedTxToSentry.js b/app/scripts/lib/reportFailedTxToSentry.js index fc6fbac24..de4d57145 100644 --- a/app/scripts/lib/reportFailedTxToSentry.js +++ b/app/scripts/lib/reportFailedTxToSentry.js @@ -7,9 +7,9 @@ module.exports = reportFailedTxToSentry // for sending to sentry // -function reportFailedTxToSentry ({ raven, txMeta }) { +function reportFailedTxToSentry ({ sentry, txMeta }) { const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message) - raven.captureMessage(errorMessage, { + sentry.captureMessage(errorMessage, { // "extra" key is required by Sentry extra: { txMeta }, }) diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js deleted file mode 100644 index e1dc4bb8b..000000000 --- a/app/scripts/lib/setupRaven.js +++ /dev/null @@ -1,101 +0,0 @@ -const Raven = require('raven-js') -const METAMASK_DEBUG = process.env.METAMASK_DEBUG -const extractEthjsErrorMessage = require('./extractEthjsErrorMessage') -const PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505' -const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' - -module.exports = setupRaven - -// Setup raven / sentry remote error reporting -function setupRaven (opts) { - const { release } = opts - let ravenTarget - // detect brave - const isBrave = Boolean(window.chrome.ipcRenderer) - - if (METAMASK_DEBUG) { - console.log('Setting up Sentry Remote Error Reporting: DEV') - ravenTarget = DEV - } else { - console.log('Setting up Sentry Remote Error Reporting: PROD') - ravenTarget = PROD - } - - const client = Raven.config(ravenTarget, { - release, - transport: function (opts) { - const report = opts.data - - try { - // mark browser as brave or not - report.extra.isBrave = isBrave - // handle error-like non-error exceptions - rewriteErrorLikeExceptions(report) - // simplify certain complex error messages (e.g. Ethjs) - simplifyErrorMessages(report) - // modify report urls - rewriteReportUrls(report) - } catch (err) { - console.warn(err) - } - // make request normally - client._makeRequest(opts) - }, - }) - client.install() - - return Raven -} - -function rewriteErrorLikeExceptions (report) { - // handle errors that lost their error-ness in serialization (e.g. dnode) - rewriteErrorMessages(report, (errorMessage) => { - if (!errorMessage.includes('Non-Error exception captured with keys:')) return errorMessage - if (!(report.extra && report.extra.__serialized__ && report.extra.__serialized__.message)) return errorMessage - return `Non-Error Exception: ${report.extra.__serialized__.message}` - }) -} - -function simplifyErrorMessages (report) { - rewriteErrorMessages(report, (errorMessage) => { - // simplify ethjs error messages - errorMessage = extractEthjsErrorMessage(errorMessage) - // simplify 'Transaction Failed: known transaction' - if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) { - // cut the hash from the error message - errorMessage = 'Transaction Failed: known transaction' - } - return errorMessage - }) -} - -function rewriteErrorMessages (report, rewriteFn) { - // rewrite top level message - if (typeof report.message === 'string') report.message = rewriteFn(report.message) - // rewrite each exception message - if (report.exception && report.exception.values) { - report.exception.values.forEach(item => { - if (typeof item.value === 'string') item.value = rewriteFn(item.value) - }) - } -} - -function rewriteReportUrls (report) { - // update request url - report.request.url = toMetamaskUrl(report.request.url) - // update exception stack trace - if (report.exception && report.exception.values) { - report.exception.values.forEach(item => { - item.stacktrace.frames.forEach(frame => { - frame.filename = toMetamaskUrl(frame.filename) - }) - }) - } -} - -function toMetamaskUrl (origUrl) { - const filePath = origUrl.split(location.origin)[1] - if (!filePath) return origUrl - const metamaskUrl = `metamask${filePath}` - return metamaskUrl -} diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js new file mode 100644 index 000000000..aa8d72194 --- /dev/null +++ b/app/scripts/lib/setupSentry.js @@ -0,0 +1,91 @@ +const Sentry = require('@sentry/browser') +const METAMASK_DEBUG = process.env.METAMASK_DEBUG +const extractEthjsErrorMessage = require('./extractEthjsErrorMessage') +const SENTRY_DSN_PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505' +const SENTRY_DSN_DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496' + +module.exports = setupSentry + +// Setup sentry remote error reporting +function setupSentry (opts) { + const { release } = opts + let sentryTarget + // detect brave + const isBrave = Boolean(window.chrome.ipcRenderer) + + if (METAMASK_DEBUG) { + console.log('Setting up Sentry Remote Error Reporting: SENTRY_DSN_DEV') + sentryTarget = SENTRY_DSN_DEV + } else { + console.log('Setting up Sentry Remote Error Reporting: SENTRY_DSN_PROD') + sentryTarget = SENTRY_DSN_PROD + } + + Sentry.init({ + dsn: sentryTarget, + debug: METAMASK_DEBUG, + release, + beforeSend: (report) => rewriteReport(report), + }) + + Sentry.configureScope(scope => { + scope.setExtra('isBrave', isBrave) + }) + + function rewriteReport(report) { + try { + // simplify certain complex error messages (e.g. Ethjs) + simplifyErrorMessages(report) + // modify report urls + rewriteReportUrls(report) + } catch (err) { + console.warn(err) + } + } + + return Sentry +} + +function simplifyErrorMessages (report) { + rewriteErrorMessages(report, (errorMessage) => { + // simplify ethjs error messages + errorMessage = extractEthjsErrorMessage(errorMessage) + // simplify 'Transaction Failed: known transaction' + if (errorMessage.indexOf('Transaction Failed: known transaction') === 0) { + // cut the hash from the error message + errorMessage = 'Transaction Failed: known transaction' + } + return errorMessage + }) +} + +function rewriteErrorMessages (report, rewriteFn) { + // rewrite top level message + if (typeof report.message === 'string') report.message = rewriteFn(report.message) + // rewrite each exception message + if (report.exception && report.exception.values) { + report.exception.values.forEach(item => { + if (typeof item.value === 'string') item.value = rewriteFn(item.value) + }) + } +} + +function rewriteReportUrls (report) { + // update request url + report.request.url = toMetamaskUrl(report.request.url) + // update exception stack trace + if (report.exception && report.exception.values) { + report.exception.values.forEach(item => { + item.stacktrace.frames.forEach(frame => { + frame.filename = toMetamaskUrl(frame.filename) + }) + }) + } +} + +function toMetamaskUrl (origUrl) { + const filePath = origUrl.split(location.origin)[1] + if (!filePath) return origUrl + const metamaskUrl = `metamask${filePath}` + return metamaskUrl +} diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 98a036338..8893ceaad 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -9,7 +9,7 @@ const extension = require('extensionizer') const ExtensionPlatform = require('./platforms/extension') const NotificationManager = require('./lib/notification-manager') const notificationManager = new NotificationManager() -const setupRaven = require('./lib/setupRaven') +const setupSentry = require('./lib/setupSentry') const log = require('loglevel') start().catch(log.error) @@ -21,7 +21,7 @@ async function start () { // setup sentry error reporting const release = global.platform.getVersion() - setupRaven({ release }) + setupSentry({ release }) // inject css // const css = MetaMaskUiCss() -- cgit From 73ec4e66cb7a476d01371a61692b0d8d9224da04 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 20 Oct 2018 03:14:59 -0400 Subject: sentry - include app state in ui errors --- app/scripts/lib/setupSentry.js | 8 +++++++- app/scripts/ui.js | 12 +++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/lib/setupSentry.js b/app/scripts/lib/setupSentry.js index aa8d72194..69042bc19 100644 --- a/app/scripts/lib/setupSentry.js +++ b/app/scripts/lib/setupSentry.js @@ -8,7 +8,7 @@ module.exports = setupSentry // Setup sentry remote error reporting function setupSentry (opts) { - const { release } = opts + const { release, getState } = opts let sentryTarget // detect brave const isBrave = Boolean(window.chrome.ipcRenderer) @@ -38,9 +38,15 @@ function setupSentry (opts) { simplifyErrorMessages(report) // modify report urls rewriteReportUrls(report) + // append app state + if (getState) { + const appState = getState() + report.extra.appState = appState + } } catch (err) { console.warn(err) } + return report } return Sentry diff --git a/app/scripts/ui.js b/app/scripts/ui.js index 8893ceaad..c4f6615db 100644 --- a/app/scripts/ui.js +++ b/app/scripts/ui.js @@ -21,7 +21,17 @@ async function start () { // setup sentry error reporting const release = global.platform.getVersion() - setupSentry({ release }) + setupSentry({ release, getState }) + // provide app state to append to error logs + function getState() { + // get app state + const state = window.getCleanAppState() + // remove unnecessary data + delete state.localeMessages + delete state.metamask.recentBlocks + // return state to be added to request + return state + } // inject css // const css = MetaMaskUiCss() -- cgit From 31e5cad1e34c1b07079c430bb1903f7914021111 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sun, 21 Oct 2018 01:01:21 -0400 Subject: tx-gas-util - set error message when invalidating tx based on tx data but no contract code --- app/scripts/controllers/transactions/tx-gas-utils.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js index 436900715..def67c2c3 100644 --- a/app/scripts/controllers/transactions/tx-gas-utils.js +++ b/app/scripts/controllers/transactions/tx-gas-utils.js @@ -62,20 +62,21 @@ class TxGasUtil { const recipient = txParams.to const hasRecipient = Boolean(recipient) - // see if we can set the gas based on the recipient + // see if we can set the gas based on the recipient if (hasRecipient) { const code = await this.query.getCode(recipient) // For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0' const codeIsEmpty = !code || code === '0x' || code === '0x0' - + if (codeIsEmpty) { // if there's data in the params, but there's no contract code, it's not a valid transaction if (txParams.data) { - const err = new Error() + const err = new Error('TxGasUtil - Trying to call a function on a non-contract address') + // set error key so ui can display localized error message err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY throw err } - + // This is a standard ether simple send, gas requirement is exactly 21k txParams.gas = SIMPLE_GAS_COST // prevents buffer addition -- cgit From 61c7bbb1c1f0c216d5f8cd0d0753c78bc635624e Mon Sep 17 00:00:00 2001 From: kumavis Date: Sun, 21 Oct 2018 01:20:08 -0400 Subject: network - improve logging and type conversion --- app/scripts/controllers/network/network.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index b386161da..904c20cff 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -86,17 +86,19 @@ module.exports = class NetworkController extends EventEmitter { return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } const ethQuery = new EthQuery(this._provider) - ethQuery.sendAsync({ method: 'eth_chainId' }, (err, chainId) => { + ethQuery.sendAsync({ method: 'eth_chainId' }, (err, chainIdHex) => { if (err) { + // if eth_chainId is not supported, fallback to net_verion ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') - log.info('web3.getNetwork returned net_version = ' + network) + log.info(`net_version returned ${network}`) this.setNetworkState(network) }) return } - log.info('web3.getNetwork returned chainId = ' + parseInt(chainId)) - this.setNetworkState(parseInt(chainId)) + const chainId = Number.parseInt(chainIdHex, 16) + log.info(`net_version returned ${chainId}`) + this.setNetworkState(chainId) }) } -- cgit From b62d07f3a5bdfde7e295f17b7f601c6f2d5314ef Mon Sep 17 00:00:00 2001 From: kumavis Date: Sun, 21 Oct 2018 04:32:07 -0400 Subject: Update network.js --- app/scripts/controllers/network/network.js | 1 + 1 file changed, 1 insertion(+) (limited to 'app/scripts') diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 904c20cff..6a5369f06 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -86,6 +86,7 @@ module.exports = class NetworkController extends EventEmitter { return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } const ethQuery = new EthQuery(this._provider) + // first attempt to perform lookup via eth_chainId ethQuery.sendAsync({ method: 'eth_chainId' }, (err, chainIdHex) => { if (err) { // if eth_chainId is not supported, fallback to net_verion -- cgit From 6d09f60bbfe5ee737ff7a138260cc33e3db5ca46 Mon Sep 17 00:00:00 2001 From: kumavis Date: Sun, 21 Oct 2018 05:48:15 -0400 Subject: ens-ipfs - refactor for readability (#5568) * ens-ipfs - refactor for readability * ens-ipfs - use official ipfs gateway for better performance * lint - remove unused code * ens-ipfs - support path and search * lint - gotta love that linter * ens-ipfs - improve loading page formatting * ens-ipfs - loading - redirect to 404 after 1 min timeout * ens-ipfs - destructure for cleaner code --- app/scripts/background.js | 10 ++-- app/scripts/lib/contracts/registrar.js | 1 - app/scripts/lib/contracts/resolver.js | 2 - app/scripts/lib/ens-ipfs/contracts/registrar.js | 1 + app/scripts/lib/ens-ipfs/contracts/resolver.js | 2 + app/scripts/lib/ens-ipfs/resolver.js | 54 +++++++++++++++++++ app/scripts/lib/ens-ipfs/setup.js | 63 ++++++++++++++++++++++ app/scripts/lib/ipfsContent.js | 46 ---------------- app/scripts/lib/resolver.js | 71 ------------------------- 9 files changed, 123 insertions(+), 127 deletions(-) delete mode 100644 app/scripts/lib/contracts/registrar.js delete mode 100644 app/scripts/lib/contracts/resolver.js create mode 100644 app/scripts/lib/ens-ipfs/contracts/registrar.js create mode 100644 app/scripts/lib/ens-ipfs/contracts/resolver.js create mode 100644 app/scripts/lib/ens-ipfs/resolver.js create mode 100644 app/scripts/lib/ens-ipfs/setup.js delete mode 100644 app/scripts/lib/ipfsContent.js delete mode 100644 app/scripts/lib/resolver.js (limited to 'app/scripts') diff --git a/app/scripts/background.js b/app/scripts/background.js index 509a0001d..6b3ac2664 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -29,7 +29,7 @@ const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics') const EdgeEncryptor = require('./edge-encryptor') const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code') const getObjStructure = require('./lib/getObjStructure') -const ipfsContent = require('./lib/ipfsContent.js') +const setupEnsIpfsResolver = require('./lib/ens-ipfs/setup') const { ENVIRONMENT_TYPE_POPUP, @@ -58,7 +58,6 @@ const isIE = !!document.documentMode // Edge 20+ const isEdge = !isIE && !!window.StyleMedia -let ipfsHandle let popupIsOpen = false let notificationIsOpen = false const openMetamaskTabsIDs = {} @@ -164,7 +163,6 @@ async function initialize () { const initLangCode = await getFirstPreferredLangCode() await setupController(initState, initLangCode) log.debug('MetaMask initialization complete.') - ipfsHandle = ipfsContent(initState.NetworkController.provider) } // @@ -269,10 +267,8 @@ function setupController (initState, initLangCode) { }) global.metamaskController = controller - controller.networkController.on('networkDidChange', () => { - ipfsHandle && ipfsHandle.remove() - ipfsHandle = ipfsContent(controller.networkController.providerStore.getState()) - }) + const provider = controller.provider + setupEnsIpfsResolver({ provider }) // report failed transactions to Sentry controller.txController.on(`tx:status-update`, (txId, status) => { diff --git a/app/scripts/lib/contracts/registrar.js b/app/scripts/lib/contracts/registrar.js deleted file mode 100644 index 99ca24458..000000000 --- a/app/scripts/lib/contracts/registrar.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = [{'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'resolver', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'label', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setSubnodeOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'ttl', 'type': 'uint64'}], 'name': 'setTTL', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'ttl', 'outputs': [{'name': '', 'type': 'uint64'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'resolver', 'type': 'address'}], 'name': 'setResolver', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'label', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'NewOwner', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'resolver', 'type': 'address'}], 'name': 'NewResolver', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'ttl', 'type': 'uint64'}], 'name': 'NewTTL', 'type': 'event'}] diff --git a/app/scripts/lib/contracts/resolver.js b/app/scripts/lib/contracts/resolver.js deleted file mode 100644 index 1bf3f90ce..000000000 --- a/app/scripts/lib/contracts/resolver.js +++ /dev/null @@ -1,2 +0,0 @@ -module.exports = -[{'constant': true, 'inputs': [{'name': 'interfaceID', 'type': 'bytes4'}], 'name': 'supportsInterface', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentTypes', 'type': 'uint256'}], 'name': 'ABI', 'outputs': [{'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'name': 'setPubkey', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'content', 'outputs': [{'name': 'ret', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'addr', 'outputs': [{'name': 'ret', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'name': 'setABI', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'name', 'outputs': [{'name': 'ret', 'type': 'string'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'name', 'type': 'string'}], 'name': 'setName', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes32'}], 'name': 'setContent', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'pubkey', 'outputs': [{'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'addr', 'type': 'address'}], 'name': 'setAddr', 'outputs': [], 'payable': false, 'type': 'function'}, {'inputs': [{'name': 'ensAddr', 'type': 'address'}], 'payable': false, 'type': 'constructor'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'a', 'type': 'address'}], 'name': 'AddrChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'hash', 'type': 'bytes32'}], 'name': 'ContentChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'name', 'type': 'string'}], 'name': 'NameChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'contentType', 'type': 'uint256'}], 'name': 'ABIChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'x', 'type': 'bytes32'}, {'indexed': false, 'name': 'y', 'type': 'bytes32'}], 'name': 'PubkeyChanged', 'type': 'event'}] diff --git a/app/scripts/lib/ens-ipfs/contracts/registrar.js b/app/scripts/lib/ens-ipfs/contracts/registrar.js new file mode 100644 index 000000000..99ca24458 --- /dev/null +++ b/app/scripts/lib/ens-ipfs/contracts/registrar.js @@ -0,0 +1 @@ +module.exports = [{'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'resolver', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'owner', 'outputs': [{'name': '', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'label', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setSubnodeOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'ttl', 'type': 'uint64'}], 'name': 'setTTL', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'ttl', 'outputs': [{'name': '', 'type': 'uint64'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'resolver', 'type': 'address'}], 'name': 'setResolver', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'owner', 'type': 'address'}], 'name': 'setOwner', 'outputs': [], 'payable': false, 'type': 'function'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'Transfer', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'label', 'type': 'bytes32'}, {'indexed': false, 'name': 'owner', 'type': 'address'}], 'name': 'NewOwner', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'resolver', 'type': 'address'}], 'name': 'NewResolver', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'ttl', 'type': 'uint64'}], 'name': 'NewTTL', 'type': 'event'}] diff --git a/app/scripts/lib/ens-ipfs/contracts/resolver.js b/app/scripts/lib/ens-ipfs/contracts/resolver.js new file mode 100644 index 000000000..1bf3f90ce --- /dev/null +++ b/app/scripts/lib/ens-ipfs/contracts/resolver.js @@ -0,0 +1,2 @@ +module.exports = +[{'constant': true, 'inputs': [{'name': 'interfaceID', 'type': 'bytes4'}], 'name': 'supportsInterface', 'outputs': [{'name': '', 'type': 'bool'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentTypes', 'type': 'uint256'}], 'name': 'ABI', 'outputs': [{'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'name': 'setPubkey', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'content', 'outputs': [{'name': 'ret', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'addr', 'outputs': [{'name': 'ret', 'type': 'address'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'contentType', 'type': 'uint256'}, {'name': 'data', 'type': 'bytes'}], 'name': 'setABI', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'name', 'outputs': [{'name': 'ret', 'type': 'string'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'name', 'type': 'string'}], 'name': 'setName', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'hash', 'type': 'bytes32'}], 'name': 'setContent', 'outputs': [], 'payable': false, 'type': 'function'}, {'constant': true, 'inputs': [{'name': 'node', 'type': 'bytes32'}], 'name': 'pubkey', 'outputs': [{'name': 'x', 'type': 'bytes32'}, {'name': 'y', 'type': 'bytes32'}], 'payable': false, 'type': 'function'}, {'constant': false, 'inputs': [{'name': 'node', 'type': 'bytes32'}, {'name': 'addr', 'type': 'address'}], 'name': 'setAddr', 'outputs': [], 'payable': false, 'type': 'function'}, {'inputs': [{'name': 'ensAddr', 'type': 'address'}], 'payable': false, 'type': 'constructor'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'a', 'type': 'address'}], 'name': 'AddrChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'hash', 'type': 'bytes32'}], 'name': 'ContentChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'name', 'type': 'string'}], 'name': 'NameChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': true, 'name': 'contentType', 'type': 'uint256'}], 'name': 'ABIChanged', 'type': 'event'}, {'anonymous': false, 'inputs': [{'indexed': true, 'name': 'node', 'type': 'bytes32'}, {'indexed': false, 'name': 'x', 'type': 'bytes32'}, {'indexed': false, 'name': 'y', 'type': 'bytes32'}], 'name': 'PubkeyChanged', 'type': 'event'}] diff --git a/app/scripts/lib/ens-ipfs/resolver.js b/app/scripts/lib/ens-ipfs/resolver.js new file mode 100644 index 000000000..fe2dc1134 --- /dev/null +++ b/app/scripts/lib/ens-ipfs/resolver.js @@ -0,0 +1,54 @@ +const namehash = require('eth-ens-namehash') +const multihash = require('multihashes') +const Eth = require('ethjs-query') +const EthContract = require('ethjs-contract') +const registrarAbi = require('./contracts/registrar') +const resolverAbi = require('./contracts/resolver') + +module.exports = resolveEnsToIpfsContentId + + +async function resolveEnsToIpfsContentId ({ provider, name }) { + const eth = new Eth(provider) + const hash = namehash.hash(name) + const contract = new EthContract(eth) + // lookup registrar + const chainId = Number.parseInt(await eth.net_version(), 10) + const registrarAddress = getRegistrarForChainId(chainId) + if (!registrarAddress) { + throw new Error(`EnsIpfsResolver - no known ens-ipfs registrar for chainId "${chainId}"`) + } + const Registrar = contract(registrarAbi).at(registrarAddress) + // lookup resolver + const resolverLookupResult = await Registrar.resolver(hash) + const resolverAddress = resolverLookupResult[0] + if (hexValueIsEmpty(resolverAddress)) { + throw new Error(`EnsIpfsResolver - no resolver found for name "${name}"`) + } + const Resolver = contract(resolverAbi).at(resolverAddress) + // lookup content id + const contentLookupResult = await Resolver.content(hash) + const contentHash = contentLookupResult[0] + if (hexValueIsEmpty(contentHash)) { + throw new Error(`EnsIpfsResolver - no content ID found for name "${name}"`) + } + const nonPrefixedHex = contentHash.slice(2) + const buffer = multihash.fromHexString(nonPrefixedHex) + const contentId = multihash.toB58String(multihash.encode(buffer, 'sha2-256')) + return contentId +} + +function hexValueIsEmpty(value) { + return [undefined, null, '0x', '0x0', '0x0000000000000000000000000000000000000000000000000000000000000000'].includes(value) +} + +function getRegistrarForChainId (chainId) { + switch (chainId) { + // mainnet + case 1: + return '0x314159265dd8dbb310642f98f50c066173c1259b' + // ropsten + case 3: + return '0x112234455c3a32fd11230c42e7bccd4a84e02010' + } +} diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js new file mode 100644 index 000000000..45eb1ce14 --- /dev/null +++ b/app/scripts/lib/ens-ipfs/setup.js @@ -0,0 +1,63 @@ +const urlUtil = require('url') +const extension = require('extensionizer') +const resolveEnsToIpfsContentId = require('./resolver.js') + +const supportedTopLevelDomains = ['eth'] + +module.exports = setupEnsIpfsResolver + +function setupEnsIpfsResolver({ provider }) { + + // install listener + const urlPatterns = supportedTopLevelDomains.map(tld => `*://*.${tld}/*`) + extension.webRequest.onErrorOccurred.addListener(webRequestDidFail, { urls: urlPatterns }) + + // return api object + return { + // uninstall listener + remove () { + extension.webRequest.onErrorOccurred.removeListener(webRequestDidFail) + }, + } + + async function webRequestDidFail (details) { + const { tabId, url } = details + // ignore requests that are not associated with tabs + if (tabId === -1) return + // parse ens name + const urlData = urlUtil.parse(url) + const { hostname: name, path, search } = urlData + const domainParts = name.split('.') + const topLevelDomain = domainParts[domainParts.length - 1] + // if unsupported TLD, abort + if (!supportedTopLevelDomains.includes(topLevelDomain)) return + // otherwise attempt resolve + attemptResolve({ tabId, name, path, search }) + } + + async function attemptResolve({ tabId, name, path, search }) { + extension.tabs.update(tabId, { url: `loading.html` }) + try { + const ipfsContentId = await resolveEnsToIpfsContentId({ provider, name }) + let url = `https://gateway.ipfs.io/ipfs/${ipfsContentId}${path}${search || ''}` + try { + // check if ipfs gateway has result + const response = await fetch(url, { method: 'HEAD' }) + // if failure, redirect to 404 page + if (response.status !== 200) { + extension.tabs.update(tabId, { url: '404.html' }) + return + } + // otherwise redirect to the correct page + extension.tabs.update(tabId, { url }) + } catch (err) { + console.warn(err) + // if HEAD fetch failed, redirect so user can see relevant error page + extension.tabs.update(tabId, { url }) + } + } catch (err) { + console.warn(err) + extension.tabs.update(tabId, { url: `error.html?name=${name}` }) + } + } +} diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js deleted file mode 100644 index 8b08453c4..000000000 --- a/app/scripts/lib/ipfsContent.js +++ /dev/null @@ -1,46 +0,0 @@ -const extension = require('extensionizer') -const resolver = require('./resolver.js') - -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' }) - - clearTime = setTimeout(() => { - return extension.tabs.update(tab.id, { url: '404.html' }) - }, 60000) - - resolver.resolve(name, provider).then(ipfsHash => { - clearTimeout(clearTime) - let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash - return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => { - if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' }) - extension.tabs.update(tab.id, { url: url }) - }) - .catch(err => { - url = 'https://ipfs.infura.io/ipfs/' + ipfsHash - extension.tabs.update(tab.id, {url: url}) - return err - }) - }) - .catch(err => { - clearTimeout(clearTime) - const url = err === 'unsupport' ? 'unsupport' : 'error' - extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`}) - }) - }) - return { cancel: true } - } - - extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/'], types: ['main_frame']}) - - return { - remove () { - extension.webRequest.onErrorOccurred.removeListener(ipfsContent) - }, - } -} diff --git a/app/scripts/lib/resolver.js b/app/scripts/lib/resolver.js deleted file mode 100644 index ff0fed161..000000000 --- a/app/scripts/lib/resolver.js +++ /dev/null @@ -1,71 +0,0 @@ -const namehash = require('eth-ens-namehash') -const multihash = require('multihashes') -const HttpProvider = require('ethjs-provider-http') -const Eth = require('ethjs-query') -const EthContract = require('ethjs-contract') -const registrarAbi = require('./contracts/registrar') -const resolverAbi = require('./contracts/resolver') - -function ens (name, provider) { - const eth = new Eth(new HttpProvider(getProvider(provider.type))) - const hash = namehash.hash(name) - const contract = new EthContract(eth) - const Registrar = contract(registrarAbi).at(getRegistrar(provider.type)) - return new Promise((resolve, reject) => { - if (provider.type === 'mainnet' || provider.type === 'ropsten') { - Registrar.resolver(hash).then((address) => { - if (address === '0x0000000000000000000000000000000000000000') { - reject(null) - } else { - const Resolver = contract(resolverAbi).at(address['0']) - return Resolver.content(hash) - } - }).then((contentHash) => { - if (contentHash['0'] === '0x0000000000000000000000000000000000000000000000000000000000000000') reject(null) - if (contentHash.ret !== '0x') { - const hex = contentHash['0'].substring(2) - const buf = multihash.fromHexString(hex) - resolve(multihash.toB58String(multihash.encode(buf, 'sha2-256'))) - } else { - reject(null) - } - }) - } else { - return reject('unsupport') - } - }) -} - -function getProvider (type) { - switch (type) { - case 'mainnet': - return 'https://mainnet.infura.io/' - case 'ropsten': - return 'https://ropsten.infura.io/' - default: - return 'http://localhost:8545/' - } -} - -function getRegistrar (type) { - switch (type) { - case 'mainnet': - return '0x314159265dd8dbb310642f98f50c066173c1259b' - case 'ropsten': - return '0x112234455c3a32fd11230c42e7bccd4a84e02010' - default: - return '0x0000000000000000000000000000000000000000' - } -} - -module.exports.resolve = function (name, provider) { - const path = name.split('.') - const topLevelDomain = path[path.length - 1] - if (topLevelDomain === 'eth' || topLevelDomain === 'test') { - return ens(name, provider) - } else { - return new Promise((resolve, reject) => { - reject(null) - }) - } -} -- cgit From 1d65687ce48bc7f35ee0167c94813f8b3cb3a6ee Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 24 Oct 2018 20:03:55 -0700 Subject: Validate signTypedData in eth-json-rpc-middleware --- .../network/createMetamaskMiddleware.js | 2 + app/scripts/metamask-controller.js | 58 +++++++++++----------- 2 files changed, 32 insertions(+), 28 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js index 9e6a45888..319c5bf3e 100644 --- a/app/scripts/controllers/network/createMetamaskMiddleware.js +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -11,6 +11,7 @@ function createMetamaskMiddleware ({ processTransaction, processEthSignMessage, processTypedMessage, + processTypedMessageV3, processPersonalMessage, getPendingNonce, }) { @@ -25,6 +26,7 @@ function createMetamaskMiddleware ({ processTransaction, processEthSignMessage, processTypedMessage, + processTypedMessageV3, processPersonalMessage, }), createPendingNonceMiddleware({ getPendingNonce }), diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7913662d4..1e02d8488 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -138,12 +138,12 @@ module.exports = class MetamaskController extends EventEmitter { this.accountTracker.stop() } }) - + // ensure accountTracker updates balances after network change this.networkController.on('networkDidChange', () => { this.accountTracker._updateAccounts() }) - + // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ @@ -275,6 +275,8 @@ module.exports = class MetamaskController extends EventEmitter { processTransaction: this.newUnapprovedTransaction.bind(this), // msg signing processEthSignMessage: this.newUnsignedMessage.bind(this), + processTypedMessage: this.newUnsignedTypedMessage.bind(this), + processTypedMessageV3: this.newUnsignedTypedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), getPendingNonce: this.getPendingNonce.bind(this), } @@ -978,8 +980,8 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_signTypedData. * @param {Function} cb - The callback function, called with the signature. */ - newUnsignedTypedMessage (msgParams, req) { - const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req) + newUnsignedTypedMessage (msgParams, req, version) { + const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req, version) this.sendUpdate() this.opts.showUnconfirmedMessage() return promise @@ -1274,9 +1276,9 @@ module.exports = class MetamaskController extends EventEmitter { // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) // 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)) + // 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 })) @@ -1542,27 +1544,27 @@ module.exports = class MetamaskController extends EventEmitter { * @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() - } - } - } + // 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 -- cgit From 95b92a1ddcaf478fe612f46c94c8e0600826cfd6 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 24 Oct 2018 20:13:40 -0700 Subject: Remove commented out/unused methods --- app/scripts/metamask-controller.js | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1e02d8488..5a182d3f0 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -980,8 +980,8 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_signTypedData. * @param {Function} cb - The callback function, called with the signature. */ - newUnsignedTypedMessage (msgParams, req, version) { - const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req, version) + newUnsignedTypedMessage (msgParams, req) { + const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req) this.sendUpdate() this.opts.showUnconfirmedMessage() return promise @@ -1275,10 +1275,6 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(subscriptionManager.middleware) // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) - // 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 })) @@ -1544,27 +1540,6 @@ module.exports = class MetamaskController extends EventEmitter { * @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 -- cgit From 8c0e5e97e5ee50122e4ff120e1076e71394be378 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 24 Oct 2018 20:24:42 -0700 Subject: Add version to unapprovedMessage --- app/scripts/metamask-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 5a182d3f0..bbff95618 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -980,8 +980,8 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Object} msgParams - The params passed to eth_signTypedData. * @param {Function} cb - The callback function, called with the signature. */ - newUnsignedTypedMessage (msgParams, req) { - const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req) + newUnsignedTypedMessage (msgParams, req, version) { + const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req, version) this.sendUpdate() this.opts.showUnconfirmedMessage() return promise -- cgit From 54a8ade2669cb5f8f046509873bc2a9c25425847 Mon Sep 17 00:00:00 2001 From: HackyMiner Date: Fri, 26 Oct 2018 17:26:43 +0900 Subject: Add support for RPC endpoints with custom chain IDs (#5134) --- app/scripts/controllers/currency.js | 52 +++++++++++++++++++++++--- app/scripts/controllers/network/network.js | 60 +++++++++++++++++++++++++----- app/scripts/controllers/preferences.js | 31 ++++++++------- app/scripts/metamask-controller.js | 18 ++++++--- 4 files changed, 128 insertions(+), 33 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js index d5bc5fe2b..1e866d2c9 100644 --- a/app/scripts/controllers/currency.js +++ b/app/scripts/controllers/currency.js @@ -21,6 +21,7 @@ class CurrencyController { * since midnight of January 1, 1970 * @property {number} conversionInterval The id of the interval created by the scheduleConversionInterval method. * Used to clear an existing interval on subsequent calls of that method. + * @property {string} nativeCurrency The ticker/symbol of the native chain currency * */ constructor (opts = {}) { @@ -28,6 +29,7 @@ class CurrencyController { currentCurrency: 'usd', conversionRate: 0, conversionDate: 'N/A', + nativeCurrency: 'ETH', }, opts.initState) this.store = new ObservableStore(initState) } @@ -36,6 +38,29 @@ class CurrencyController { // PUBLIC METHODS // + /** + * A getter for the nativeCurrency property + * + * @returns {string} A 2-4 character shorthand that describes the specific currency + * + */ + getNativeCurrency () { + return this.store.getState().nativeCurrency + } + + /** + * A setter for the nativeCurrency property + * + * @param {string} nativeCurrency The new currency to set as the nativeCurrency in the store + * + */ + setNativeCurrency (nativeCurrency) { + this.store.updateState({ + nativeCurrency, + ticker: nativeCurrency, + }) + } + /** * A getter for the currentCurrency property * @@ -104,15 +129,32 @@ class CurrencyController { * */ async updateConversionRate () { - let currentCurrency + let currentCurrency, nativeCurrency try { currentCurrency = this.getCurrentCurrency() - const response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`) + nativeCurrency = this.getNativeCurrency() + let apiUrl + if (nativeCurrency === 'ETH') { + apiUrl = `https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}` + } else { + apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}` + } + const response = await fetch(apiUrl) const parsedResponse = await response.json() - this.setConversionRate(Number(parsedResponse.bid)) - this.setConversionDate(Number(parsedResponse.timestamp)) + if (nativeCurrency === 'ETH') { + this.setConversionRate(Number(parsedResponse.bid)) + this.setConversionDate(Number(parsedResponse.timestamp)) + } else { + if (parsedResponse[currentCurrency.toUpperCase()]) { + this.setConversionRate(Number(parsedResponse[currentCurrency.toUpperCase()])) + this.setConversionDate(parseInt((new Date()).getTime() / 1000)) + } else { + this.setConversionRate(0) + this.setConversionDate('N/A') + } + } } catch (err) { - log.warn(`MetaMask - Failed to query currency conversion:`, currentCurrency, err) + log.warn(`MetaMask - Failed to query currency conversion:`, nativeCurrency, currentCurrency, err) this.setConversionRate(0) this.setConversionDate('N/A') } diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index c1667d9a6..b459b8013 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -11,6 +11,8 @@ const createInfuraClient = require('./createInfuraClient') const createJsonRpcClient = require('./createJsonRpcClient') const createLocalhostClient = require('./createLocalhostClient') const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') +const extend = require('extend') +const networks = { networkList: {} } const { ROPSTEN, @@ -29,6 +31,10 @@ const defaultProviderConfig = { type: testMode ? RINKEBY : MAINNET, } +const defaultNetworkConfig = { + ticker: 'ETH', +} + module.exports = class NetworkController extends EventEmitter { constructor (opts = {}) { @@ -39,7 +45,8 @@ module.exports = class NetworkController extends EventEmitter { // create stores this.providerStore = new ObservableStore(providerConfig) this.networkStore = new ObservableStore('loading') - this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) + this.networkConfig = new ObservableStore(defaultNetworkConfig) + this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, settings: this.networkConfig }) this.on('networkDidChange', this.lookupNetwork) // provider and block tracker this._provider = null @@ -51,8 +58,8 @@ module.exports = class NetworkController extends EventEmitter { initializeProvider (providerParams) { this._baseProviderParams = providerParams - const { type, rpcTarget } = this.providerStore.getState() - this._configureProvider({ type, rpcTarget }) + const { type, rpcTarget, chainId, ticker, nickname } = this.providerStore.getState() + this._configureProvider({ type, rpcTarget, chainId, ticker, nickname }) this.lookupNetwork() } @@ -72,7 +79,20 @@ module.exports = class NetworkController extends EventEmitter { return this.networkStore.getState() } - setNetworkState (network) { + getNetworkConfig () { + return this.networkConfig.getState() + } + + setNetworkState (network, type) { + if (network === 'loading') { + return this.networkStore.putState(network) + } + + // type must be defined + if (!type) { + return + } + network = networks.networkList[type] && networks.networkList[type].chainId ? networks.networkList[type].chainId : network return this.networkStore.putState(network) } @@ -85,18 +105,22 @@ module.exports = class NetworkController extends EventEmitter { if (!this._provider) { return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } + var { type } = this.providerStore.getState() const ethQuery = new EthQuery(this._provider) ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') log.info('web3.getNetwork returned ' + network) - this.setNetworkState(network) + this.setNetworkState(network, type) }) } - setRpcTarget (rpcTarget) { + setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '') { const providerConfig = { type: 'rpc', rpcTarget, + chainId, + ticker, + nickname, } this.providerConfig = providerConfig } @@ -132,7 +156,7 @@ module.exports = class NetworkController extends EventEmitter { } _configureProvider (opts) { - const { type, rpcTarget } = opts + const { type, rpcTarget, chainId, ticker, nickname } = opts // infura type-based endpoints const isInfura = INFURA_PROVIDER_TYPES.includes(type) if (isInfura) { @@ -142,7 +166,7 @@ module.exports = class NetworkController extends EventEmitter { this._configureLocalhostProvider() // url-based rpc endpoints } else if (type === 'rpc') { - this._configureStandardProvider({ rpcUrl: rpcTarget }) + this._configureStandardProvider({ rpcUrl: rpcTarget, chainId, ticker, nickname }) } else { throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`) } @@ -152,6 +176,11 @@ module.exports = class NetworkController extends EventEmitter { log.info('NetworkController - configureInfuraProvider', type) const networkClient = createInfuraClient({ network: type }) this._setNetworkClient(networkClient) + // setup networkConfig + var settings = { + ticker: 'ETH', + } + this.networkConfig.putState(settings) } _configureLocalhostProvider () { @@ -160,9 +189,22 @@ module.exports = class NetworkController extends EventEmitter { this._setNetworkClient(networkClient) } - _configureStandardProvider ({ rpcUrl }) { + _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) { log.info('NetworkController - configureStandardProvider', rpcUrl) const networkClient = createJsonRpcClient({ rpcUrl }) + // hack to add a 'rpc' network with chainId + networks.networkList['rpc'] = { + chainId: chainId, + rpcUrl, + ticker: ticker || 'ETH', + nickname, + } + // setup networkConfig + var settings = { + network: chainId, + } + settings = extend(settings, networks.networkList['rpc']) + this.networkConfig.putState(settings) this._setNetworkClient(networkClient) } diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 20b13398c..120801f06 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -25,7 +25,7 @@ class PreferencesController { */ constructor (opts = {}) { const initState = extend({ - frequentRpcList: [], + frequentRpcListDetail: [], currentAccountTab: 'history', accountTokens: {}, assetImages: {}, @@ -39,7 +39,7 @@ class PreferencesController { seedWords: null, forgottenPassword: false, preferences: { - useETHAsPrimaryCurrency: true, + useNativeCurrencyAsPrimaryCurrency: true, }, }, opts.initState) @@ -392,19 +392,22 @@ class PreferencesController { * Adds custom RPC url to state. * * @param {string} url The RPC url to add to frequentRpcList. + * @param {number} chainId Optional chainId of the selected network. + * @param {string} ticker Optional ticker symbol of the selected network. + * @param {string} nickname Optional nickname of the selected network. * @returns {Promise} Promise resolving to updated frequentRpcList. * */ - addToFrequentRpcList (url) { - const rpcList = this.getFrequentRpcList() - const index = rpcList.findIndex((element) => { return element === url }) + addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '') { + const rpcList = this.getFrequentRpcListDetail() + const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) if (index !== -1) { rpcList.splice(index, 1) } if (url !== 'http://localhost:8545') { - rpcList.push(url) + rpcList.push({ rpcUrl: url, chainId, ticker, nickname }) } - this.store.updateState({ frequentRpcList: rpcList }) + this.store.updateState({ frequentRpcListiDetail: rpcList }) return Promise.resolve(rpcList) } @@ -416,23 +419,23 @@ class PreferencesController { * */ removeFromFrequentRpcList (url) { - const rpcList = this.getFrequentRpcList() - const index = rpcList.findIndex((element) => { return element === url }) + const rpcList = this.getFrequentRpcListDetail() + const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) if (index !== -1) { rpcList.splice(index, 1) } - this.store.updateState({ frequentRpcList: rpcList }) + this.store.updateState({ frequentRpcListDetail: rpcList }) return Promise.resolve(rpcList) } /** - * Getter for the `frequentRpcList` property. + * Getter for the `frequentRpcListDetail` property. * - * @returns {array} An array of one or two rpc urls. + * @returns {array} An array of rpc urls. * */ - getFrequentRpcList () { - return this.store.getState().frequentRpcList + getFrequentRpcListDetail () { + return this.store.getState().frequentRpcListDetail } /** diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 7913662d4..3778dbdb6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -138,12 +138,12 @@ module.exports = class MetamaskController extends EventEmitter { this.accountTracker.stop() } }) - + // ensure accountTracker updates balances after network change this.networkController.on('networkDidChange', () => { this.accountTracker._updateAccounts() }) - + // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] this.keyringController = new KeyringController({ @@ -197,6 +197,8 @@ module.exports = class MetamaskController extends EventEmitter { }) this.networkController.on('networkDidChange', () => { this.balancesController.updateAllBalances() + var currentCurrency = this.currencyController.getCurrentCurrency() + this.setCurrentCurrency(currentCurrency, function() {}) }) this.balancesController.updateAllBalances() @@ -1412,10 +1414,13 @@ module.exports = class MetamaskController extends EventEmitter { * @param {Function} cb - A callback function returning currency info. */ setCurrentCurrency (currencyCode, cb) { + const { ticker } = this.networkController.getNetworkConfig() try { + this.currencyController.setNativeCurrency(ticker) this.currencyController.setCurrentCurrency(currencyCode) this.currencyController.updateConversionRate() const data = { + nativeCurrency: ticker || 'ETH', conversionRate: this.currencyController.getConversionRate(), currentCurrency: this.currencyController.getCurrentCurrency(), conversionDate: this.currencyController.getConversionDate(), @@ -1454,11 +1459,14 @@ module.exports = class MetamaskController extends EventEmitter { /** * A method for selecting a custom URL for an ethereum RPC provider. * @param {string} rpcTarget - A URL for a valid Ethereum RPC API. + * @param {number} chainId - The chainId of the selected network. + * @param {string} ticker - The ticker symbol of the selected network. + * @param {string} nickname - Optional nickname of the selected network. * @returns {Promise} - The RPC Target URL confirmed. */ - async setCustomRpc (rpcTarget) { - this.networkController.setRpcTarget(rpcTarget) - await this.preferencesController.addToFrequentRpcList(rpcTarget) + async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '') { + this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname) + await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname) return rpcTarget } -- cgit From 986f8b4c2143fd1f1caec1e21aea19b260f3c176 Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 29 Oct 2018 18:56:29 -0400 Subject: preferences - fix typo --- app/scripts/controllers/preferences.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 120801f06..eaeaee499 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -407,7 +407,7 @@ class PreferencesController { if (url !== 'http://localhost:8545') { rpcList.push({ rpcUrl: url, chainId, ticker, nickname }) } - this.store.updateState({ frequentRpcListiDetail: rpcList }) + this.store.updateState({ frequentRpcListDetail: rpcList }) return Promise.resolve(rpcList) } -- cgit From 2c1bca1ab0a6284b89338d5fd36552583c960f36 Mon Sep 17 00:00:00 2001 From: kumavis Date: Fri, 2 Nov 2018 22:14:40 -0400 Subject: token-rates - protect against bad token data --- app/scripts/controllers/token-rates.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js index 87d716aa6..b6f084841 100644 --- a/app/scripts/controllers/token-rates.js +++ b/app/scripts/controllers/token-rates.js @@ -1,5 +1,5 @@ const ObservableStore = require('obs-store') -const { warn } = require('loglevel') +const log = require('loglevel') // By default, poll every 3 minutes const DEFAULT_INTERVAL = 180 * 1000 @@ -26,8 +26,11 @@ class TokenRatesController { async updateExchangeRates () { if (!this.isActive) { return } const contractExchangeRates = {} - for (const i in this._tokens) { - const address = this._tokens[i].address + // copy array to ensure its not modified during iteration + const tokens = this._tokens.slice() + for (const token of tokens) { + if (!token) return log.error(`TokenRatesController - invalid tokens state:\n${JSON.stringify(tokens, null, 2)}`) + const address = token.address contractExchangeRates[address] = await this.fetchExchangeRate(address) } this.store.putState({ contractExchangeRates }) @@ -44,7 +47,7 @@ class TokenRatesController { const json = await response.json() return json && json.length ? json[0].averagePrice : 0 } catch (error) { - warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error) + log.warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error) return 0 } } -- cgit From ed4f612bdc490435b88feb26b2a9df8c9320d3bb Mon Sep 17 00:00:00 2001 From: Thomas Huang Date: Sat, 3 Nov 2018 12:28:59 -0700 Subject: Revert "support eth_chainId" --- app/scripts/controllers/network/network.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index c21e9c764..b459b8013 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -107,20 +107,10 @@ module.exports = class NetworkController extends EventEmitter { } var { type } = this.providerStore.getState() const ethQuery = new EthQuery(this._provider) - // first attempt to perform lookup via eth_chainId - ethQuery.sendAsync({ method: 'eth_chainId' }, (err, chainIdHex) => { - if (err) { - // if eth_chainId is not supported, fallback to net_verion - ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { - if (err) return this.setNetworkState('loading') - log.info(`net_version returned ${network}`) - this.setNetworkState(network, type) - }) - return - } - const chainId = Number.parseInt(chainIdHex, 16) - log.info(`net_version returned ${chainId}`) - this.setNetworkState(chainId, type) + ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { + if (err) return this.setNetworkState('loading') + log.info('web3.getNetwork returned ' + network) + this.setNetworkState(network, type) }) } -- cgit From 4489a57f2fd32ae4b9b5aa12aede289fa0b03fb1 Mon Sep 17 00:00:00 2001 From: Esteban Miño Date: Mon, 5 Nov 2018 16:06:34 -0300 Subject: Update watchAsset ERC20 validation (#5653) * update ERC20 token valodation for watchAsset * update ERC20 validation test descriptions --- app/scripts/controllers/preferences.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index eaeaee499..dc6fecaf5 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -583,8 +583,8 @@ class PreferencesController { */ _validateERC20AssetParams (opts) { const { rawAddress, symbol, decimals } = opts - if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`) - if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`) + if (!rawAddress || !symbol || typeof decimals === 'undefined') throw new Error(`Cannot suggest token without address, symbol, and decimals`) + if (!(symbol.length < 7)) throw new Error(`Invalid symbol ${symbol} more than six characters`) const numDecimals = parseInt(decimals, 10) if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`) -- cgit From 86f09e6bb53bb90e524366c1f225cc9005e01a2a Mon Sep 17 00:00:00 2001 From: kumavis Date: Mon, 5 Nov 2018 14:13:37 -0500 Subject: network - infura - hardcode net_version and eth_chainId (#5670) * network - infura - hardcode net_version and eth_chainId * network - infura - add rinkeby handling * lint fix --- .../controllers/network/createInfuraClient.js | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) (limited to 'app/scripts') diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 326bcb355..c70fa9e38 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -1,4 +1,5 @@ const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware') 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') @@ -16,6 +17,7 @@ function createInfuraClient ({ network }) { const blockTracker = new BlockTracker({ provider: infuraProvider }) const networkMiddleware = mergeMiddleware([ + createNetworkAndChainIdMiddleware({ network }), createBlockCacheMiddleware({ blockTracker }), createInflightMiddleware(), createBlockReRefMiddleware({ blockTracker, provider: infuraProvider }), @@ -25,3 +27,34 @@ function createInfuraClient ({ network }) { ]) return { networkMiddleware, blockTracker } } + +function createNetworkAndChainIdMiddleware({ network }) { + let chainId + let netId + + switch (network) { + case 'mainnet': + netId = '1' + chainId = '0x01' + break + case 'ropsten': + netId = '3' + chainId = '0x03' + break + case 'rinkeby': + netId = '4' + chainId = '0x04' + break + case 'kovan': + netId = '42' + chainId = '0x2a' + break + default: + throw new Error(`createInfuraClient - unknown network "${network}"`) + } + + return createScaffoldMiddleware({ + eth_chainId: chainId, + net_version: netId, + }) +} -- cgit From c76c9ca2c86317f902f443db2c5704d4bf6311c0 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Thu, 27 Sep 2018 14:19:09 -0400 Subject: EIP-1102: updated implementation --- app/scripts/background.js | 5 +- app/scripts/contentscript.js | 61 ++++++++++++++++---- app/scripts/controllers/preferences.js | 4 +- app/scripts/controllers/provider-approval.js | 84 ++++++++++++++++++++++++++++ app/scripts/inpage.js | 23 ++++---- app/scripts/metamask-controller.js | 21 ++++++- app/scripts/platforms/extension.js | 12 ++++ 7 files changed, 182 insertions(+), 28 deletions(-) create mode 100644 app/scripts/controllers/provider-approval.js (limited to 'app/scripts') diff --git a/app/scripts/background.js b/app/scripts/background.js index 2a3c5b08b..078e84928 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -256,7 +256,8 @@ function setupController (initState, initLangCode) { showUnconfirmedMessage: triggerUi, unlockAccountMessage: triggerUi, showUnapprovedTx: triggerUi, - showWatchAssetUi: showWatchAssetUi, + openPopup: openPopup, + closePopup: notificationManager.closePopup.bind(notificationManager), // initial state initState, // initial locale code @@ -447,7 +448,7 @@ function triggerUi () { * Opens the browser popup for user confirmation of watchAsset * then it waits until user interact with the UI */ -function showWatchAssetUi () { +function openPopup () { triggerUi() return new Promise( (resolve) => { diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 33523eb46..060343031 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -11,6 +11,7 @@ const PortStream = require('extension-port-stream') const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' const inpageBundle = inpageContent + inpageSuffix +let originApproved = false // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction @@ -20,24 +21,24 @@ const inpageBundle = inpageContent + inpageSuffix // MetaMask will be much faster loading and performant on Firefox. if (shouldInjectWeb3()) { - setupInjection() + injectScript(inpageBundle) setupStreams() + listenForProviderRequest() } /** - * Creates a script tag that injects inpage.js + * Injects a script tag into the current document + * + * @param {string} content - Code to be executed in the current document */ -function setupInjection () { +function injectScript (content) { try { - // inject in-page script - var scriptTag = document.createElement('script') - scriptTag.textContent = inpageBundle - scriptTag.onload = function () { this.parentNode.removeChild(this) } - var container = document.head || document.documentElement - // append as first child + const container = document.head || document.documentElement + const scriptTag = document.createElement('script') + scriptTag.textContent = content container.insertBefore(scriptTag, container.children[0]) } catch (e) { - console.error('Metamask injection failed.', e) + console.error('Metamask script injection failed.', e) } } @@ -54,6 +55,16 @@ function setupStreams () { const pluginPort = extension.runtime.connect({ name: 'contentscript' }) const pluginStream = new PortStream(pluginPort) + // Until this origin is approved, cut-off publicConfig stream writes at the content + // script level so malicious sites can't snoop on the currently-selected address + pageStream._write = function (data, encoding, cb) { + if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !originApproved) { + cb() + return + } + LocalMessageDuplexStream.prototype._write.apply(pageStream, arguments) + } + // forward communication plugin->inpage pump( pageStream, @@ -97,6 +108,36 @@ function setupStreams () { mux.ignoreStream('publicConfig') } +/** + * Establishes listeners for requests to fully-enable the provider from the dapp context + * and for full-provider approvals and rejections from the background script context. Dapps + * should not post messages directly and should instead call provider.enable(), which + * handles posting these messages automatically. + */ +function listenForProviderRequest () { + window.addEventListener('message', (event) => { + if (event.source !== window) { return } + if (!event.data || !event.data.type || event.data.type !== 'ETHEREUM_ENABLE_PROVIDER') { return } + extension.runtime.sendMessage({ + action: 'init-provider-request', + origin: event.source.location.hostname, + }) + }) + + extension.runtime.onMessage.addListener(({ action }) => { + if (!action) { return } + switch (action) { + case 'approve-provider-request': + originApproved = true + injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: {}}))`) + break + case 'reject-provider-request': + injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`) + break + } + }) +} + /** * Error handler for page to plugin stream disconnections diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index dc6fecaf5..ffb593b09 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -46,7 +46,7 @@ class PreferencesController { this.diagnostics = opts.diagnostics this.network = opts.network this.store = new ObservableStore(initState) - this.showWatchAssetUi = opts.showWatchAssetUi + this.openPopup = opts.openPopup this._subscribeProviderType() } // PUBLIC METHODS @@ -567,7 +567,7 @@ class PreferencesController { } const tokenOpts = { rawAddress, decimals, symbol, image } this.addSuggestedERC20Asset(tokenOpts) - return this.showWatchAssetUi().then(() => { + return this.openPopup().then(() => { const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress)) return tokenAddresses.length > 0 }) diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js new file mode 100644 index 000000000..e9b1b8e16 --- /dev/null +++ b/app/scripts/controllers/provider-approval.js @@ -0,0 +1,84 @@ +const ObservableStore = require('obs-store') + +/** + * A controller that services user-approved requests for a full Ethereum provider API + */ +class ProviderApprovalController { + /** + * Creates a ProviderApprovalController + * + * @param {Object} [config] - Options to configure controller + */ + constructor ({ closePopup, openPopup, platform, publicConfigStore } = {}) { + this.store = new ObservableStore() + this.closePopup = closePopup + this.openPopup = openPopup + this.platform = platform + this.publicConfigStore = publicConfigStore + this.approvedOrigins = {} + platform && platform.addMessageListener && platform.addMessageListener(({ action, origin }) => { + action && action === 'init-provider-request' && this.handleProviderRequest(origin) + }) + } + + /** + * Called when a tab requests access to a full Ethereum provider API + * + * @param {string} origin - Origin of the window requesting full provider access + */ + handleProviderRequest (origin) { + this.store.updateState({ providerRequests: [{ origin }] }) + if (this.approvedOrigins[origin]) { + this.approveProviderRequest(origin) + return + } + this.openPopup && this.openPopup() + } + + /** + * Called when a user approves access to a full Ethereum provider API + * + * @param {string} origin - Origin of the target window to approve provider access + */ + approveProviderRequest (origin) { + this.closePopup && this.closePopup() + const requests = this.store.getState().providerRequests || [] + this.platform && this.platform.sendMessage({ action: 'approve-provider-request' }, { active: true }) + this.publicConfigStore.emit('update', this.publicConfigStore.getState()) + const providerRequests = requests.filter(request => request.origin !== origin) + this.store.updateState({ providerRequests }) + this.approvedOrigins[origin] = true + } + + /** + * Called when a tab rejects access to a full Ethereum provider API + * + * @param {string} origin - Origin of the target window to reject provider access + */ + rejectProviderRequest (origin) { + this.closePopup && this.closePopup() + const requests = this.store.getState().providerRequests || [] + this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { active: true }) + const providerRequests = requests.filter(request => request.origin !== origin) + this.store.updateState({ providerRequests }) + } + + /** + * Clears any cached approvals for user-approved origins + */ + clearApprovedOrigins () { + this.approvedOrigins = {} + } + + /** + * Determines if a given origin has been approved + * + * @param {string} origin - Domain origin to check for approval status + * @returns {boolean} - True if the origin has been approved + */ + isApproved (origin) { + return this.approvedOrigins[origin] + } +} + +module.exports = ProviderApprovalController diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index b885a7e05..25bfe1416 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -31,19 +31,18 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream) inpageProvider.setMaxListeners(100) // Augment the provider with its enable method -inpageProvider.enable = function (options = {}) { +inpageProvider.enable = function () { 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) - } - }) - } + window.addEventListener('ethereumprovider', ({ detail }) => { + if (typeof detail.error !== 'undefined') { + reject(detail.error) + } else { + inpageProvider.publicConfigStore.once('update', () => { + resolve(inpageProvider.send({ method: 'eth_accounts' }).result) + }) + } + }) + window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER' }, '*') }) } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 1f6a8659b..cffc5797b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -37,6 +37,7 @@ const TransactionController = require('./controllers/transactions') const BalancesController = require('./controllers/computed-balances') const TokenRatesController = require('./controllers/token-rates') const DetectTokensController = require('./controllers/detect-tokens') +const ProviderApprovalController = require('./controllers/provider-approval') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') @@ -89,7 +90,7 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, - showWatchAssetUi: opts.showWatchAssetUi, + openPopup: opts.openPopup, network: this.networkController, }) @@ -219,6 +220,13 @@ module.exports = class MetamaskController extends EventEmitter { this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) this.publicConfigStore = this.initPublicConfigStore() + this.providerApprovalController = new ProviderApprovalController({ + closePopup: opts.closePopup, + openPopup: opts.openPopup, + platform: opts.platform, + publicConfigStore: this.publicConfigStore, + }) + this.store.updateStructure({ TransactionController: this.txController.store, KeyringController: this.keyringController.store, @@ -248,6 +256,7 @@ module.exports = class MetamaskController extends EventEmitter { NoticeController: this.noticeController.memStore, ShapeshiftController: this.shapeshiftController.store, InfuraController: this.infuraController.store, + ProviderApprovalController: this.providerApprovalController.store, }) this.memStore.subscribe(this.sendUpdate.bind(this)) } @@ -263,7 +272,10 @@ module.exports = class MetamaskController extends EventEmitter { }, version, // account mgmt - getAccounts: async () => { + getAccounts: async ({ origin }) => { + // Expose no accounts if this origin has not been approved, preventing + // account-requring RPC methods from completing successfully + if (origin !== 'MetaMask' && !this.providerApprovalController.isApproved(origin)) { return [] } const isUnlocked = this.keyringController.memStore.getState().isUnlocked const selectedAddress = this.preferencesController.getSelectedAddress() // only show address if account is unlocked @@ -349,6 +361,7 @@ module.exports = class MetamaskController extends EventEmitter { const noticeController = this.noticeController const addressBookController = this.addressBookController const networkController = this.networkController + const providerApprovalController = this.providerApprovalController return { // etc @@ -437,6 +450,10 @@ module.exports = class MetamaskController extends EventEmitter { // notices checkNotices: noticeController.updateNoticesList.bind(noticeController), markNoticeRead: noticeController.markNoticeRead.bind(noticeController), + + approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController), + clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), + rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController), } } diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 71b162dd0..9ef0d22c9 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -57,6 +57,18 @@ class ExtensionPlatform { } } + addMessageListener (cb) { + extension.runtime.onMessage.addListener(cb) + } + + sendMessage (message, query = {}) { + extension.tabs.query(query, tabs => { + tabs.forEach(tab => { + extension.tabs.sendMessage(tab.id, message) + }) + }) + } + _showConfirmedTransaction (txMeta) { this._subscribeToNotificationClicked() -- cgit From 89b4aa5d62237f36fac9dcce9c546005ec18968b Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 1 Oct 2018 20:52:31 -0400 Subject: EIP-1102: Add option to force-enable provider --- app/scripts/contentscript.js | 19 +++++++++++++++++++ app/scripts/controllers/provider-approval.js | 19 ++++++++++++++++--- app/scripts/inpage.js | 8 ++++++-- app/scripts/metamask-controller.js | 4 +++- 4 files changed, 44 insertions(+), 6 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 060343031..1788cfc36 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -24,6 +24,7 @@ if (shouldInjectWeb3()) { injectScript(inpageBundle) setupStreams() listenForProviderRequest() + checkForcedInjection() } /** @@ -134,10 +135,28 @@ function listenForProviderRequest () { case 'reject-provider-request': injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`) break + case 'force-injection': + extension.storage.local.get(['forcedOrigins'], ({ forcedOrigins = [] }) => { + extension.storage.local.set({ forcedOrigins: [ ...forcedOrigins, window.location.hostname ] }, () => { + injectScript(`window.location.reload()`) + }) + }) + break } }) } +/** + * Checks the current origin to see if it exists in the extension's locally-stored list + * off user-whitelisted dapp origins. If it is, this origin will be marked as approved, + * meaning the publicConfig stream will be enabled. This is only meant to ease the transition + * to 1102 and will be removed in the future. + */ +function checkForcedInjection () { + extension.storage.local.get(['forcedOrigins'], ({ forcedOrigins = [] }) => { + originApproved = forcedOrigins.indexOf(window.location.hostname) > -1 + }) +} /** * Error handler for page to plugin stream disconnections diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index e9b1b8e16..8c7520d59 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -1,4 +1,5 @@ const ObservableStore = require('obs-store') +const extension = require('extensionizer') /** * A controller that services user-approved requests for a full Ethereum provider API @@ -26,9 +27,9 @@ class ProviderApprovalController { * * @param {string} origin - Origin of the window requesting full provider access */ - handleProviderRequest (origin) { + async handleProviderRequest (origin) { this.store.updateState({ providerRequests: [{ origin }] }) - if (this.approvedOrigins[origin]) { + if (await this.isApproved(origin)) { this.approveProviderRequest(origin) return } @@ -68,6 +69,7 @@ class ProviderApprovalController { */ clearApprovedOrigins () { this.approvedOrigins = {} + extension.storage.local.set({ forcedOrigins: [] }) } /** @@ -77,7 +79,18 @@ class ProviderApprovalController { * @returns {boolean} - True if the origin has been approved */ isApproved (origin) { - return this.approvedOrigins[origin] + return new Promise(resolve => { + extension.storage.local.get(['forcedOrigins'], ({ forcedOrigins = [] }) => { + resolve(this.approvedOrigins[origin] || forcedOrigins.indexOf(origin) > -1) + }) + }) + } + + /** + * Called when a user forces the exposure of a full Ethereum provider API + */ + forceInjection () { + this.platform.sendMessage({ action: 'force-injection' }, { active: true }) } } diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 25bfe1416..5b5c02c26 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -37,8 +37,12 @@ inpageProvider.enable = function () { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - inpageProvider.publicConfigStore.once('update', () => { - resolve(inpageProvider.send({ method: 'eth_accounts' }).result) + inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { + if (error) { + reject(error) + } else { + resolve(response.result) + } }) } }) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index cffc5797b..d8f8a4602 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -275,7 +275,8 @@ module.exports = class MetamaskController extends EventEmitter { getAccounts: async ({ origin }) => { // Expose no accounts if this origin has not been approved, preventing // account-requring RPC methods from completing successfully - if (origin !== 'MetaMask' && !this.providerApprovalController.isApproved(origin)) { return [] } + const isApproved = await this.providerApprovalController.isApproved(origin) + if (origin !== 'MetaMask' && !isApproved) { return [] } const isUnlocked = this.keyringController.memStore.getState().isUnlocked const selectedAddress = this.preferencesController.getSelectedAddress() // only show address if account is unlocked @@ -454,6 +455,7 @@ module.exports = class MetamaskController extends EventEmitter { approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController), + forceInjection: providerApprovalController.forceInjection.bind(providerApprovalController), } } -- cgit From bfcb73ad533b7c2acea012a586c2a391811faf03 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Thu, 4 Oct 2018 11:05:32 -0400 Subject: EIP-1102: add isEnabled convenience method to provider --- app/scripts/contentscript.js | 28 ++++++++++++++++++++-------- app/scripts/controllers/provider-approval.js | 20 +++++++++++++++++++- app/scripts/inpage.js | 14 ++++++++++++++ 3 files changed, 53 insertions(+), 9 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 1788cfc36..b1c1e9a0d 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -116,16 +116,25 @@ function setupStreams () { * handles posting these messages automatically. */ function listenForProviderRequest () { - window.addEventListener('message', (event) => { - if (event.source !== window) { return } - if (!event.data || !event.data.type || event.data.type !== 'ETHEREUM_ENABLE_PROVIDER') { return } - extension.runtime.sendMessage({ - action: 'init-provider-request', - origin: event.source.location.hostname, - }) + window.addEventListener('message', ({ source, data }) => { + if (source !== window || !data || !data.type) { return } + switch (data.type) { + case 'ETHEREUM_ENABLE_PROVIDER': + extension.runtime.sendMessage({ + action: 'init-provider-request', + origin: source.location.hostname, + }) + break + case 'ETHEREUM_PROVIDER_STATUS': + extension.runtime.sendMessage({ + action: 'provider-status-request', + origin: source.location.hostname, + }) + break + } }) - extension.runtime.onMessage.addListener(({ action }) => { + extension.runtime.onMessage.addListener(({ action, isEnabled }) => { if (!action) { return } switch (action) { case 'approve-provider-request': @@ -142,6 +151,9 @@ function listenForProviderRequest () { }) }) break + case 'provider-status': + injectScript(`window.dispatchEvent(new CustomEvent('ethereumproviderstatus', { detail: { isEnabled: ${isEnabled}}}))`) + break } }) } diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 8c7520d59..918fc8ad0 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -18,7 +18,15 @@ class ProviderApprovalController { this.publicConfigStore = publicConfigStore this.approvedOrigins = {} platform && platform.addMessageListener && platform.addMessageListener(({ action, origin }) => { - action && action === 'init-provider-request' && this.handleProviderRequest(origin) + if (!action) { return } + switch (action) { + case 'init-provider-request': + this.handleProviderRequest(origin) + break + case 'provider-status-request': + this.handleProviderStatusRequest(origin) + break + } }) } @@ -36,6 +44,16 @@ class ProviderApprovalController { this.openPopup && this.openPopup() } + /** + * Called by a tab to detemrine if a full Ethereum provider API is exposed + * + * @param {string} origin - Origin of the window requesting provider status + */ + async handleProviderStatusRequest (origin) { + const isEnabled = await this.isApproved(origin) + this.platform && this.platform.sendMessage({ action: 'provider-status', isEnabled }, { active: true }) + } + /** * Called when a user approves access to a full Ethereum provider API * diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 5b5c02c26..c5f4ee4c9 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -50,6 +50,19 @@ inpageProvider.enable = function () { }) } +inpageProvider.isEnabled = function () { + return new Promise((resolve, reject) => { + window.addEventListener('ethereumproviderstatus', ({ detail }) => { + if (typeof detail.error !== 'undefined') { + reject(detail.error) + } else { + resolve(detail.isEnabled) + } + }) + window.postMessage({ type: 'ETHEREUM_PROVIDER_STATUS' }, '*') + }) +} + // 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, { @@ -60,6 +73,7 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { window.ethereum = proxiedInpageProvider + // // setup web3 // -- cgit From 226601a956594d00817cdb1fa5214242aae7936c Mon Sep 17 00:00:00 2001 From: bitpshr Date: Wed, 10 Oct 2018 14:52:26 -0400 Subject: EIP-1102: add user privacy option --- app/scripts/contentscript.js | 27 ++++++------------ app/scripts/controllers/provider-approval.js | 41 ++++++++++++++-------------- app/scripts/inpage.js | 4 +-- app/scripts/metamask-controller.js | 4 +-- 4 files changed, 32 insertions(+), 44 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index b1c1e9a0d..29fa3f5c7 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -24,7 +24,7 @@ if (shouldInjectWeb3()) { injectScript(inpageBundle) setupStreams() listenForProviderRequest() - checkForcedInjection() + checkPrivacyMode() } /** @@ -125,9 +125,9 @@ function listenForProviderRequest () { origin: source.location.hostname, }) break - case 'ETHEREUM_PROVIDER_STATUS': + case 'ETHEREUM_QUERY_STATUS': extension.runtime.sendMessage({ - action: 'provider-status-request', + action: 'init-status-request', origin: source.location.hostname, }) break @@ -144,14 +144,7 @@ function listenForProviderRequest () { case 'reject-provider-request': injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`) break - case 'force-injection': - extension.storage.local.get(['forcedOrigins'], ({ forcedOrigins = [] }) => { - extension.storage.local.set({ forcedOrigins: [ ...forcedOrigins, window.location.hostname ] }, () => { - injectScript(`window.location.reload()`) - }) - }) - break - case 'provider-status': + case 'answer-status-request': injectScript(`window.dispatchEvent(new CustomEvent('ethereumproviderstatus', { detail: { isEnabled: ${isEnabled}}}))`) break } @@ -159,15 +152,11 @@ function listenForProviderRequest () { } /** - * Checks the current origin to see if it exists in the extension's locally-stored list - * off user-whitelisted dapp origins. If it is, this origin will be marked as approved, - * meaning the publicConfig stream will be enabled. This is only meant to ease the transition - * to 1102 and will be removed in the future. + * Checks if MetaMask is currently operating in "privacy mode", meaning + * dapps must call ethereum.enable in order to access user accounts */ -function checkForcedInjection () { - extension.storage.local.get(['forcedOrigins'], ({ forcedOrigins = [] }) => { - originApproved = forcedOrigins.indexOf(window.location.hostname) > -1 - }) +function checkPrivacyMode () { + extension.runtime.sendMessage({ action: 'init-privacy-request' }) } /** diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 918fc8ad0..a44d2b3ab 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -1,5 +1,4 @@ const ObservableStore = require('obs-store') -const extension = require('extensionizer') /** * A controller that services user-approved requests for a full Ethereum provider API @@ -10,22 +9,25 @@ class ProviderApprovalController { * * @param {Object} [config] - Options to configure controller */ - constructor ({ closePopup, openPopup, platform, publicConfigStore } = {}) { + constructor ({ closePopup, openPopup, platform, preferencesController, publicConfigStore } = {}) { this.store = new ObservableStore() this.closePopup = closePopup this.openPopup = openPopup this.platform = platform this.publicConfigStore = publicConfigStore this.approvedOrigins = {} + this.preferencesController = preferencesController platform && platform.addMessageListener && platform.addMessageListener(({ action, origin }) => { if (!action) { return } switch (action) { case 'init-provider-request': this.handleProviderRequest(origin) break - case 'provider-status-request': + case 'init-status-request': this.handleProviderStatusRequest(origin) break + case 'init-privacy-request': + this.handlePrivacyStatusRequest() } }) } @@ -35,9 +37,9 @@ class ProviderApprovalController { * * @param {string} origin - Origin of the window requesting full provider access */ - async handleProviderRequest (origin) { + handleProviderRequest (origin) { this.store.updateState({ providerRequests: [{ origin }] }) - if (await this.isApproved(origin)) { + if (this.isApproved(origin)) { this.approveProviderRequest(origin) return } @@ -45,13 +47,21 @@ class ProviderApprovalController { } /** - * Called by a tab to detemrine if a full Ethereum provider API is exposed + * Called by a tab to determine if a full Ethereum provider API is exposed * * @param {string} origin - Origin of the window requesting provider status */ async handleProviderStatusRequest (origin) { - const isEnabled = await this.isApproved(origin) - this.platform && this.platform.sendMessage({ action: 'provider-status', isEnabled }, { active: true }) + const isEnabled = this.isApproved(origin) + this.platform && this.platform.sendMessage({ action: 'answer-status-request', isEnabled }, { active: true }) + } + + handlePrivacyStatusRequest () { + const privacyMode = this.preferencesController.getFeatureFlags().privacyMode + if (!privacyMode) { + this.platform && this.platform.sendMessage({ action: 'approve-provider-request' }, { active: true }) + this.publicConfigStore.emit('update', this.publicConfigStore.getState()) + } } /** @@ -87,7 +97,6 @@ class ProviderApprovalController { */ clearApprovedOrigins () { this.approvedOrigins = {} - extension.storage.local.set({ forcedOrigins: [] }) } /** @@ -97,18 +106,8 @@ class ProviderApprovalController { * @returns {boolean} - True if the origin has been approved */ isApproved (origin) { - return new Promise(resolve => { - extension.storage.local.get(['forcedOrigins'], ({ forcedOrigins = [] }) => { - resolve(this.approvedOrigins[origin] || forcedOrigins.indexOf(origin) > -1) - }) - }) - } - - /** - * Called when a user forces the exposure of a full Ethereum provider API - */ - forceInjection () { - this.platform.sendMessage({ action: 'force-injection' }, { active: true }) + const privacyMode = this.preferencesController.getFeatureFlags().privacyMode + return !privacyMode || this.approvedOrigins[origin] } } diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index c5f4ee4c9..c5cbcc120 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -56,10 +56,10 @@ inpageProvider.isEnabled = function () { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - resolve(detail.isEnabled) + resolve(!!detail.isEnabled) } }) - window.postMessage({ type: 'ETHEREUM_PROVIDER_STATUS' }, '*') + window.postMessage({ type: 'ETHEREUM_QUERY_STATUS' }, '*') }) } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index d8f8a4602..2265838fb 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -224,6 +224,7 @@ module.exports = class MetamaskController extends EventEmitter { closePopup: opts.closePopup, openPopup: opts.openPopup, platform: opts.platform, + preferencesController: this.preferencesController, publicConfigStore: this.publicConfigStore, }) @@ -275,7 +276,7 @@ module.exports = class MetamaskController extends EventEmitter { getAccounts: async ({ origin }) => { // Expose no accounts if this origin has not been approved, preventing // account-requring RPC methods from completing successfully - const isApproved = await this.providerApprovalController.isApproved(origin) + const isApproved = this.providerApprovalController.isApproved(origin) if (origin !== 'MetaMask' && !isApproved) { return [] } const isUnlocked = this.keyringController.memStore.getState().isUnlocked const selectedAddress = this.preferencesController.getSelectedAddress() @@ -455,7 +456,6 @@ module.exports = class MetamaskController extends EventEmitter { approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController), - forceInjection: providerApprovalController.forceInjection.bind(providerApprovalController), } } -- cgit From 3a2ade4e8490fa754b66e8f342a8308879e2f9ed Mon Sep 17 00:00:00 2001 From: bitpshr Date: Wed, 17 Oct 2018 17:45:21 -0400 Subject: Update isEnabled check --- app/scripts/inpage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index c5cbcc120..bfafc255d 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -29,6 +29,7 @@ var metamaskStream = new LocalMessageDuplexStream({ var inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) +var originApproved = false // Augment the provider with its enable method inpageProvider.enable = function () { @@ -37,6 +38,7 @@ inpageProvider.enable = function () { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { + originApproved = true inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { if (error) { reject(error) @@ -56,7 +58,7 @@ inpageProvider.isEnabled = function () { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - resolve(!!detail.isEnabled) + resolve(originApproved && !!detail.isEnabled) } }) window.postMessage({ type: 'ETHEREUM_QUERY_STATUS' }, '*') -- cgit From 573139b9357ccd97eb6b866721fafc93ceb080b6 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Wed, 17 Oct 2018 18:29:40 -0400 Subject: Differentiate isEnabled and isApproved hook --- app/scripts/inpage.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index bfafc255d..88831c0cc 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -27,9 +27,10 @@ var metamaskStream = new LocalMessageDuplexStream({ // compose the inpage provider var inpageProvider = new MetamaskInpageProvider(metamaskStream) + // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) -var originApproved = false +var isEnabled = false // Augment the provider with its enable method inpageProvider.enable = function () { @@ -38,11 +39,11 @@ inpageProvider.enable = function () { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - originApproved = true inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { if (error) { reject(error) } else { + isEnabled = true resolve(response.result) } }) @@ -53,18 +54,26 @@ inpageProvider.enable = function () { } inpageProvider.isEnabled = function () { + return isEnabled +} + +inpageProvider.isApproved = function () { return new Promise((resolve, reject) => { window.addEventListener('ethereumproviderstatus', ({ detail }) => { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - resolve(originApproved && !!detail.isEnabled) + resolve(!!detail.isEnabled) } }) window.postMessage({ type: 'ETHEREUM_QUERY_STATUS' }, '*') }) } +inpageProvider.isUnlocked = function () { + +} + // 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, { -- cgit From 84874a639d217da36926869fa3cb235c05725cf5 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Wed, 17 Oct 2018 18:38:31 -0400 Subject: Add isUnlocked provider hook --- app/scripts/contentscript.js | 10 +++++++++- app/scripts/controllers/provider-approval.js | 10 +++++++++- app/scripts/inpage.js | 11 ++++++++++- app/scripts/metamask-controller.js | 1 + 4 files changed, 29 insertions(+), 3 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 29fa3f5c7..bb79e1d4a 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -131,10 +131,15 @@ function listenForProviderRequest () { origin: source.location.hostname, }) break + case 'METAMASK_UNLOCK_STATUS': + extension.runtime.sendMessage({ + action: 'init-unlock-request', + }) + break } }) - extension.runtime.onMessage.addListener(({ action, isEnabled }) => { + extension.runtime.onMessage.addListener(({ action, isEnabled, isUnlocked }) => { if (!action) { return } switch (action) { case 'approve-provider-request': @@ -147,6 +152,9 @@ function listenForProviderRequest () { case 'answer-status-request': injectScript(`window.dispatchEvent(new CustomEvent('ethereumproviderstatus', { detail: { isEnabled: ${isEnabled}}}))`) break + case 'answer-unlock-request': + injectScript(`window.dispatchEvent(new CustomEvent('metamaskunlockstatus', { detail: { isUnlocked: ${isUnlocked}}}))`) + break } }) } diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index a44d2b3ab..c9680cb46 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -9,7 +9,7 @@ class ProviderApprovalController { * * @param {Object} [config] - Options to configure controller */ - constructor ({ closePopup, openPopup, platform, preferencesController, publicConfigStore } = {}) { + constructor ({ closePopup, openPopup, keyringController, platform, preferencesController, publicConfigStore } = {}) { this.store = new ObservableStore() this.closePopup = closePopup this.openPopup = openPopup @@ -17,6 +17,7 @@ class ProviderApprovalController { this.publicConfigStore = publicConfigStore this.approvedOrigins = {} this.preferencesController = preferencesController + this.keyringController = keyringController platform && platform.addMessageListener && platform.addMessageListener(({ action, origin }) => { if (!action) { return } switch (action) { @@ -26,6 +27,8 @@ class ProviderApprovalController { case 'init-status-request': this.handleProviderStatusRequest(origin) break + case 'init-unlock-request': + this.handleUnlockRequest() case 'init-privacy-request': this.handlePrivacyStatusRequest() } @@ -56,6 +59,11 @@ class ProviderApprovalController { this.platform && this.platform.sendMessage({ action: 'answer-status-request', isEnabled }, { active: true }) } + handleUnlockRequest() { + const isUnlocked = this.keyringController.memStore.getState().isUnlocked + this.platform && this.platform.sendMessage({ action: 'answer-unlock-request', isUnlocked }, { active: true }) + } + handlePrivacyStatusRequest () { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode if (!privacyMode) { diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 88831c0cc..72f7033f9 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -71,7 +71,16 @@ inpageProvider.isApproved = function () { } inpageProvider.isUnlocked = function () { - + return new Promise((resolve, reject) => { + window.addEventListener('metamaskunlockstatus', ({ detail }) => { + if (typeof detail.error !== 'undefined') { + reject(detail.error) + } else { + resolve(!!detail.isUnlocked) + } + }) + window.postMessage({ type: 'METAMASK_UNLOCK_STATUS' }, '*') + }) } // Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2265838fb..8930a3f2d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -226,6 +226,7 @@ module.exports = class MetamaskController extends EventEmitter { platform: opts.platform, preferencesController: this.preferencesController, publicConfigStore: this.publicConfigStore, + keyringController: this.keyringController, }) this.store.updateStructure({ -- cgit From 504f4a50f13b12588576f2f06bb2e371bd966dfb Mon Sep 17 00:00:00 2001 From: bitpshr Date: Wed, 17 Oct 2018 18:43:51 -0400 Subject: Fix lint issues --- app/scripts/controllers/provider-approval.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index c9680cb46..fa2fb2cf8 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -29,8 +29,10 @@ class ProviderApprovalController { break case 'init-unlock-request': this.handleUnlockRequest() + break case 'init-privacy-request': this.handlePrivacyStatusRequest() + break } }) } @@ -59,7 +61,7 @@ class ProviderApprovalController { this.platform && this.platform.sendMessage({ action: 'answer-status-request', isEnabled }, { active: true }) } - handleUnlockRequest() { + handleUnlockRequest () { const isUnlocked = this.keyringController.memStore.getState().isUnlocked this.platform && this.platform.sendMessage({ action: 'answer-unlock-request', isUnlocked }, { active: true }) } -- cgit From 32630b68dfae257a59bf1ce985e9a04ca1ca58b4 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 22 Oct 2018 15:08:26 -0400 Subject: Move convenience methods to _metamask namespace --- app/scripts/inpage.js | 83 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 28 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 72f7033f9..9d50a695f 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -10,8 +10,8 @@ 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. ' + +console.warn('ATTENTION: In an effort to improve user privacy, MetaMask ' + +'stopped 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.') @@ -31,8 +31,9 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) var isEnabled = false +var warned = false -// Augment the provider with its enable method +// augment the provider with its enable method inpageProvider.enable = function () { return new Promise((resolve, reject) => { window.addEventListener('ethereumprovider', ({ detail }) => { @@ -53,35 +54,61 @@ inpageProvider.enable = function () { }) } -inpageProvider.isEnabled = function () { - return isEnabled -} +// add metamask-specific convenience methods +inpageProvider._metamask = new Proxy({ + /** + * Determines if this domain is currently enabled + * + * @returns {boolean} - true if this domain is currently enabled + */ + isEnabled: function () { + return isEnabled + }, -inpageProvider.isApproved = function () { - return new Promise((resolve, reject) => { - window.addEventListener('ethereumproviderstatus', ({ detail }) => { - if (typeof detail.error !== 'undefined') { - reject(detail.error) - } else { - resolve(!!detail.isEnabled) - } + /** + * Determines if this domain has been previously approved + * + * @returns {Promise} - Promise resolving to true if this domain has been previously approved + */ + isApproved: function() { + return new Promise((resolve, reject) => { + window.addEventListener('ethereumproviderstatus', ({ detail }) => { + if (typeof detail.error !== 'undefined') { + reject(detail.error) + } else { + resolve(!!detail.isEnabled) + } + }) + window.postMessage({ type: 'ETHEREUM_QUERY_STATUS' }, '*') }) - window.postMessage({ type: 'ETHEREUM_QUERY_STATUS' }, '*') - }) -} + }, -inpageProvider.isUnlocked = function () { - return new Promise((resolve, reject) => { - window.addEventListener('metamaskunlockstatus', ({ detail }) => { - if (typeof detail.error !== 'undefined') { - reject(detail.error) - } else { - resolve(!!detail.isUnlocked) - } + /** + * Determines if MetaMask is unlocked by the user + * + * @returns {Promise} - Promise resolving to true if MetaMask is currently unlocked + */ + isUnlocked: function () { + return new Promise((resolve, reject) => { + window.addEventListener('metamaskunlockstatus', ({ detail }) => { + if (typeof detail.error !== 'undefined') { + reject(detail.error) + } else { + resolve(!!detail.isUnlocked) + } + }) + window.postMessage({ type: 'METAMASK_UNLOCK_STATUS' }, '*') }) - window.postMessage({ type: 'METAMASK_UNLOCK_STATUS' }, '*') - }) -} + }, +}, { + get: function(obj, prop) { + !warned && console.warn('Heads up! ethereum._metamask exposes convenience methods that have ' + + 'not been standardized yet. This means that these methods may not be implemented ' + + 'in other dapp browsers.') + warned = true + return obj[prop] + }, +}) // 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 -- cgit From f35466a247e503659228f0b8e098275a875026a4 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Tue, 23 Oct 2018 13:19:01 -0400 Subject: Fix race condition with publicConfigStore --- app/scripts/inpage.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 9d50a695f..cadc61727 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -40,14 +40,33 @@ inpageProvider.enable = function () { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { - if (error) { - reject(error) + const publicConfig = new Promise((resolve) => { + const { selectedAddress } = inpageProvider.publicConfigStore.getState() + if (selectedAddress) { + resolve() } else { - isEnabled = true - resolve(response.result) + inpageProvider.publicConfigStore.on('update', ({ selectedAddress }) => { + selectedAddress && resolve() + }) } }) + + const ethAccounts = new Promise((resolveAccounts, rejectAccounts) => { + inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { + if (error) { + rejectAccounts(error) + } else { + resolveAccounts(response.result) + } + }) + }) + + Promise.all([ethAccounts, publicConfig]) + .then(([selectedAddress]) => { + isEnabled = true + resolve(selectedAddress) + }) + .catch(reject) } }) window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER' }, '*') -- cgit From ace7cfa065d701b49fb07c4e6f169e7cce545c56 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 29 Oct 2018 19:31:06 +0100 Subject: Only filter selectedAddress from publicConfig store --- app/scripts/contentscript.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index bb79e1d4a..aa06068c0 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -7,6 +7,7 @@ const PongStream = require('ping-pong-stream/pong') const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') const PortStream = require('extension-port-stream') +const TransformStream = require('stream').Transform const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' @@ -58,18 +59,21 @@ function setupStreams () { // Until this origin is approved, cut-off publicConfig stream writes at the content // script level so malicious sites can't snoop on the currently-selected address - pageStream._write = function (data, encoding, cb) { - if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !originApproved) { - cb() - return + const approvalTransform = new TransformStream({ + objectMode: true, + transform: (data, _, done) => { + if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !originApproved) { + data.data.selectedAddress = undefined + } + done(null, { ...data }) } - LocalMessageDuplexStream.prototype._write.apply(pageStream, arguments) - } + }) // forward communication plugin->inpage pump( pageStream, pluginStream, + approvalTransform, pageStream, (err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err) ) -- cgit From f02e18dd80672a0b7440256cb7946feabf907ee1 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 29 Oct 2018 19:38:45 +0100 Subject: Fix lint issues --- app/scripts/contentscript.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index aa06068c0..fdc04ba1b 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -66,7 +66,7 @@ function setupStreams () { data.data.selectedAddress = undefined } done(null, { ...data }) - } + }, }) // forward communication plugin->inpage -- cgit From ba40fcbcb43c5adcb3a961afd4050cdb2025b7a6 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 29 Oct 2018 21:55:13 +0100 Subject: Handle logout gracefully --- app/scripts/contentscript.js | 21 ++++++++++++--------- app/scripts/controllers/provider-approval.js | 25 +++++++++++++++---------- app/scripts/inpage.js | 14 +++++++++----- app/scripts/metamask-controller.js | 7 ++++++- 4 files changed, 42 insertions(+), 25 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index fdc04ba1b..0244f6fa0 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -129,21 +129,21 @@ function listenForProviderRequest () { origin: source.location.hostname, }) break - case 'ETHEREUM_QUERY_STATUS': + case 'ETHEREUM_IS_APPROVED': extension.runtime.sendMessage({ - action: 'init-status-request', + action: 'init-is-approved', origin: source.location.hostname, }) break - case 'METAMASK_UNLOCK_STATUS': + case 'METAMASK_IS_UNLOCKED': extension.runtime.sendMessage({ - action: 'init-unlock-request', + action: 'init-is-unlocked', }) break } }) - extension.runtime.onMessage.addListener(({ action, isEnabled, isUnlocked }) => { + extension.runtime.onMessage.addListener(({ action, isEnabled, isApproved, isUnlocked }) => { if (!action) { return } switch (action) { case 'approve-provider-request': @@ -153,11 +153,14 @@ function listenForProviderRequest () { case 'reject-provider-request': injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`) break - case 'answer-status-request': - injectScript(`window.dispatchEvent(new CustomEvent('ethereumproviderstatus', { detail: { isEnabled: ${isEnabled}}}))`) + case 'answer-is-approved': + injectScript(`window.dispatchEvent(new CustomEvent('ethereumisapproved', { detail: { isApproved: ${isApproved}}}))`) break - case 'answer-unlock-request': - injectScript(`window.dispatchEvent(new CustomEvent('metamaskunlockstatus', { detail: { isUnlocked: ${isUnlocked}}}))`) + case 'answer-is-unlocked': + injectScript(`window.dispatchEvent(new CustomEvent('metamaskisunlocked', { detail: { isUnlocked: ${isUnlocked}}}))`) + break + case 'metamask-set-locked': + injectScript(`window.dispatchEvent(new CustomEvent('metamasksetlocked', { detail: {}}))`) break } }) diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index fa2fb2cf8..3af165438 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -24,11 +24,11 @@ class ProviderApprovalController { case 'init-provider-request': this.handleProviderRequest(origin) break - case 'init-status-request': - this.handleProviderStatusRequest(origin) + case 'init-is-approved': + this.handleIsApproved(origin) break - case 'init-unlock-request': - this.handleUnlockRequest() + case 'init-is-unlocked': + this.handleIsUnlocked() break case 'init-privacy-request': this.handlePrivacyStatusRequest() @@ -44,7 +44,8 @@ class ProviderApprovalController { */ handleProviderRequest (origin) { this.store.updateState({ providerRequests: [{ origin }] }) - if (this.isApproved(origin)) { + const isUnlocked = this.keyringController.memStore.getState().isUnlocked + if (isUnlocked && this.isApproved(origin)) { this.approveProviderRequest(origin) return } @@ -56,14 +57,14 @@ class ProviderApprovalController { * * @param {string} origin - Origin of the window requesting provider status */ - async handleProviderStatusRequest (origin) { - const isEnabled = this.isApproved(origin) - this.platform && this.platform.sendMessage({ action: 'answer-status-request', isEnabled }, { active: true }) + async handleIsApproved (origin) { + const isApproved = this.isApproved(origin) + this.platform && this.platform.sendMessage({ action: 'answer-is-approved', isApproved }, { active: true }) } - handleUnlockRequest () { + handleIsUnlocked () { const isUnlocked = this.keyringController.memStore.getState().isUnlocked - this.platform && this.platform.sendMessage({ action: 'answer-unlock-request', isUnlocked }, { active: true }) + this.platform && this.platform.sendMessage({ action: 'answer-is-unlocked', isUnlocked }, { active: true }) } handlePrivacyStatusRequest () { @@ -119,6 +120,10 @@ class ProviderApprovalController { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode return !privacyMode || this.approvedOrigins[origin] } + + setLocked () { + this.platform.sendMessage({ action: 'metamask-set-locked' }) + } } module.exports = ProviderApprovalController diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index cadc61727..49a18c5e9 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -33,6 +33,10 @@ inpageProvider.setMaxListeners(100) var isEnabled = false var warned = false +window.addEventListener('metamasksetlocked', () => { + isEnabled = false +}) + // augment the provider with its enable method inpageProvider.enable = function () { return new Promise((resolve, reject) => { @@ -91,14 +95,14 @@ inpageProvider._metamask = new Proxy({ */ isApproved: function() { return new Promise((resolve, reject) => { - window.addEventListener('ethereumproviderstatus', ({ detail }) => { + window.addEventListener('ethereumisapproved', ({ detail }) => { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - resolve(!!detail.isEnabled) + resolve(!!detail.isApproved) } }) - window.postMessage({ type: 'ETHEREUM_QUERY_STATUS' }, '*') + window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*') }) }, @@ -109,14 +113,14 @@ inpageProvider._metamask = new Proxy({ */ isUnlocked: function () { return new Promise((resolve, reject) => { - window.addEventListener('metamaskunlockstatus', ({ detail }) => { + window.addEventListener('metamaskisunlocked', ({ detail }) => { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { resolve(!!detail.isUnlocked) } }) - window.postMessage({ type: 'METAMASK_UNLOCK_STATUS' }, '*') + window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*') }) }, }, { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8930a3f2d..bf1df7ff5 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -422,7 +422,7 @@ module.exports = class MetamaskController extends EventEmitter { setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController), // KeyringController - setLocked: nodeify(keyringController.setLocked, keyringController), + setLocked: nodeify(this.setLocked, this), createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this), createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this), addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController), @@ -1576,4 +1576,9 @@ module.exports = class MetamaskController extends EventEmitter { whitelistPhishingDomain (hostname) { return this.blacklistController.whitelistDomain(hostname) } + + setLocked() { + this.providerApprovalController.setLocked() + return this.keyringController.setLocked() + } } -- cgit From d7618bd5c6cffe02d8737fe6925a31484a1fc0b0 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 29 Oct 2018 22:28:59 +0100 Subject: Code bath --- app/scripts/contentscript.js | 17 +++++------ app/scripts/controllers/provider-approval.js | 44 +++++++++++++++++----------- app/scripts/inpage.js | 16 ++++++---- app/scripts/metamask-controller.js | 5 +++- 4 files changed, 49 insertions(+), 33 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 0244f6fa0..0723c03f7 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -12,7 +12,7 @@ const TransformStream = require('stream').Transform const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' const inpageBundle = inpageContent + inpageSuffix -let originApproved = false +let isEnabled = false // Eventually this streaming injection could be replaced with: // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction @@ -40,7 +40,7 @@ function injectScript (content) { scriptTag.textContent = content container.insertBefore(scriptTag, container.children[0]) } catch (e) { - console.error('Metamask script injection failed.', e) + console.error('MetaMask script injection failed', e) } } @@ -57,12 +57,11 @@ function setupStreams () { const pluginPort = extension.runtime.connect({ name: 'contentscript' }) const pluginStream = new PortStream(pluginPort) - // Until this origin is approved, cut-off publicConfig stream writes at the content - // script level so malicious sites can't snoop on the currently-selected address + // Filter out selectedAddress until this origin is enabled const approvalTransform = new TransformStream({ objectMode: true, transform: (data, _, done) => { - if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !originApproved) { + if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !isEnabled) { data.data.selectedAddress = undefined } done(null, { ...data }) @@ -117,7 +116,7 @@ function setupStreams () { * Establishes listeners for requests to fully-enable the provider from the dapp context * and for full-provider approvals and rejections from the background script context. Dapps * should not post messages directly and should instead call provider.enable(), which - * handles posting these messages automatically. + * handles posting these messages internally. */ function listenForProviderRequest () { window.addEventListener('message', ({ source, data }) => { @@ -143,11 +142,10 @@ function listenForProviderRequest () { } }) - extension.runtime.onMessage.addListener(({ action, isEnabled, isApproved, isUnlocked }) => { - if (!action) { return } + extension.runtime.onMessage.addListener(({ action = '', isApproved, isUnlocked }) => { switch (action) { case 'approve-provider-request': - originApproved = true + isEnabled = true injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: {}}))`) break case 'reject-provider-request': @@ -160,6 +158,7 @@ function listenForProviderRequest () { injectScript(`window.dispatchEvent(new CustomEvent('metamaskisunlocked', { detail: { isUnlocked: ${isUnlocked}}}))`) break case 'metamask-set-locked': + isEnabled = false injectScript(`window.dispatchEvent(new CustomEvent('metamasksetlocked', { detail: {}}))`) break } diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 3af165438..42393de85 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -9,29 +9,29 @@ class ProviderApprovalController { * * @param {Object} [config] - Options to configure controller */ - constructor ({ closePopup, openPopup, keyringController, platform, preferencesController, publicConfigStore } = {}) { - this.store = new ObservableStore() + constructor ({ closePopup, keyringController, openPopup, platform, preferencesController, publicConfigStore } = {}) { + this.approvedOrigins = {} this.closePopup = closePopup + this.keyringController = keyringController this.openPopup = openPopup this.platform = platform - this.publicConfigStore = publicConfigStore - this.approvedOrigins = {} this.preferencesController = preferencesController - this.keyringController = keyringController - platform && platform.addMessageListener && platform.addMessageListener(({ action, origin }) => { - if (!action) { return } + this.publicConfigStore = publicConfigStore + this.store = new ObservableStore() + + platform && platform.addMessageListener && platform.addMessageListener(({ action = '', origin }) => { switch (action) { case 'init-provider-request': - this.handleProviderRequest(origin) + this._handleProviderRequest(origin) break case 'init-is-approved': - this.handleIsApproved(origin) + this._handleIsApproved(origin) break case 'init-is-unlocked': - this.handleIsUnlocked() + this._handleIsUnlocked() break case 'init-privacy-request': - this.handlePrivacyStatusRequest() + this._handlePrivacyRequest() break } }) @@ -42,7 +42,7 @@ class ProviderApprovalController { * * @param {string} origin - Origin of the window requesting full provider access */ - handleProviderRequest (origin) { + _handleProviderRequest (origin) { this.store.updateState({ providerRequests: [{ origin }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked if (isUnlocked && this.isApproved(origin)) { @@ -53,21 +53,27 @@ class ProviderApprovalController { } /** - * Called by a tab to determine if a full Ethereum provider API is exposed + * Called by a tab to determine if an origin has been approved in the past * - * @param {string} origin - Origin of the window requesting provider status + * @param {string} origin - Origin of the window */ - async handleIsApproved (origin) { + _handleIsApproved (origin) { const isApproved = this.isApproved(origin) this.platform && this.platform.sendMessage({ action: 'answer-is-approved', isApproved }, { active: true }) } - handleIsUnlocked () { + /** + * Called by a tab to determine if MetaMask is currently locked or unlocked + */ + _handleIsUnlocked () { const isUnlocked = this.keyringController.memStore.getState().isUnlocked this.platform && this.platform.sendMessage({ action: 'answer-is-unlocked', isUnlocked }, { active: true }) } - handlePrivacyStatusRequest () { + /** + * Called to check privacy mode; if privacy mode is off, this will automatically enable the provider (legacy behavior) + */ + _handlePrivacyRequest () { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode if (!privacyMode) { this.platform && this.platform.sendMessage({ action: 'approve-provider-request' }, { active: true }) @@ -121,6 +127,10 @@ class ProviderApprovalController { return !privacyMode || this.approvedOrigins[origin] } + /** + * Tells all tabs that MetaMask is now locked. This is primarily used to set + * internal flags in the contentscript and inpage script. + */ setLocked () { this.platform.sendMessage({ action: 'metamask-set-locked' }) } diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 49a18c5e9..a5e0118d4 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -6,14 +6,18 @@ const LocalMessageDuplexStream = require('post-message-stream') const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('metamask-inpage-provider') +let isEnabled = false +let warned = false + restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') console.warn('ATTENTION: In an effort to improve user privacy, MetaMask ' + -'stopped 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.') +'stopped exposing user accounts to dapps if "privacy mode" is enabled on ' + +'November 2nd, 2018. Dapps should now 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 @@ -30,9 +34,8 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) -var isEnabled = false -var warned = false +// set up a listener for when MetaMask is locked window.addEventListener('metamasksetlocked', () => { isEnabled = false }) @@ -44,6 +47,7 @@ inpageProvider.enable = function () { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { + // wait for the publicConfig store to populate with an account const publicConfig = new Promise((resolve) => { const { selectedAddress } = inpageProvider.publicConfigStore.getState() if (selectedAddress) { @@ -55,6 +59,7 @@ inpageProvider.enable = function () { } }) + // wait for the background to update with an accoount const ethAccounts = new Promise((resolveAccounts, rejectAccounts) => { inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { if (error) { @@ -143,7 +148,6 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { window.ethereum = proxiedInpageProvider - // // setup web3 // diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index bf1df7ff5..33278db85 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -222,11 +222,11 @@ module.exports = class MetamaskController extends EventEmitter { this.providerApprovalController = new ProviderApprovalController({ closePopup: opts.closePopup, + keyringController: this.keyringController, openPopup: opts.openPopup, platform: opts.platform, preferencesController: this.preferencesController, publicConfigStore: this.publicConfigStore, - keyringController: this.keyringController, }) this.store.updateStructure({ @@ -1577,6 +1577,9 @@ module.exports = class MetamaskController extends EventEmitter { return this.blacklistController.whitelistDomain(hostname) } + /** + * Locks MetaMask + */ setLocked() { this.providerApprovalController.setLocked() return this.keyringController.setLocked() -- cgit From cc1bab6ebbef8d8219f83039fdc3baca6de718fd Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 29 Oct 2018 22:54:39 +0100 Subject: Differentiate locked and enabled --- app/scripts/contentscript.js | 4 ---- app/scripts/controllers/provider-approval.js | 11 +---------- app/scripts/inpage.js | 21 +++++++++------------ app/scripts/metamask-controller.js | 1 - 4 files changed, 10 insertions(+), 27 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 0723c03f7..2c2efda1c 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -157,10 +157,6 @@ function listenForProviderRequest () { case 'answer-is-unlocked': injectScript(`window.dispatchEvent(new CustomEvent('metamaskisunlocked', { detail: { isUnlocked: ${isUnlocked}}}))`) break - case 'metamask-set-locked': - isEnabled = false - injectScript(`window.dispatchEvent(new CustomEvent('metamasksetlocked', { detail: {}}))`) - break } }) } diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 42393de85..66017c58e 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -44,8 +44,7 @@ class ProviderApprovalController { */ _handleProviderRequest (origin) { this.store.updateState({ providerRequests: [{ origin }] }) - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - if (isUnlocked && this.isApproved(origin)) { + if (this.isApproved(origin)) { this.approveProviderRequest(origin) return } @@ -126,14 +125,6 @@ class ProviderApprovalController { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode return !privacyMode || this.approvedOrigins[origin] } - - /** - * Tells all tabs that MetaMask is now locked. This is primarily used to set - * internal flags in the contentscript and inpage script. - */ - setLocked () { - this.platform.sendMessage({ action: 'metamask-set-locked' }) - } } module.exports = ProviderApprovalController diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index a5e0118d4..a60d19480 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -35,11 +35,6 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) -// set up a listener for when MetaMask is locked -window.addEventListener('metamasksetlocked', () => { - isEnabled = false -}) - // augment the provider with its enable method inpageProvider.enable = function () { return new Promise((resolve, reject) => { @@ -50,13 +45,15 @@ inpageProvider.enable = function () { // wait for the publicConfig store to populate with an account const publicConfig = new Promise((resolve) => { const { selectedAddress } = inpageProvider.publicConfigStore.getState() - if (selectedAddress) { - resolve() - } else { - inpageProvider.publicConfigStore.on('update', ({ selectedAddress }) => { - selectedAddress && resolve() - }) - } + inpageProvider._metamask.isUnlocked().then(unlocked => { + if (!unlocked || selectedAddress) { + resolve() + } else { + inpageProvider.publicConfigStore.on('update', ({ selectedAddress }) => { + selectedAddress && resolve() + }) + } + }) }) // wait for the background to update with an accoount diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 33278db85..c7fc42eb4 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1581,7 +1581,6 @@ module.exports = class MetamaskController extends EventEmitter { * Locks MetaMask */ setLocked() { - this.providerApprovalController.setLocked() return this.keyringController.setLocked() } } -- cgit From d4171ccea51db04aa40320de8770e22203d4d6c2 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 29 Oct 2018 23:44:04 +0100 Subject: Disable approval caching --- app/scripts/contentscript.js | 8 ++++++-- app/scripts/controllers/provider-approval.js | 20 +++++++++++++++++--- app/scripts/inpage.js | 11 ++++++++++- app/scripts/metamask-controller.js | 1 + 4 files changed, 34 insertions(+), 6 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 2c2efda1c..fa8b3207f 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -142,7 +142,7 @@ function listenForProviderRequest () { } }) - extension.runtime.onMessage.addListener(({ action = '', isApproved, isUnlocked }) => { + extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked }) => { switch (action) { case 'approve-provider-request': isEnabled = true @@ -152,11 +152,15 @@ function listenForProviderRequest () { injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`) break case 'answer-is-approved': - injectScript(`window.dispatchEvent(new CustomEvent('ethereumisapproved', { detail: { isApproved: ${isApproved}}}))`) + injectScript(`window.dispatchEvent(new CustomEvent('ethereumisapproved', { detail: { isApproved: ${isApproved}, caching: ${caching}}}))`) break case 'answer-is-unlocked': injectScript(`window.dispatchEvent(new CustomEvent('metamaskisunlocked', { detail: { isUnlocked: ${isUnlocked}}}))`) break + case 'metamask-set-locked': + isEnabled = false + injectScript(`window.dispatchEvent(new CustomEvent('metamasksetlocked', { detail: {}}))`) + break } }) } diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 66017c58e..cbdebc3e3 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -4,6 +4,11 @@ const ObservableStore = require('obs-store') * A controller that services user-approved requests for a full Ethereum provider API */ class ProviderApprovalController { + /** + * Determines if caching is enabled + */ + caching = false + /** * Creates a ProviderApprovalController * @@ -44,7 +49,7 @@ class ProviderApprovalController { */ _handleProviderRequest (origin) { this.store.updateState({ providerRequests: [{ origin }] }) - if (this.isApproved(origin)) { + if (this.isApproved(origin) && this.caching) { this.approveProviderRequest(origin) return } @@ -57,8 +62,9 @@ class ProviderApprovalController { * @param {string} origin - Origin of the window */ _handleIsApproved (origin) { - const isApproved = this.isApproved(origin) - this.platform && this.platform.sendMessage({ action: 'answer-is-approved', isApproved }, { active: true }) + const isApproved = this.isApproved(origin) && this.caching + const caching = this.caching + this.platform && this.platform.sendMessage({ action: 'answer-is-approved', isApproved, caching }, { active: true }) } /** @@ -125,6 +131,14 @@ class ProviderApprovalController { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode return !privacyMode || this.approvedOrigins[origin] } + + /** + * Tells all tabs that MetaMask is now locked. This is primarily used to set + * internal flags in the contentscript and inpage script. + */ + setLocked () { + this.platform.sendMessage({ action: 'metamask-set-locked' }) + } } module.exports = ProviderApprovalController diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index a60d19480..29811c216 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -35,6 +35,11 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream) // set a high max listener count to avoid unnecesary warnings inpageProvider.setMaxListeners(100) +// set up a listener for when MetaMask is locked +window.addEventListener('metamasksetlocked', () => { + isEnabled = false +}) + // augment the provider with its enable method inpageProvider.enable = function () { return new Promise((resolve, reject) => { @@ -101,7 +106,11 @@ inpageProvider._metamask = new Proxy({ if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - resolve(!!detail.isApproved) + if (!detail.caching) { + resolve(!!detail.isApproved) + } else { + resolve(isEnabled) + } } }) window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*') diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index c7fc42eb4..33278db85 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1581,6 +1581,7 @@ module.exports = class MetamaskController extends EventEmitter { * Locks MetaMask */ setLocked() { + this.providerApprovalController.setLocked() return this.keyringController.setLocked() } } -- cgit From cc27a09a1aa676573c5b7354ba73b29ba38e08b8 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 29 Oct 2018 23:51:29 +0100 Subject: rebase --- app/scripts/inpage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 29811c216..ddbb43326 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -106,7 +106,7 @@ inpageProvider._metamask = new Proxy({ if (typeof detail.error !== 'undefined') { reject(detail.error) } else { - if (!detail.caching) { + if (detail.caching) { resolve(!!detail.isApproved) } else { resolve(isEnabled) @@ -136,9 +136,9 @@ inpageProvider._metamask = new Proxy({ }, }, { get: function(obj, prop) { - !warned && console.warn('Heads up! ethereum._metamask exposes convenience methods that have ' + + !warned && console.warn('Heads up! ethereum._metamask exposes methods that have ' + 'not been standardized yet. This means that these methods may not be implemented ' + - 'in other dapp browsers.') + 'in other dapp browsers and may be removed from MetaMask in the future.') warned = true return obj[prop] }, -- cgit From 72730b39296bf4531f69daa219b8cfccf06caac9 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Tue, 30 Oct 2018 00:09:21 +0100 Subject: Remove internal listeners --- app/scripts/inpage.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index ddbb43326..d22072a3d 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -8,6 +8,9 @@ const MetamaskInpageProvider = require('metamask-inpage-provider') let isEnabled = false let warned = false +let providerHandle +let isApprovedHandle +let isUnlockedHandle restoreContextAfterImports() @@ -43,7 +46,8 @@ window.addEventListener('metamasksetlocked', () => { // augment the provider with its enable method inpageProvider.enable = function () { return new Promise((resolve, reject) => { - window.addEventListener('ethereumprovider', ({ detail }) => { + window.removeEventListener('ethereumprovider', providerHandle) + providerHandle = ({ detail }) => { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { @@ -79,7 +83,8 @@ inpageProvider.enable = function () { }) .catch(reject) } - }) + } + window.addEventListener('ethereumprovider', providerHandle) window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER' }, '*') }) } @@ -102,7 +107,8 @@ inpageProvider._metamask = new Proxy({ */ isApproved: function() { return new Promise((resolve, reject) => { - window.addEventListener('ethereumisapproved', ({ detail }) => { + window.removeEventListener('ethereumisapproved', isApprovedHandle) + isApprovedHandle = ({ detail }) => { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { @@ -112,7 +118,8 @@ inpageProvider._metamask = new Proxy({ resolve(isEnabled) } } - }) + } + window.addEventListener('ethereumisapproved', isApprovedHandle) window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*') }) }, @@ -124,13 +131,15 @@ inpageProvider._metamask = new Proxy({ */ isUnlocked: function () { return new Promise((resolve, reject) => { - window.addEventListener('metamaskisunlocked', ({ detail }) => { + window.removeEventListener('metamaskisunlocked', isUnlockedHandle) + isUnlockedHandle = ({ detail }) => { if (typeof detail.error !== 'undefined') { reject(detail.error) } else { resolve(!!detail.isUnlocked) } - }) + } + window.addEventListener('metamaskisunlocked', isUnlockedHandle) window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*') }) }, -- cgit From 991e08e34639e8b5741a724682192fc6e626cb1f Mon Sep 17 00:00:00 2001 From: bitpshr Date: Wed, 31 Oct 2018 00:36:01 +0100 Subject: isApproved should return false --- app/scripts/inpage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index d22072a3d..794f15a0b 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -115,7 +115,7 @@ inpageProvider._metamask = new Proxy({ if (detail.caching) { resolve(!!detail.isApproved) } else { - resolve(isEnabled) + resolve(false) } } } -- cgit From f557d3371892ae7625528aa5168549c57cfd740e Mon Sep 17 00:00:00 2001 From: Paul Bouchon Date: Wed, 31 Oct 2018 10:07:49 +0100 Subject: Enable caching --- app/scripts/controllers/provider-approval.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index cbdebc3e3..003f221ac 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -7,7 +7,7 @@ class ProviderApprovalController { /** * Determines if caching is enabled */ - caching = false + caching = true /** * Creates a ProviderApprovalController -- cgit From 9984f7edebb0de4432e152a7c6f0d93bb00b9da6 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Sat, 3 Nov 2018 11:06:11 +0000 Subject: Mark origins as unapproved if user explicitly locks MetaMask --- app/scripts/controllers/provider-approval.js | 6 ++++-- app/scripts/inpage.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 003f221ac..10b971a73 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -49,7 +49,8 @@ class ProviderApprovalController { */ _handleProviderRequest (origin) { this.store.updateState({ providerRequests: [{ origin }] }) - if (this.isApproved(origin) && this.caching) { + const isUnlocked = this.keyringController.memStore.getState().isUnlocked + if (this.isApproved(origin) && this.caching && isUnlocked) { this.approveProviderRequest(origin) return } @@ -128,8 +129,9 @@ class ProviderApprovalController { * @returns {boolean} - True if the origin has been approved */ isApproved (origin) { + const isUnlocked = this.keyringController.memStore.getState().isUnlocked const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - return !privacyMode || this.approvedOrigins[origin] + return !privacyMode || (isUnlocked && this.approvedOrigins[origin]) } /** diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 794f15a0b..2ca3abde2 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -65,7 +65,7 @@ inpageProvider.enable = function () { }) }) - // wait for the background to update with an accoount + // wait for the background to update with an account const ethAccounts = new Promise((resolveAccounts, rejectAccounts) => { inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { if (error) { -- cgit From 31cb111d2e98c17728a75ebe00430654d827e136 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Sun, 4 Nov 2018 12:19:47 -0500 Subject: Do not modify isApproved when locked --- app/scripts/controllers/provider-approval.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 10b971a73..728361c79 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -129,9 +129,8 @@ class ProviderApprovalController { * @returns {boolean} - True if the origin has been approved */ isApproved (origin) { - const isUnlocked = this.keyringController.memStore.getState().isUnlocked const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - return !privacyMode || (isUnlocked && this.approvedOrigins[origin]) + return !privacyMode || this.approvedOrigins[origin] } /** -- cgit From 26ada8a828ab684c310080a18115a8ef3234aaee Mon Sep 17 00:00:00 2001 From: Whymarrh Whitby Date: Mon, 5 Nov 2018 09:37:56 -0330 Subject: Update Connect Request screen design (#5644) * Parameterize NetworkDisplay background colour * Update design for login request screen * Pass siteTitle, siteImage through for calls to ethereum.enable() * Bring the site images closer together --- app/scripts/contentscript.js | 30 +++++++++++++++++++++ app/scripts/controllers/provider-approval.js | 40 +++++++++++++++------------- 2 files changed, 52 insertions(+), 18 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index fa8b3207f..1cdc85945 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -126,6 +126,8 @@ function listenForProviderRequest () { extension.runtime.sendMessage({ action: 'init-provider-request', origin: source.location.hostname, + siteImage: getSiteIcon(source), + siteTitle: getSiteName(source), }) break case 'ETHEREUM_IS_APPROVED': @@ -285,3 +287,31 @@ function redirectToPhishingWarning () { href: window.location.href, })}` } + +function getSiteName (window) { + const document = window.document + const siteName = document.querySelector('head > meta[property="og:site_name"]') + if (siteName) { + return siteName.content + } + + return document.title +} + +function getSiteIcon (window) { + const document = window.document + + // Use the site's favicon if it exists + const shortcutIcon = document.querySelector('head > link[rel="shortcut icon"]') + if (shortcutIcon) { + return shortcutIcon.href + } + + // Search through available icons in no particular order + const icon = Array.from(document.querySelectorAll('head > link[rel="icon"]')).find((icon) => Boolean(icon.href)) + if (icon) { + return icon.href + } + + return null +} diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 728361c79..f2d40e67d 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -24,31 +24,35 @@ class ProviderApprovalController { this.publicConfigStore = publicConfigStore this.store = new ObservableStore() - platform && platform.addMessageListener && platform.addMessageListener(({ action = '', origin }) => { - switch (action) { - case 'init-provider-request': - this._handleProviderRequest(origin) - break - case 'init-is-approved': - this._handleIsApproved(origin) - break - case 'init-is-unlocked': - this._handleIsUnlocked() - break - case 'init-privacy-request': - this._handlePrivacyRequest() - break - } - }) + if (platform && platform.addMessageListener) { + platform.addMessageListener(({ action = '', origin, siteTitle, siteImage }) => { + switch (action) { + case 'init-provider-request': + this._handleProviderRequest(origin, siteTitle, siteImage) + break + case 'init-is-approved': + this._handleIsApproved(origin) + break + case 'init-is-unlocked': + this._handleIsUnlocked() + break + case 'init-privacy-request': + this._handlePrivacyRequest() + break + } + }) + } } /** * Called when a tab requests access to a full Ethereum provider API * * @param {string} origin - Origin of the window requesting full provider access + * @param {string} siteTitle - The title of the document requesting full provider access + * @param {string} siteImage - The icon of the window requesting full provider access */ - _handleProviderRequest (origin) { - this.store.updateState({ providerRequests: [{ origin }] }) + _handleProviderRequest (origin, siteTitle, siteImage) { + this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked if (this.isApproved(origin) && this.caching && isUnlocked) { this.approveProviderRequest(origin) -- cgit From 879997af517b36cf701ec74c08ec4293a2206baa Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 5 Nov 2018 09:03:30 -0500 Subject: Add experimental RPC method support --- app/scripts/contentscript.js | 1 + app/scripts/controllers/provider-approval.js | 8 ++++---- app/scripts/inpage.js | 17 +++++++++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 1cdc85945..2327dc4ac 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -125,6 +125,7 @@ function listenForProviderRequest () { case 'ETHEREUM_ENABLE_PROVIDER': extension.runtime.sendMessage({ action: 'init-provider-request', + force: data.force, origin: source.location.hostname, siteImage: getSiteIcon(source), siteTitle: getSiteName(source), diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index f2d40e67d..f17220cb9 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -25,10 +25,10 @@ class ProviderApprovalController { this.store = new ObservableStore() if (platform && platform.addMessageListener) { - platform.addMessageListener(({ action = '', origin, siteTitle, siteImage }) => { + platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }) => { switch (action) { case 'init-provider-request': - this._handleProviderRequest(origin, siteTitle, siteImage) + this._handleProviderRequest(origin, siteTitle, siteImage, force) break case 'init-is-approved': this._handleIsApproved(origin) @@ -51,10 +51,10 @@ class ProviderApprovalController { * @param {string} siteTitle - The title of the document requesting full provider access * @param {string} siteImage - The icon of the window requesting full provider access */ - _handleProviderRequest (origin, siteTitle, siteImage) { + _handleProviderRequest (origin, siteTitle, siteImage, force) { this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked - if (this.isApproved(origin) && this.caching && isUnlocked) { + if (!force && this.isApproved(origin) && this.caching && isUnlocked) { this.approveProviderRequest(origin) return } diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 2ca3abde2..e1948a522 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -44,7 +44,7 @@ window.addEventListener('metamasksetlocked', () => { }) // augment the provider with its enable method -inpageProvider.enable = function () { +inpageProvider.enable = function ({ force } = {}) { return new Promise((resolve, reject) => { window.removeEventListener('ethereumprovider', providerHandle) providerHandle = ({ detail }) => { @@ -85,10 +85,23 @@ inpageProvider.enable = function () { } } window.addEventListener('ethereumprovider', providerHandle) - window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER' }, '*') + window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*') }) } +// detect eth_requestAccounts and pipe to enable for now +function detectAccountRequest(method) { + const originalMethod = inpageProvider[method] + inpageProvider[method] = function ({ method }) { + if (method === 'eth_requestAccounts') { + return ethereum.enable() + } + return originalMethod.apply(this, arguments) + } +} +detectAccountRequest('send') +detectAccountRequest('sendAsync') + // add metamask-specific convenience methods inpageProvider._metamask = new Proxy({ /** -- cgit From b3f428fd1f2fd5c4041c03d55b38c6de298fc789 Mon Sep 17 00:00:00 2001 From: bitpshr Date: Mon, 5 Nov 2018 09:13:22 -0500 Subject: Move experimental provider augmentation --- app/scripts/inpage.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index e1948a522..327e25042 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -89,19 +89,6 @@ inpageProvider.enable = function ({ force } = {}) { }) } -// detect eth_requestAccounts and pipe to enable for now -function detectAccountRequest(method) { - const originalMethod = inpageProvider[method] - inpageProvider[method] = function ({ method }) { - if (method === 'eth_requestAccounts') { - return ethereum.enable() - } - return originalMethod.apply(this, arguments) - } -} -detectAccountRequest('send') -detectAccountRequest('sendAsync') - // add metamask-specific convenience methods inpageProvider._metamask = new Proxy({ /** @@ -176,6 +163,19 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { window.ethereum = proxiedInpageProvider +// detect eth_requestAccounts and pipe to enable for now +function detectAccountRequest(method) { + const originalMethod = inpageProvider[method] + inpageProvider[method] = function ({ method }) { + if (method === 'eth_requestAccounts') { + return window.ethereum.enable() + } + return originalMethod.apply(this, arguments) + } +} +detectAccountRequest('send') +detectAccountRequest('sendAsync') + // // setup web3 // -- cgit From 34d80f21e2ed4842d196dc580a9441a2731a5c0e Mon Sep 17 00:00:00 2001 From: bitpshr Date: Tue, 6 Nov 2018 14:13:27 -0500 Subject: Clear cached approval after rejection --- app/scripts/controllers/provider-approval.js | 15 +++++++++------ app/scripts/metamask-controller.js | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index f17220cb9..42834f1cd 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -54,7 +54,7 @@ class ProviderApprovalController { _handleProviderRequest (origin, siteTitle, siteImage, force) { this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked - if (!force && this.isApproved(origin) && this.caching && isUnlocked) { + if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) { this.approveProviderRequest(origin) return } @@ -67,9 +67,11 @@ class ProviderApprovalController { * @param {string} origin - Origin of the window */ _handleIsApproved (origin) { - const isApproved = this.isApproved(origin) && this.caching - const caching = this.caching - this.platform && this.platform.sendMessage({ action: 'answer-is-approved', isApproved, caching }, { active: true }) + this.platform && this.platform.sendMessage({ + action: 'answer-is-approved', + isApproved: this.approvedOrigins[origin] && this.caching, + caching: this.caching + }, { active: true }) } /** @@ -117,6 +119,7 @@ class ProviderApprovalController { this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { active: true }) const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) + delete this.approvedOrigins[origin] } /** @@ -127,12 +130,12 @@ class ProviderApprovalController { } /** - * Determines if a given origin has been approved + * Determines if a given origin should have accounts exposed * * @param {string} origin - Domain origin to check for approval status * @returns {boolean} - True if the origin has been approved */ - isApproved (origin) { + shouldExposeAccounts (origin) { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode return !privacyMode || this.approvedOrigins[origin] } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 33278db85..5ae0f608d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -277,8 +277,8 @@ module.exports = class MetamaskController extends EventEmitter { getAccounts: async ({ origin }) => { // Expose no accounts if this origin has not been approved, preventing // account-requring RPC methods from completing successfully - const isApproved = this.providerApprovalController.isApproved(origin) - if (origin !== 'MetaMask' && !isApproved) { return [] } + const exposeAccounts = this.providerApprovalController.shouldExposeAccounts(origin) + if (origin !== 'MetaMask' && !exposeAccounts) { return [] } const isUnlocked = this.keyringController.memStore.getState().isUnlocked const selectedAddress = this.preferencesController.getSelectedAddress() // only show address if account is unlocked -- cgit From d7b1cacabca6abe55ddea54369e5dcac0dc0963b Mon Sep 17 00:00:00 2001 From: bitpshr Date: Tue, 6 Nov 2018 14:26:02 -0500 Subject: Remove injected script tags after use --- app/scripts/contentscript.js | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app/scripts') diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 2327dc4ac..1a10cdb34 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -37,8 +37,10 @@ function injectScript (content) { try { const container = document.head || document.documentElement const scriptTag = document.createElement('script') + scriptTag.setAttribute('async', false) scriptTag.textContent = content container.insertBefore(scriptTag, container.children[0]) + container.removeChild(scriptTag) } catch (e) { console.error('MetaMask script injection failed', e) } -- cgit From 896ae0ab89096ed9a206a64c00c351820ed9ab1b Mon Sep 17 00:00:00 2001 From: bitpshr Date: Tue, 6 Nov 2018 14:30:33 -0500 Subject: Fix lint errors --- app/scripts/controllers/provider-approval.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/scripts') diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 42834f1cd..d3b7f6dff 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -70,7 +70,7 @@ class ProviderApprovalController { this.platform && this.platform.sendMessage({ action: 'answer-is-approved', isApproved: this.approvedOrigins[origin] && this.caching, - caching: this.caching + caching: this.caching, }, { active: true }) } -- cgit