From 2845398c3d824e5da1830ba7905ffdbf8149cf9e Mon Sep 17 00:00:00 2001 From: kumavis Date: Sat, 4 May 2019 01:32:05 +0800 Subject: Refactor ProviderApprovalController to use rpc and publicConfigStore (#6410) * Ensure home screen does not render if there are unapproved txs (#6501) * Ensure that the confirm screen renders before the home screen if there are unapproved txs. * Only render confirm screen before home screen on mount. * inpage - revert _metamask api to isEnabled isApproved isUnlocked --- app/scripts/contentscript.js | 213 +++++++++------------ .../controllers/network/createBlockTracker.js | 19 -- .../controllers/network/createInfuraClient.js | 6 +- .../controllers/network/createJsonRpcClient.js | 6 +- .../controllers/network/createLocalhostClient.js | 6 +- app/scripts/controllers/network/network.js | 9 +- app/scripts/controllers/provider-approval.js | 123 ++++-------- app/scripts/createStandardProvider.js | 29 +-- app/scripts/inpage.js | 122 +++--------- app/scripts/lib/createDnodeRemoteGetter.js | 16 ++ app/scripts/metamask-controller.js | 102 +++++++--- app/scripts/platforms/extension.js | 14 -- package.json | 1 - .../provider-page-container.component.js | 13 +- .../provider-approval.component.js | 10 +- .../provider-approval.container.js | 6 +- ui/app/store/actions.js | 12 +- 17 files changed, 298 insertions(+), 409 deletions(-) delete mode 100644 app/scripts/controllers/network/createBlockTracker.js create mode 100644 app/scripts/lib/createDnodeRemoteGetter.js diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 2325cecdd..0c55ae39f 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -1,18 +1,17 @@ const fs = require('fs') const path = require('path') const pump = require('pump') +const log = require('loglevel') +const Dnode = require('dnode') const querystring = require('querystring') const LocalMessageDuplexStream = require('post-message-stream') -const PongStream = require('ping-pong-stream/pong') const ObjectMultiplex = require('obj-multiplex') const extension = require('extensionizer') const PortStream = require('extension-port-stream') -const {Transform: TransformStream} = require('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 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 @@ -23,9 +22,7 @@ let isEnabled = false if (shouldInjectWeb3()) { injectScript(inpageBundle) - setupStreams() - listenForProviderRequest() - checkPrivacyMode() + start() } /** @@ -46,149 +43,108 @@ function injectScript (content) { } } +/** + * Sets up the stream communication and submits site metadata + * + */ +async function start () { + await setupStreams() + await domIsReady() +} + /** * Sets up two-way communication streams between the - * browser extension and local per-page browser context + * browser extension and local per-page browser context. + * */ -function setupStreams () { - // setup communication to page and plugin +async function setupStreams () { + // the transport-specific streams for communication between inpage and background const pageStream = new LocalMessageDuplexStream({ name: 'contentscript', target: 'inpage', }) - const pluginPort = extension.runtime.connect({ name: 'contentscript' }) - const pluginStream = new PortStream(pluginPort) + const extensionPort = extension.runtime.connect({ name: 'contentscript' }) + const extensionStream = new PortStream(extensionPort) - // 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' && !isEnabled) { - data.data.selectedAddress = undefined - } - done(null, { ...data }) - }, - }) + // create and connect channel muxers + // so we can handle the channels individually + const pageMux = new ObjectMultiplex() + pageMux.setMaxListeners(25) + const extensionMux = new ObjectMultiplex() + extensionMux.setMaxListeners(25) - // forward communication plugin->inpage pump( + pageMux, pageStream, - pluginStream, - approvalTransform, - pageStream, - (err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err) + pageMux, + (err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err) ) - - // setup local multistream channels - const mux = new ObjectMultiplex() - mux.setMaxListeners(25) - pump( - mux, - pageStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask Inpage', err) - ) - pump( - mux, - pluginStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask Background', err) + extensionMux, + extensionStream, + extensionMux, + (err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err) ) - // connect ping stream - const pongStream = new PongStream({ objectMode: true }) - pump( - mux, - pongStream, - mux, - (err) => logStreamDisconnectWarning('MetaMask PingPongStream', err) - ) + // forward communication across inpage-background for these channels only + forwardTrafficBetweenMuxers('provider', pageMux, extensionMux) + forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux) - // connect phishing warning stream - const phishingStream = mux.createStream('phishing') + // connect "phishing" channel to warning system + const phishingStream = extensionMux.createStream('phishing') phishingStream.once('data', redirectToPhishingWarning) - // ignore unused channels (handled by background, inpage) - mux.ignoreStream('provider') - mux.ignoreStream('publicConfig') + // connect "publicApi" channel to submit page metadata + const publicApiStream = extensionMux.createStream('publicApi') + const background = await setupPublicApi(publicApiStream) + + return { background } } -/** - * 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 internally. - */ -function listenForProviderRequest () { - 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', - force: data.force, - origin: source.location.hostname, - siteImage: getSiteIcon(source), - siteTitle: getSiteName(source), - }) - break - case 'ETHEREUM_IS_APPROVED': - extension.runtime.sendMessage({ - action: 'init-is-approved', - origin: source.location.hostname, - }) - break - case 'METAMASK_IS_UNLOCKED': - extension.runtime.sendMessage({ - action: 'init-is-unlocked', - }) - break - } - }) +function forwardTrafficBetweenMuxers (channelName, muxA, muxB) { + const channelA = muxA.createStream(channelName) + const channelB = muxB.createStream(channelName) + pump( + channelA, + channelB, + channelA, + (err) => logStreamDisconnectWarning(`MetaMask muxed traffic for channel "${channelName}" failed.`, err) + ) +} - extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked, selectedAddress }) => { - switch (action) { - case 'approve-provider-request': - isEnabled = true - window.postMessage({ type: 'ethereumprovider', selectedAddress }, '*') - break - case 'approve-legacy-provider-request': - isEnabled = true - window.postMessage({ type: 'ethereumproviderlegacy', selectedAddress }, '*') - break - case 'reject-provider-request': - window.postMessage({ type: 'ethereumprovider', error: 'User denied account authorization' }, '*') - break - case 'answer-is-approved': - window.postMessage({ type: 'ethereumisapproved', isApproved, caching }, '*') - break - case 'answer-is-unlocked': - window.postMessage({ type: 'metamaskisunlocked', isUnlocked }, '*') - break - case 'metamask-set-locked': - isEnabled = false - window.postMessage({ type: 'metamasksetlocked' }, '*') - break - case 'ethereum-ping-success': - window.postMessage({ type: 'ethereumpingsuccess' }, '*') - break - case 'ethereum-ping-error': - window.postMessage({ type: 'ethereumpingerror' }, '*') +async function setupPublicApi (outStream) { + const api = { + getSiteMetadata: (cb) => cb(null, getSiteMetadata()), + } + const dnode = Dnode(api) + pump( + outStream, + dnode, + outStream, + (err) => { + // report any error + if (err) log.error(err) } - }) + ) + const background = await new Promise(resolve => dnode.once('remote', resolve)) + return background } /** - * Checks if MetaMask is currently operating in "privacy mode", meaning - * dapps must call ethereum.enable in order to access user accounts + * Gets site metadata and returns it + * */ -function checkPrivacyMode () { - extension.runtime.sendMessage({ action: 'init-privacy-request' }) +function getSiteMetadata () { + // get metadata + const metadata = { + name: getSiteName(window), + icon: getSiteIcon(window), + } + return metadata } /** - * Error handler for page to plugin stream disconnections + * Error handler for page to extension stream disconnections * * @param {string} remoteLabel Remote stream name * @param {Error} err Stream connection error @@ -301,6 +257,10 @@ function redirectToPhishingWarning () { })}` } + +/** + * Extracts a name for the site from the DOM + */ function getSiteName (window) { const document = window.document const siteName = document.querySelector('head > meta[property="og:site_name"]') @@ -316,6 +276,9 @@ function getSiteName (window) { return document.title } +/** + * Extracts an icon for the site from the DOM + */ function getSiteIcon (window) { const document = window.document @@ -333,3 +296,13 @@ function getSiteIcon (window) { return null } + +/** + * Returns a promise that resolves when the DOM is loaded (does not wait for images to load) + */ +async function domIsReady () { + // already loaded + if (['interactive', 'complete'].includes(document.readyState)) return + // wait for load + await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true })) +} diff --git a/app/scripts/controllers/network/createBlockTracker.js b/app/scripts/controllers/network/createBlockTracker.js deleted file mode 100644 index 6573b18a1..000000000 --- a/app/scripts/controllers/network/createBlockTracker.js +++ /dev/null @@ -1,19 +0,0 @@ -const BlockTracker = require('eth-block-tracker') - -/** - * Creates a block tracker that sends platform events on success and failure - */ -module.exports = function createBlockTracker (args, platform) { - const blockTracker = new BlockTracker(args) - blockTracker.on('latest', () => { - if (platform && platform.sendMessage) { - platform.sendMessage({ action: 'ethereum-ping-success' }) - } - }) - blockTracker.on('error', () => { - if (platform && platform.sendMessage) { - platform.sendMessage({ action: 'ethereum-ping-error' }) - } - }) - return blockTracker -} diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 70b332867..0a6e9ecb0 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -7,14 +7,14 @@ const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const createInfuraMiddleware = require('eth-json-rpc-infura') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createInfuraClient -function createInfuraClient ({ network, platform }) { +function createInfuraClient ({ network }) { const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }) const infuraProvider = providerFromMiddleware(infuraMiddleware) - const blockTracker = createBlockTracker({ provider: infuraProvider }, platform) + const blockTracker = new BlockTracker({ provider: infuraProvider }) const networkMiddleware = mergeMiddleware([ createNetworkAndChainIdMiddleware({ network }), diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index 369dcd299..a8cbf2aaf 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -5,14 +5,14 @@ const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache' const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createJsonRpcClient -function createJsonRpcClient ({ rpcUrl, platform }) { +function createJsonRpcClient ({ rpcUrl }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = createBlockTracker({ provider: blockProvider }, platform) + const blockTracker = new BlockTracker({ provider: blockProvider }) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js index 36593dc70..09b1d3c1c 100644 --- a/app/scripts/controllers/network/createLocalhostClient.js +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -3,14 +3,14 @@ const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const createBlockTracker = require('./createBlockTracker') +const BlockTracker = require('eth-block-tracker') module.exports = createLocalhostClient -function createLocalhostClient ({ platform }) { +function createLocalhostClient () { const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = createBlockTracker({ provider: blockProvider, pollingInterval: 1000 }, platform) + const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index c00ac7e6a..fc8e0df5d 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -46,9 +46,8 @@ const defaultNetworkConfig = { module.exports = class NetworkController extends EventEmitter { - constructor (opts = {}, platform) { + constructor (opts = {}) { super() - this.platform = platform // parse options const providerConfig = opts.provider || defaultProviderConfig @@ -190,7 +189,7 @@ module.exports = class NetworkController extends EventEmitter { _configureInfuraProvider ({ type }) { log.info('NetworkController - configureInfuraProvider', type) - const networkClient = createInfuraClient({ network: type, platform: this.platform }) + const networkClient = createInfuraClient({ network: type }) this._setNetworkClient(networkClient) // setup networkConfig var settings = { @@ -201,13 +200,13 @@ module.exports = class NetworkController extends EventEmitter { _configureLocalhostProvider () { log.info('NetworkController - configureLocalhostProvider') - const networkClient = createLocalhostClient({ platform: this.platform }) + const networkClient = createLocalhostClient() this._setNetworkClient(networkClient) } _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) { log.info('NetworkController - configureStandardProvider', rpcUrl) - const networkClient = createJsonRpcClient({ rpcUrl, platform: this.platform }) + const networkClient = createJsonRpcClient({ rpcUrl }) // hack to add a 'rpc' network with chainId networks.networkList['rpc'] = { chainId: chainId, diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js index 2c9182b52..8206b2f8a 100644 --- a/app/scripts/controllers/provider-approval.js +++ b/app/scripts/controllers/provider-approval.js @@ -1,9 +1,11 @@ const ObservableStore = require('obs-store') +const SafeEventEmitter = require('safe-event-emitter') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') /** * A controller that services user-approved requests for a full Ethereum provider API */ -class ProviderApprovalController { +class ProviderApprovalController extends SafeEventEmitter { /** * Determines if caching is enabled */ @@ -14,38 +16,43 @@ class ProviderApprovalController { * * @param {Object} [config] - Options to configure controller */ - constructor ({ closePopup, keyringController, openPopup, platform, preferencesController, publicConfigStore } = {}) { + constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) { + super() this.approvedOrigins = {} this.closePopup = closePopup this.keyringController = keyringController this.openPopup = openPopup - this.platform = platform this.preferencesController = preferencesController - this.publicConfigStore = publicConfigStore this.store = new ObservableStore({ providerRequests: [], }) + } - if (platform && platform.addMessageListener) { - platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }, { tab }) => { - if (tab && tab.id) { - switch (action) { - case 'init-provider-request': - this._handleProviderRequest(origin, siteTitle, siteImage, force, tab.id) - break - case 'init-is-approved': - this._handleIsApproved(origin, tab.id) - break - case 'init-is-unlocked': - this._handleIsUnlocked(tab.id) - break - case 'init-privacy-request': - this._handlePrivacyRequest(tab.id) - break - } - } - }) - } + /** + * Called when a user approves access to a full Ethereum provider API + * + * @param {object} opts - opts for the middleware contains the origin for the middleware + */ + createMiddleware ({ origin, getSiteMetadata }) { + return createAsyncMiddleware(async (req, res, next) => { + // only handle requestAccounts + if (req.method !== 'eth_requestAccounts') return next() + // if already approved or privacy mode disabled, return early + if (this.shouldExposeAccounts(origin)) { + res.result = [this.preferencesController.getSelectedAddress()] + return + } + // register the provider request + const metadata = await getSiteMetadata(origin) + this._handleProviderRequest(origin, metadata.name, metadata.icon, false, null) + // wait for resolution of request + const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved))) + if (approved) { + res.result = [this.preferencesController.getSelectedAddress()] + } else { + throw new Error('User denied account authorization') + } + }) } /** @@ -59,79 +66,37 @@ class ProviderApprovalController { this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage, tabID }] }) const isUnlocked = this.keyringController.memStore.getState().isUnlocked if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) { - this.approveProviderRequest(tabID) return } this.openPopup && this.openPopup() } - /** - * Called by a tab to determine if an origin has been approved in the past - * - * @param {string} origin - Origin of the window - */ - _handleIsApproved (origin, tabID) { - this.platform && this.platform.sendMessage({ - action: 'answer-is-approved', - isApproved: this.approvedOrigins[origin] && this.caching, - caching: this.caching, - }, { id: tabID }) - } - - /** - * Called by a tab to determine if MetaMask is currently locked or unlocked - */ - _handleIsUnlocked (tabID) { - const isUnlocked = this.keyringController.memStore.getState().isUnlocked - this.platform && this.platform.sendMessage({ action: 'answer-is-unlocked', isUnlocked }, { id: tabID }) - } - - /** - * Called to check privacy mode; if privacy mode is off, this will automatically enable the provider (legacy behavior) - */ - _handlePrivacyRequest (tabID) { - const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - if (!privacyMode) { - this.platform && this.platform.sendMessage({ - action: 'approve-legacy-provider-request', - selectedAddress: this.publicConfigStore.getState().selectedAddress, - }, { id: tabID }) - this.publicConfigStore.emit('update', this.publicConfigStore.getState()) - } - } - /** * Called when a user approves access to a full Ethereum provider API * - * @param {string} tabID - ID of the target window that approved provider access + * @param {string} origin - origin of the domain that had provider access approved */ - approveProviderRequest (tabID) { + approveProviderRequestByOrigin (origin) { this.closePopup && this.closePopup() const requests = this.store.getState().providerRequests - const origin = requests.find(request => request.tabID === tabID).origin - this.platform && this.platform.sendMessage({ - action: 'approve-provider-request', - selectedAddress: this.publicConfigStore.getState().selectedAddress, - }, { id: tabID }) - this.publicConfigStore.emit('update', this.publicConfigStore.getState()) - const providerRequests = requests.filter(request => request.tabID !== tabID) + const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) this.approvedOrigins[origin] = true + this.emit(`resolvedRequest:${origin}`, { approved: true }) } /** * Called when a tab rejects access to a full Ethereum provider API * - * @param {string} tabID - ID of the target window that rejected provider access + * @param {string} origin - origin of the domain that had provider access approved */ - rejectProviderRequest (tabID) { + rejectProviderRequestByOrigin (origin) { this.closePopup && this.closePopup() const requests = this.store.getState().providerRequests - const origin = requests.find(request => request.tabID === tabID).origin - this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { id: tabID }) - const providerRequests = requests.filter(request => request.tabID !== tabID) + const providerRequests = requests.filter(request => request.origin !== origin) this.store.updateState({ providerRequests }) delete this.approvedOrigins[origin] + this.emit(`resolvedRequest:${origin}`, { approved: false }) } /** @@ -149,16 +114,10 @@ class ProviderApprovalController { */ shouldExposeAccounts (origin) { const privacyMode = this.preferencesController.getFeatureFlags().privacyMode - return !privacyMode || this.approvedOrigins[origin] + const result = !privacyMode || Boolean(this.approvedOrigins[origin]) + return result } - /** - * 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/createStandardProvider.js b/app/scripts/createStandardProvider.js index a5f9c5d03..2059b9b3a 100644 --- a/app/scripts/createStandardProvider.js +++ b/app/scripts/createStandardProvider.js @@ -4,18 +4,10 @@ class StandardProvider { constructor (provider) { this._provider = provider - this._onMessage('ethereumpingerror', this._onClose.bind(this)) - this._onMessage('ethereumpingsuccess', this._onConnect.bind(this)) - window.addEventListener('load', () => { - this._subscribe() - this._ping() - }) - } - - _onMessage (type, handler) { - window.addEventListener('message', function ({ data }) { - if (!data || data.type !== type) return - handler.apply(this, arguments) + this._subscribe() + // indicate that we've connected, mostly just for standard compliance + setTimeout(() => { + this._onConnect() }) } @@ -34,15 +26,6 @@ class StandardProvider { this._isConnected = true } - async _ping () { - try { - await this.send('net_version') - window.postMessage({ type: 'ethereumpingsuccess' }, '*') - } catch (error) { - window.postMessage({ type: 'ethereumpingerror' }, '*') - } - } - _subscribe () { this._provider.on('data', (error, { method, params }) => { if (!error && method === 'eth_subscription') { @@ -59,11 +42,9 @@ class StandardProvider { * @returns {Promise<*>} Promise resolving to the result if successful */ send (method, params = []) { - if (method === 'eth_requestAccounts') return this._provider.enable() - return new Promise((resolve, reject) => { try { - this._provider.sendAsync({ method, params, beta: true }, (error, response) => { + this._provider.sendAsync({ id: 1, jsonrpc: '2.0', method, params }, (error, response) => { error = error || response.error error ? reject(error) : resolve(response) }) diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index 71cfb875c..a4fb552f1 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -7,32 +7,12 @@ const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('metamask-inpage-provider') const createStandardProvider = require('./createStandardProvider').default -let isEnabled = false let warned = false -let providerHandle -let isApprovedHandle -let isUnlockedHandle restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') -/** - * Adds a postMessage listener for a specific message type - * - * @param {string} messageType - postMessage type to listen for - * @param {Function} handler - event handler - * @param {boolean} remove - removes this handler after being triggered - */ -function onMessage (messageType, callback, remove) { - const handler = function ({ data }) { - if (!data || data.type !== messageType) { return } - remove && window.removeEventListener('message', handler) - callback.apply(window, arguments) - } - window.addEventListener('message', handler) -} - // // setup plugin communication // @@ -49,45 +29,16 @@ const 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 -onMessage('metamasksetlocked', () => { isEnabled = false }) - -// set up a listener for privacy mode responses -onMessage('ethereumproviderlegacy', ({ data: { selectedAddress } }) => { - isEnabled = true - setTimeout(() => { - inpageProvider.publicConfigStore.updateState({ selectedAddress }) - }, 0) -}, true) - // augment the provider with its enable method inpageProvider.enable = function ({ force } = {}) { return new Promise((resolve, reject) => { - providerHandle = ({ data: { error, selectedAddress } }) => { - if (typeof error !== 'undefined') { - reject({ - message: error, - code: 4001, - }) + inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => { + if (error) { + reject(error) } else { - window.removeEventListener('message', providerHandle) - setTimeout(() => { - inpageProvider.publicConfigStore.updateState({ selectedAddress }) - }, 0) - - // wait for the background to update with an account - inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => { - if (error) { - reject(error) - } else { - isEnabled = true - resolve(response.result) - } - }) + resolve(response.result) } - } - onMessage('ethereumprovider', providerHandle, true) - window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*') + }) }) } @@ -98,31 +49,23 @@ inpageProvider.autoRefreshOnNetworkChange = true // add metamask-specific convenience methods inpageProvider._metamask = new Proxy({ /** - * Determines if this domain is currently enabled + * Synchronously determines if this domain is currently enabled, with a potential false negative if called to soon * - * @returns {boolean} - true if this domain is currently enabled + * @returns {boolean} - returns true if this domain is currently enabled */ isEnabled: function () { - return isEnabled + const { isEnabled } = inpageProvider.publicConfigStore.getState() + return Boolean(isEnabled) }, /** - * Determines if this domain has been previously approved + * Asynchronously determines if this domain is currently enabled * - * @returns {Promise} - Promise resolving to true if this domain has been previously approved + * @returns {Promise} - Promise resolving to true if this domain is currently enabled */ - isApproved: function () { - return new Promise((resolve) => { - isApprovedHandle = ({ data: { caching, isApproved } }) => { - if (caching) { - resolve(!!isApproved) - } else { - resolve(false) - } - } - onMessage('ethereumisapproved', isApprovedHandle, true) - window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*') - }) + isApproved: async function () { + const { isEnabled } = await getPublicConfigWhenReady() + return Boolean(isEnabled) }, /** @@ -130,14 +73,9 @@ inpageProvider._metamask = new Proxy({ * * @returns {Promise} - Promise resolving to true if MetaMask is currently unlocked */ - isUnlocked: function () { - return new Promise((resolve) => { - isUnlockedHandle = ({ data: { isUnlocked } }) => { - resolve(!!isUnlocked) - } - onMessage('metamaskisunlocked', isUnlockedHandle, true) - window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*') - }) + isUnlocked: async function () { + const { isUnlocked } = await getPublicConfigWhenReady() + return Boolean(isUnlocked) }, }, { get: function (obj, prop) { @@ -149,6 +87,19 @@ inpageProvider._metamask = new Proxy({ }, }) +// publicConfig isn't populated until we get a message from background. +// Using this getter will ensure the state is available +async function getPublicConfigWhenReady () { + const store = inpageProvider.publicConfigStore + let state = store.getState() + // if state is missing, wait for first update + if (!state.networkVersion) { + state = await new Promise(resolve => store.once('update', resolve)) + console.log('new state', state) + } + return state +} + // 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, { @@ -159,19 +110,6 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { window.ethereum = createStandardProvider(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 // diff --git a/app/scripts/lib/createDnodeRemoteGetter.js b/app/scripts/lib/createDnodeRemoteGetter.js new file mode 100644 index 000000000..b70d218f3 --- /dev/null +++ b/app/scripts/lib/createDnodeRemoteGetter.js @@ -0,0 +1,16 @@ +module.exports = createDnodeRemoteGetter + +function createDnodeRemoteGetter (dnode) { + let remote + + dnode.once('remote', (_remote) => { + remote = _remote + }) + + async function getRemote () { + if (remote) return remote + return await new Promise(resolve => dnode.once('remote', resolve)) + } + + return getRemote +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index be2090f63..7d666ae88 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -7,8 +7,10 @@ const EventEmitter = require('events') const pump = require('pump') const Dnode = require('dnode') +const pify = require('pify') const ObservableStore = require('obs-store') const ComposableObservableStore = require('./lib/ComposableObservableStore') +const createDnodeRemoteGetter = require('./lib/createDnodeRemoteGetter') const asStream = require('obs-store/lib/asStream') const AccountTracker = require('./lib/account-tracker') const RpcEngine = require('json-rpc-engine') @@ -87,7 +89,7 @@ module.exports = class MetamaskController extends EventEmitter { this.createVaultMutex = new Mutex() // network store - this.networkController = new NetworkController(initState.NetworkController, this.platform) + this.networkController = new NetworkController(initState.NetworkController) // preferences controller this.preferencesController = new PreferencesController({ @@ -235,15 +237,17 @@ module.exports = class MetamaskController extends EventEmitter { this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) - this.publicConfigStore = this.initPublicConfigStore() + + // ensure isClientOpenAndUnlocked is updated when memState updates + this.on('update', (memState) => { + this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen + }) this.providerApprovalController = new ProviderApprovalController({ closePopup: opts.closePopup, keyringController: this.keyringController, openPopup: opts.openPopup, - platform: opts.platform, preferencesController: this.preferencesController, - publicConfigStore: this.publicConfigStore, }) this.store.updateStructure({ @@ -322,22 +326,32 @@ module.exports = class MetamaskController extends EventEmitter { * Constructor helper: initialize a public config store. * This store is used to make some config info available to Dapps synchronously. */ - initPublicConfigStore () { - // get init state + createPublicConfigStore ({ checkIsEnabled }) { + // subset of state for metamask inpage provider const publicConfigStore = new ObservableStore() - // memStore -> transform -> publicConfigStore - this.on('update', (memState) => { - this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen + // setup memStore subscription hooks + this.on('update', updatePublicConfigStore) + updatePublicConfigStore(this.getState()) + + publicConfigStore.destroy = () => { + this.removeEventListener('update', updatePublicConfigStore) + } + + function updatePublicConfigStore (memState) { const publicState = selectPublicState(memState) publicConfigStore.putState(publicState) - }) + } - function selectPublicState (memState) { + function selectPublicState ({ isUnlocked, selectedAddress, network, completedOnboarding }) { + const isEnabled = checkIsEnabled() + const isReady = isUnlocked && isEnabled const result = { - selectedAddress: memState.isUnlocked ? memState.selectedAddress : undefined, - networkVersion: memState.network, - onboardingcomplete: memState.completedOnboarding, + isUnlocked, + isEnabled, + selectedAddress: isReady ? selectedAddress : undefined, + networkVersion: network, + onboardingcomplete: completedOnboarding, } return result } @@ -477,9 +491,10 @@ module.exports = class MetamaskController extends EventEmitter { signTypedMessage: nodeify(this.signTypedMessage, this), cancelTypedMessage: this.cancelTypedMessage.bind(this), - approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController), + // provider approval + approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController), + rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController), - rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController), } } @@ -1296,8 +1311,9 @@ module.exports = class MetamaskController extends EventEmitter { // setup multiplexing const mux = setupMultiplex(connectionStream) // connect features - this.setupProviderConnection(mux.createStream('provider'), originDomain) - this.setupPublicConfig(mux.createStream('publicConfig')) + const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain) + this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi) + this.setupPublicConfig(mux.createStream('publicConfig'), originDomain) } /** @@ -1370,7 +1386,7 @@ module.exports = class MetamaskController extends EventEmitter { * @param {*} outStream - The stream to provide over. * @param {string} origin - The URI of the requesting resource. */ - setupProviderConnection (outStream, origin) { + setupProviderConnection (outStream, origin, publicApi) { // setup json rpc engine stack const engine = new RpcEngine() const provider = this.provider @@ -1390,6 +1406,11 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(subscriptionManager.middleware) // watch asset engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) + // requestAccounts + engine.push(this.providerApprovalController.createMiddleware({ + origin, + getSiteMetadata: publicApi && publicApi.getSiteMetadata, + })) // forward to metamask primary provider engine.push(providerAsMiddleware(provider)) @@ -1418,18 +1439,56 @@ module.exports = class MetamaskController extends EventEmitter { * * @param {*} outStream - The stream to provide public config over. */ - setupPublicConfig (outStream) { - const configStream = asStream(this.publicConfigStore) + setupPublicConfig (outStream, originDomain) { + const configStore = this.createPublicConfigStore({ + // check the providerApprovalController's approvedOrigins + checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(originDomain), + }) + const configStream = asStream(configStore) + pump( configStream, outStream, (err) => { + configStore.destroy() configStream.destroy() if (err) log.error(err) } ) } + /** + * A method for providing our public api over a stream. + * This includes a method for setting site metadata like title and image + * + * @param {*} outStream - The stream to provide the api over. + */ + setupPublicApi (outStream, originDomain) { + const dnode = Dnode() + // connect dnode api to remote connection + pump( + outStream, + dnode, + outStream, + (err) => { + // report any error + if (err) log.error(err) + } + ) + + const getRemote = createDnodeRemoteGetter(dnode) + + const publicApi = { + // wrap with an await remote + getSiteMetadata: async () => { + const remote = await getRemote() + return await pify(remote.getSiteMetadata)() + }, + } + + return publicApi + } + /** * Handle a KeyringController update * @param {object} state the KC state @@ -1734,7 +1793,6 @@ module.exports = class MetamaskController extends EventEmitter { * Locks MetaMask */ setLocked () { - this.providerApprovalController.setLocked() return this.keyringController.setLocked() } } diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js index 099b0d7ea..0c2d222b8 100644 --- a/app/scripts/platforms/extension.js +++ b/app/scripts/platforms/extension.js @@ -60,20 +60,6 @@ class ExtensionPlatform { } } - addMessageListener (cb) { - extension.runtime.onMessage.addListener(cb) - } - - sendMessage (message, query = {}) { - const id = query.id - delete query.id - extension.tabs.query({ ...query }, tabs => { - tabs.forEach(tab => { - extension.tabs.sendMessage(id || tab.id, message) - }) - }) - } - _showConfirmedTransaction (txMeta) { this._subscribeToNotificationClicked() diff --git a/package.json b/package.json index 2a02954d2..7f885512d 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,6 @@ "obs-store": "^3.0.2", "percentile": "^1.2.0", "pify": "^3.0.0", - "ping-pong-stream": "^1.0.0", "pojo-migrator": "^2.1.0", "polyfill-crypto.getrandomvalues": "^1.0.0", "post-message-stream": "^3.0.0", diff --git a/ui/app/components/app/provider-page-container/provider-page-container.component.js b/ui/app/components/app/provider-page-container/provider-page-container.component.js index 910def2a3..1c655d404 100644 --- a/ui/app/components/app/provider-page-container/provider-page-container.component.js +++ b/ui/app/components/app/provider-page-container/provider-page-container.component.js @@ -5,12 +5,11 @@ import { PageContainerFooter } from '../../ui/page-container' export default class ProviderPageContainer extends PureComponent { static propTypes = { - approveProviderRequest: PropTypes.func.isRequired, + approveProviderRequestByOrigin: PropTypes.func.isRequired, + rejectProviderRequestByOrigin: PropTypes.func.isRequired, origin: PropTypes.string.isRequired, - rejectProviderRequest: PropTypes.func.isRequired, siteImage: PropTypes.string, siteTitle: PropTypes.string.isRequired, - tabID: PropTypes.string.isRequired, }; static contextTypes = { @@ -29,7 +28,7 @@ export default class ProviderPageContainer extends PureComponent { } onCancel = () => { - const { tabID, rejectProviderRequest } = this.props + const { origin, rejectProviderRequestByOrigin } = this.props this.context.metricsEvent({ eventOpts: { category: 'Auth', @@ -37,11 +36,11 @@ export default class ProviderPageContainer extends PureComponent { name: 'Canceled', }, }) - rejectProviderRequest(tabID) + rejectProviderRequestByOrigin(origin) } onSubmit = () => { - const { approveProviderRequest, tabID } = this.props + const { approveProviderRequestByOrigin, origin } = this.props this.context.metricsEvent({ eventOpts: { category: 'Auth', @@ -49,7 +48,7 @@ export default class ProviderPageContainer extends PureComponent { name: 'Confirmed', }, }) - approveProviderRequest(tabID) + approveProviderRequestByOrigin(origin) } render () { diff --git a/ui/app/pages/provider-approval/provider-approval.component.js b/ui/app/pages/provider-approval/provider-approval.component.js index 1f1d68da7..70d3d0007 100644 --- a/ui/app/pages/provider-approval/provider-approval.component.js +++ b/ui/app/pages/provider-approval/provider-approval.component.js @@ -4,9 +4,9 @@ import ProviderPageContainer from '../../components/app/provider-page-container' export default class ProviderApproval extends Component { static propTypes = { - approveProviderRequest: PropTypes.func.isRequired, + approveProviderRequestByOrigin: PropTypes.func.isRequired, + rejectProviderRequestByOrigin: PropTypes.func.isRequired, providerRequest: PropTypes.object.isRequired, - rejectProviderRequest: PropTypes.func.isRequired, }; static contextTypes = { @@ -14,13 +14,13 @@ export default class ProviderApproval extends Component { }; render () { - const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props + const { approveProviderRequestByOrigin, providerRequest, rejectProviderRequestByOrigin } = this.props return ( diff --git a/ui/app/pages/provider-approval/provider-approval.container.js b/ui/app/pages/provider-approval/provider-approval.container.js index d53c0ae4d..1e167ddb7 100644 --- a/ui/app/pages/provider-approval/provider-approval.container.js +++ b/ui/app/pages/provider-approval/provider-approval.container.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux' import ProviderApproval from './provider-approval.component' -import { approveProviderRequest, rejectProviderRequest } from '../../store/actions' +import { approveProviderRequestByOrigin, rejectProviderRequestByOrigin } from '../../store/actions' function mapDispatchToProps (dispatch) { return { - approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)), - rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)), + approveProviderRequestByOrigin: origin => dispatch(approveProviderRequestByOrigin(origin)), + rejectProviderRequestByOrigin: origin => dispatch(rejectProviderRequestByOrigin(origin)), } } diff --git a/ui/app/store/actions.js b/ui/app/store/actions.js index f594d9002..e7b855119 100644 --- a/ui/app/store/actions.js +++ b/ui/app/store/actions.js @@ -343,8 +343,8 @@ var actions = { createCancelTransaction, createSpeedUpTransaction, - approveProviderRequest, - rejectProviderRequest, + approveProviderRequestByOrigin, + rejectProviderRequestByOrigin, clearApprovedOrigins, setFirstTimeFlowType, @@ -2680,15 +2680,15 @@ function setPendingTokens (pendingTokens) { } } -function approveProviderRequest (tabID) { +function approveProviderRequestByOrigin (origin) { return (dispatch) => { - background.approveProviderRequest(tabID) + background.approveProviderRequestByOrigin(origin) } } -function rejectProviderRequest (tabID) { +function rejectProviderRequestByOrigin (origin) { return (dispatch) => { - background.rejectProviderRequest(tabID) + background.rejectProviderRequestByOrigin(origin) } } -- cgit v1.2.3