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
feature/default_network_editable
kumavis 6 years ago committed by Frankie
parent 2ff522604b
commit 2845398c3d
  1. 213
      app/scripts/contentscript.js
  2. 19
      app/scripts/controllers/network/createBlockTracker.js
  3. 6
      app/scripts/controllers/network/createInfuraClient.js
  4. 6
      app/scripts/controllers/network/createJsonRpcClient.js
  5. 6
      app/scripts/controllers/network/createLocalhostClient.js
  6. 9
      app/scripts/controllers/network/network.js
  7. 123
      app/scripts/controllers/provider-approval.js
  8. 29
      app/scripts/createStandardProvider.js
  9. 122
      app/scripts/inpage.js
  10. 16
      app/scripts/lib/createDnodeRemoteGetter.js
  11. 102
      app/scripts/metamask-controller.js
  12. 14
      app/scripts/platforms/extension.js
  13. 1
      package.json
  14. 13
      ui/app/components/app/provider-page-container/provider-page-container.component.js
  15. 10
      ui/app/pages/provider-approval/provider-approval.component.js
  16. 6
      ui/app/pages/provider-approval/provider-approval.container.js
  17. 12
      ui/app/store/actions.js

@ -1,18 +1,17 @@
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const pump = require('pump') const pump = require('pump')
const log = require('loglevel')
const Dnode = require('dnode')
const querystring = require('querystring') const querystring = require('querystring')
const LocalMessageDuplexStream = require('post-message-stream') const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
const ObjectMultiplex = require('obj-multiplex') const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer') const extension = require('extensionizer')
const PortStream = require('extension-port-stream') const PortStream = require('extension-port-stream')
const {Transform: TransformStream} = require('stream')
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString() const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString()
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n' const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n'
const inpageBundle = inpageContent + inpageSuffix const inpageBundle = inpageContent + inpageSuffix
let isEnabled = false
// Eventually this streaming injection could be replaced with: // Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
@ -23,9 +22,7 @@ let isEnabled = false
if (shouldInjectWeb3()) { if (shouldInjectWeb3()) {
injectScript(inpageBundle) injectScript(inpageBundle)
setupStreams() start()
listenForProviderRequest()
checkPrivacyMode()
} }
/** /**
@ -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 * 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 () { async function setupStreams () {
// setup communication to page and plugin // the transport-specific streams for communication between inpage and background
const pageStream = new LocalMessageDuplexStream({ const pageStream = new LocalMessageDuplexStream({
name: 'contentscript', name: 'contentscript',
target: 'inpage', target: 'inpage',
}) })
const pluginPort = extension.runtime.connect({ name: 'contentscript' }) const extensionPort = extension.runtime.connect({ name: 'contentscript' })
const pluginStream = new PortStream(pluginPort) const extensionStream = new PortStream(extensionPort)
// Filter out selectedAddress until this origin is enabled // create and connect channel muxers
const approvalTransform = new TransformStream({ // so we can handle the channels individually
objectMode: true, const pageMux = new ObjectMultiplex()
transform: (data, _, done) => { pageMux.setMaxListeners(25)
if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !isEnabled) { const extensionMux = new ObjectMultiplex()
data.data.selectedAddress = undefined extensionMux.setMaxListeners(25)
}
done(null, { ...data })
},
})
// forward communication plugin->inpage
pump( pump(
pageMux,
pageStream, pageStream,
pluginStream, pageMux,
approvalTransform, (err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err)
pageStream,
(err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err)
) )
// setup local multistream channels
const mux = new ObjectMultiplex()
mux.setMaxListeners(25)
pump( pump(
mux, extensionMux,
pageStream, extensionStream,
mux, extensionMux,
(err) => logStreamDisconnectWarning('MetaMask Inpage', err) (err) => logStreamDisconnectWarning('MetaMask Background Multiplex', err)
)
pump(
mux,
pluginStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask Background', err)
) )
// connect ping stream // forward communication across inpage-background for these channels only
const pongStream = new PongStream({ objectMode: true }) forwardTrafficBetweenMuxers('provider', pageMux, extensionMux)
pump( forwardTrafficBetweenMuxers('publicConfig', pageMux, extensionMux)
mux,
pongStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask PingPongStream', err)
)
// connect phishing warning stream // connect "phishing" channel to warning system
const phishingStream = mux.createStream('phishing') const phishingStream = extensionMux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning) phishingStream.once('data', redirectToPhishingWarning)
// ignore unused channels (handled by background, inpage) // connect "publicApi" channel to submit page metadata
mux.ignoreStream('provider') const publicApiStream = extensionMux.createStream('publicApi')
mux.ignoreStream('publicConfig') const background = await setupPublicApi(publicApiStream)
return { background }
} }
/** function forwardTrafficBetweenMuxers (channelName, muxA, muxB) {
* Establishes listeners for requests to fully-enable the provider from the dapp context const channelA = muxA.createStream(channelName)
* and for full-provider approvals and rejections from the background script context. Dapps const channelB = muxB.createStream(channelName)
* should not post messages directly and should instead call provider.enable(), which pump(
* handles posting these messages internally. channelA,
*/ channelB,
function listenForProviderRequest () { channelA,
window.addEventListener('message', ({ source, data }) => { (err) => logStreamDisconnectWarning(`MetaMask muxed traffic for channel "${channelName}" failed.`, err)
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
}
})
extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked, selectedAddress }) => { async function setupPublicApi (outStream) {
switch (action) { const api = {
case 'approve-provider-request': getSiteMetadata: (cb) => cb(null, getSiteMetadata()),
isEnabled = true }
window.postMessage({ type: 'ethereumprovider', selectedAddress }, '*') const dnode = Dnode(api)
break pump(
case 'approve-legacy-provider-request': outStream,
isEnabled = true dnode,
window.postMessage({ type: 'ethereumproviderlegacy', selectedAddress }, '*') outStream,
break (err) => {
case 'reject-provider-request': // report any error
window.postMessage({ type: 'ethereumprovider', error: 'User denied account authorization' }, '*') if (err) log.error(err)
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' }, '*')
} }
}) )
const background = await new Promise(resolve => dnode.once('remote', resolve))
return background
} }
/** /**
* Checks if MetaMask is currently operating in "privacy mode", meaning * Gets site metadata and returns it
* dapps must call ethereum.enable in order to access user accounts *
*/ */
function checkPrivacyMode () { function getSiteMetadata () {
extension.runtime.sendMessage({ action: 'init-privacy-request' }) // 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 {string} remoteLabel Remote stream name
* @param {Error} err Stream connection error * @param {Error} err Stream connection error
@ -301,6 +257,10 @@ function redirectToPhishingWarning () {
})}` })}`
} }
/**
* Extracts a name for the site from the DOM
*/
function getSiteName (window) { function getSiteName (window) {
const document = window.document const document = window.document
const siteName = document.querySelector('head > meta[property="og:site_name"]') const siteName = document.querySelector('head > meta[property="og:site_name"]')
@ -316,6 +276,9 @@ function getSiteName (window) {
return document.title return document.title
} }
/**
* Extracts an icon for the site from the DOM
*/
function getSiteIcon (window) { function getSiteIcon (window) {
const document = window.document const document = window.document
@ -333,3 +296,13 @@ function getSiteIcon (window) {
return null 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 }))
}

@ -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
}

@ -7,14 +7,14 @@ const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
const createInfuraMiddleware = require('eth-json-rpc-infura') const createInfuraMiddleware = require('eth-json-rpc-infura')
const createBlockTracker = require('./createBlockTracker') const BlockTracker = require('eth-block-tracker')
module.exports = createInfuraClient module.exports = createInfuraClient
function createInfuraClient ({ network, platform }) { function createInfuraClient ({ network }) {
const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }) const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' })
const infuraProvider = providerFromMiddleware(infuraMiddleware) const infuraProvider = providerFromMiddleware(infuraMiddleware)
const blockTracker = createBlockTracker({ provider: infuraProvider }, platform) const blockTracker = new BlockTracker({ provider: infuraProvider })
const networkMiddleware = mergeMiddleware([ const networkMiddleware = mergeMiddleware([
createNetworkAndChainIdMiddleware({ network }), createNetworkAndChainIdMiddleware({ network }),

@ -5,14 +5,14 @@ const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache'
const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache')
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
const createBlockTracker = require('./createBlockTracker') const BlockTracker = require('eth-block-tracker')
module.exports = createJsonRpcClient module.exports = createJsonRpcClient
function createJsonRpcClient ({ rpcUrl, platform }) { function createJsonRpcClient ({ rpcUrl }) {
const fetchMiddleware = createFetchMiddleware({ rpcUrl }) const fetchMiddleware = createFetchMiddleware({ rpcUrl })
const blockProvider = providerFromMiddleware(fetchMiddleware) const blockProvider = providerFromMiddleware(fetchMiddleware)
const blockTracker = createBlockTracker({ provider: blockProvider }, platform) const blockTracker = new BlockTracker({ provider: blockProvider })
const networkMiddleware = mergeMiddleware([ const networkMiddleware = mergeMiddleware([
createBlockRefRewriteMiddleware({ blockTracker }), createBlockRefRewriteMiddleware({ blockTracker }),

@ -3,14 +3,14 @@ const createFetchMiddleware = require('eth-json-rpc-middleware/fetch')
const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite')
const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
const createBlockTracker = require('./createBlockTracker') const BlockTracker = require('eth-block-tracker')
module.exports = createLocalhostClient module.exports = createLocalhostClient
function createLocalhostClient ({ platform }) { function createLocalhostClient () {
const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' })
const blockProvider = providerFromMiddleware(fetchMiddleware) const blockProvider = providerFromMiddleware(fetchMiddleware)
const blockTracker = createBlockTracker({ provider: blockProvider, pollingInterval: 1000 }, platform) const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 })
const networkMiddleware = mergeMiddleware([ const networkMiddleware = mergeMiddleware([
createBlockRefRewriteMiddleware({ blockTracker }), createBlockRefRewriteMiddleware({ blockTracker }),

@ -46,9 +46,8 @@ const defaultNetworkConfig = {
module.exports = class NetworkController extends EventEmitter { module.exports = class NetworkController extends EventEmitter {
constructor (opts = {}, platform) { constructor (opts = {}) {
super() super()
this.platform = platform
// parse options // parse options
const providerConfig = opts.provider || defaultProviderConfig const providerConfig = opts.provider || defaultProviderConfig
@ -190,7 +189,7 @@ module.exports = class NetworkController extends EventEmitter {
_configureInfuraProvider ({ type }) { _configureInfuraProvider ({ type }) {
log.info('NetworkController - configureInfuraProvider', type) log.info('NetworkController - configureInfuraProvider', type)
const networkClient = createInfuraClient({ network: type, platform: this.platform }) const networkClient = createInfuraClient({ network: type })
this._setNetworkClient(networkClient) this._setNetworkClient(networkClient)
// setup networkConfig // setup networkConfig
var settings = { var settings = {
@ -201,13 +200,13 @@ module.exports = class NetworkController extends EventEmitter {
_configureLocalhostProvider () { _configureLocalhostProvider () {
log.info('NetworkController - configureLocalhostProvider') log.info('NetworkController - configureLocalhostProvider')
const networkClient = createLocalhostClient({ platform: this.platform }) const networkClient = createLocalhostClient()
this._setNetworkClient(networkClient) this._setNetworkClient(networkClient)
} }
_configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) { _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) {
log.info('NetworkController - configureStandardProvider', rpcUrl) log.info('NetworkController - configureStandardProvider', rpcUrl)
const networkClient = createJsonRpcClient({ rpcUrl, platform: this.platform }) const networkClient = createJsonRpcClient({ rpcUrl })
// hack to add a 'rpc' network with chainId // hack to add a 'rpc' network with chainId
networks.networkList['rpc'] = { networks.networkList['rpc'] = {
chainId: chainId, chainId: chainId,

@ -1,9 +1,11 @@
const ObservableStore = require('obs-store') 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 * A controller that services user-approved requests for a full Ethereum provider API
*/ */
class ProviderApprovalController { class ProviderApprovalController extends SafeEventEmitter {
/** /**
* Determines if caching is enabled * Determines if caching is enabled
*/ */
@ -14,38 +16,43 @@ class ProviderApprovalController {
* *
* @param {Object} [config] - Options to configure controller * @param {Object} [config] - Options to configure controller
*/ */
constructor ({ closePopup, keyringController, openPopup, platform, preferencesController, publicConfigStore } = {}) { constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) {
super()
this.approvedOrigins = {} this.approvedOrigins = {}
this.closePopup = closePopup this.closePopup = closePopup
this.keyringController = keyringController this.keyringController = keyringController
this.openPopup = openPopup this.openPopup = openPopup
this.platform = platform
this.preferencesController = preferencesController this.preferencesController = preferencesController
this.publicConfigStore = publicConfigStore
this.store = new ObservableStore({ this.store = new ObservableStore({
providerRequests: [], providerRequests: [],
}) })
}
if (platform && platform.addMessageListener) { /**
platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }, { tab }) => { * Called when a user approves access to a full Ethereum provider API
if (tab && tab.id) { *
switch (action) { * @param {object} opts - opts for the middleware contains the origin for the middleware
case 'init-provider-request': */
this._handleProviderRequest(origin, siteTitle, siteImage, force, tab.id) createMiddleware ({ origin, getSiteMetadata }) {
break return createAsyncMiddleware(async (req, res, next) => {
case 'init-is-approved': // only handle requestAccounts
this._handleIsApproved(origin, tab.id) if (req.method !== 'eth_requestAccounts') return next()
break // if already approved or privacy mode disabled, return early
case 'init-is-unlocked': if (this.shouldExposeAccounts(origin)) {
this._handleIsUnlocked(tab.id) res.result = [this.preferencesController.getSelectedAddress()]
break return
case 'init-privacy-request': }
this._handlePrivacyRequest(tab.id) // register the provider request
break 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 }] }) this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage, tabID }] })
const isUnlocked = this.keyringController.memStore.getState().isUnlocked const isUnlocked = this.keyringController.memStore.getState().isUnlocked
if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) { if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) {
this.approveProviderRequest(tabID)
return return
} }
this.openPopup && this.openPopup() 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 * 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() this.closePopup && this.closePopup()
const requests = this.store.getState().providerRequests const requests = this.store.getState().providerRequests
const origin = requests.find(request => request.tabID === tabID).origin const providerRequests = requests.filter(request => request.origin !== 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)
this.store.updateState({ providerRequests }) this.store.updateState({ providerRequests })
this.approvedOrigins[origin] = true this.approvedOrigins[origin] = true
this.emit(`resolvedRequest:${origin}`, { approved: true })
} }
/** /**
* Called when a tab rejects access to a full Ethereum provider API * 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() this.closePopup && this.closePopup()
const requests = this.store.getState().providerRequests const requests = this.store.getState().providerRequests
const origin = requests.find(request => request.tabID === tabID).origin const providerRequests = requests.filter(request => request.origin !== origin)
this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { id: tabID })
const providerRequests = requests.filter(request => request.tabID !== tabID)
this.store.updateState({ providerRequests }) this.store.updateState({ providerRequests })
delete this.approvedOrigins[origin] delete this.approvedOrigins[origin]
this.emit(`resolvedRequest:${origin}`, { approved: false })
} }
/** /**
@ -149,16 +114,10 @@ class ProviderApprovalController {
*/ */
shouldExposeAccounts (origin) { shouldExposeAccounts (origin) {
const privacyMode = this.preferencesController.getFeatureFlags().privacyMode 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 module.exports = ProviderApprovalController

@ -4,18 +4,10 @@ class StandardProvider {
constructor (provider) { constructor (provider) {
this._provider = provider this._provider = provider
this._onMessage('ethereumpingerror', this._onClose.bind(this)) this._subscribe()
this._onMessage('ethereumpingsuccess', this._onConnect.bind(this)) // indicate that we've connected, mostly just for standard compliance
window.addEventListener('load', () => { setTimeout(() => {
this._subscribe() this._onConnect()
this._ping()
})
}
_onMessage (type, handler) {
window.addEventListener('message', function ({ data }) {
if (!data || data.type !== type) return
handler.apply(this, arguments)
}) })
} }
@ -34,15 +26,6 @@ class StandardProvider {
this._isConnected = true this._isConnected = true
} }
async _ping () {
try {
await this.send('net_version')
window.postMessage({ type: 'ethereumpingsuccess' }, '*')
} catch (error) {
window.postMessage({ type: 'ethereumpingerror' }, '*')
}
}
_subscribe () { _subscribe () {
this._provider.on('data', (error, { method, params }) => { this._provider.on('data', (error, { method, params }) => {
if (!error && method === 'eth_subscription') { if (!error && method === 'eth_subscription') {
@ -59,11 +42,9 @@ class StandardProvider {
* @returns {Promise<*>} Promise resolving to the result if successful * @returns {Promise<*>} Promise resolving to the result if successful
*/ */
send (method, params = []) { send (method, params = []) {
if (method === 'eth_requestAccounts') return this._provider.enable()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { 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 = error || response.error
error ? reject(error) : resolve(response) error ? reject(error) : resolve(response)
}) })

@ -7,32 +7,12 @@ const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('metamask-inpage-provider') const MetamaskInpageProvider = require('metamask-inpage-provider')
const createStandardProvider = require('./createStandardProvider').default const createStandardProvider = require('./createStandardProvider').default
let isEnabled = false
let warned = false let warned = false
let providerHandle
let isApprovedHandle
let isUnlockedHandle
restoreContextAfterImports() restoreContextAfterImports()
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') 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 // setup plugin communication
// //
@ -49,45 +29,16 @@ const inpageProvider = new MetamaskInpageProvider(metamaskStream)
// set a high max listener count to avoid unnecesary warnings // set a high max listener count to avoid unnecesary warnings
inpageProvider.setMaxListeners(100) 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 // augment the provider with its enable method
inpageProvider.enable = function ({ force } = {}) { inpageProvider.enable = function ({ force } = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
providerHandle = ({ data: { error, selectedAddress } }) => { inpageProvider.sendAsync({ method: 'eth_requestAccounts', params: [force] }, (error, response) => {
if (typeof error !== 'undefined') { if (error) {
reject({ reject(error)
message: error,
code: 4001,
})
} else { } else {
window.removeEventListener('message', providerHandle) resolve(response.result)
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)
}
})
} }
} })
onMessage('ethereumprovider', providerHandle, true)
window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*')
}) })
} }
@ -98,31 +49,23 @@ inpageProvider.autoRefreshOnNetworkChange = true
// add metamask-specific convenience methods // add metamask-specific convenience methods
inpageProvider._metamask = new Proxy({ 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 () { 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<boolean>} - Promise resolving to true if this domain has been previously approved * @returns {Promise<boolean>} - Promise resolving to true if this domain is currently enabled
*/ */
isApproved: function () { isApproved: async function () {
return new Promise((resolve) => { const { isEnabled } = await getPublicConfigWhenReady()
isApprovedHandle = ({ data: { caching, isApproved } }) => { return Boolean(isEnabled)
if (caching) {
resolve(!!isApproved)
} else {
resolve(false)
}
}
onMessage('ethereumisapproved', isApprovedHandle, true)
window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*')
})
}, },
/** /**
@ -130,14 +73,9 @@ inpageProvider._metamask = new Proxy({
* *
* @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked * @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked
*/ */
isUnlocked: function () { isUnlocked: async function () {
return new Promise((resolve) => { const { isUnlocked } = await getPublicConfigWhenReady()
isUnlockedHandle = ({ data: { isUnlocked } }) => { return Boolean(isUnlocked)
resolve(!!isUnlocked)
}
onMessage('metamaskisunlocked', isUnlockedHandle, true)
window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*')
})
}, },
}, { }, {
get: function (obj, prop) { 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 // 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 // `sendAsync` method on the prototype, causing `this` reference issues with drizzle
const proxiedInpageProvider = new Proxy(inpageProvider, { const proxiedInpageProvider = new Proxy(inpageProvider, {
@ -159,19 +110,6 @@ const proxiedInpageProvider = new Proxy(inpageProvider, {
window.ethereum = createStandardProvider(proxiedInpageProvider) 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 // setup web3
// //

@ -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
}

@ -7,8 +7,10 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const pump = require('pump') const pump = require('pump')
const Dnode = require('dnode') const Dnode = require('dnode')
const pify = require('pify')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ComposableObservableStore = require('./lib/ComposableObservableStore') const ComposableObservableStore = require('./lib/ComposableObservableStore')
const createDnodeRemoteGetter = require('./lib/createDnodeRemoteGetter')
const asStream = require('obs-store/lib/asStream') const asStream = require('obs-store/lib/asStream')
const AccountTracker = require('./lib/account-tracker') const AccountTracker = require('./lib/account-tracker')
const RpcEngine = require('json-rpc-engine') const RpcEngine = require('json-rpc-engine')
@ -87,7 +89,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.createVaultMutex = new Mutex() this.createVaultMutex = new Mutex()
// network store // network store
this.networkController = new NetworkController(initState.NetworkController, this.platform) this.networkController = new NetworkController(initState.NetworkController)
// preferences controller // preferences controller
this.preferencesController = new PreferencesController({ this.preferencesController = new PreferencesController({
@ -235,15 +237,17 @@ module.exports = class MetamaskController extends EventEmitter {
this.messageManager = new MessageManager() this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager() this.personalMessageManager = new PersonalMessageManager()
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController }) 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({ this.providerApprovalController = new ProviderApprovalController({
closePopup: opts.closePopup, closePopup: opts.closePopup,
keyringController: this.keyringController, keyringController: this.keyringController,
openPopup: opts.openPopup, openPopup: opts.openPopup,
platform: opts.platform,
preferencesController: this.preferencesController, preferencesController: this.preferencesController,
publicConfigStore: this.publicConfigStore,
}) })
this.store.updateStructure({ this.store.updateStructure({
@ -322,22 +326,32 @@ module.exports = class MetamaskController extends EventEmitter {
* Constructor helper: initialize a public config store. * Constructor helper: initialize a public config store.
* This store is used to make some config info available to Dapps synchronously. * This store is used to make some config info available to Dapps synchronously.
*/ */
initPublicConfigStore () { createPublicConfigStore ({ checkIsEnabled }) {
// get init state // subset of state for metamask inpage provider
const publicConfigStore = new ObservableStore() const publicConfigStore = new ObservableStore()
// memStore -> transform -> publicConfigStore // setup memStore subscription hooks
this.on('update', (memState) => { this.on('update', updatePublicConfigStore)
this.isClientOpenAndUnlocked = memState.isUnlocked && this._isClientOpen updatePublicConfigStore(this.getState())
publicConfigStore.destroy = () => {
this.removeEventListener('update', updatePublicConfigStore)
}
function updatePublicConfigStore (memState) {
const publicState = selectPublicState(memState) const publicState = selectPublicState(memState)
publicConfigStore.putState(publicState) publicConfigStore.putState(publicState)
}) }
function selectPublicState (memState) { function selectPublicState ({ isUnlocked, selectedAddress, network, completedOnboarding }) {
const isEnabled = checkIsEnabled()
const isReady = isUnlocked && isEnabled
const result = { const result = {
selectedAddress: memState.isUnlocked ? memState.selectedAddress : undefined, isUnlocked,
networkVersion: memState.network, isEnabled,
onboardingcomplete: memState.completedOnboarding, selectedAddress: isReady ? selectedAddress : undefined,
networkVersion: network,
onboardingcomplete: completedOnboarding,
} }
return result return result
} }
@ -477,9 +491,10 @@ module.exports = class MetamaskController extends EventEmitter {
signTypedMessage: nodeify(this.signTypedMessage, this), signTypedMessage: nodeify(this.signTypedMessage, this),
cancelTypedMessage: this.cancelTypedMessage.bind(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), clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController),
rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController),
} }
} }
@ -1296,8 +1311,9 @@ module.exports = class MetamaskController extends EventEmitter {
// setup multiplexing // setup multiplexing
const mux = setupMultiplex(connectionStream) const mux = setupMultiplex(connectionStream)
// connect features // connect features
this.setupProviderConnection(mux.createStream('provider'), originDomain) const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain)
this.setupPublicConfig(mux.createStream('publicConfig')) 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 {*} outStream - The stream to provide over.
* @param {string} origin - The URI of the requesting resource. * @param {string} origin - The URI of the requesting resource.
*/ */
setupProviderConnection (outStream, origin) { setupProviderConnection (outStream, origin, publicApi) {
// setup json rpc engine stack // setup json rpc engine stack
const engine = new RpcEngine() const engine = new RpcEngine()
const provider = this.provider const provider = this.provider
@ -1390,6 +1406,11 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(subscriptionManager.middleware) engine.push(subscriptionManager.middleware)
// watch asset // watch asset
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
// requestAccounts
engine.push(this.providerApprovalController.createMiddleware({
origin,
getSiteMetadata: publicApi && publicApi.getSiteMetadata,
}))
// forward to metamask primary provider // forward to metamask primary provider
engine.push(providerAsMiddleware(provider)) engine.push(providerAsMiddleware(provider))
@ -1418,18 +1439,56 @@ module.exports = class MetamaskController extends EventEmitter {
* *
* @param {*} outStream - The stream to provide public config over. * @param {*} outStream - The stream to provide public config over.
*/ */
setupPublicConfig (outStream) { setupPublicConfig (outStream, originDomain) {
const configStream = asStream(this.publicConfigStore) const configStore = this.createPublicConfigStore({
// check the providerApprovalController's approvedOrigins
checkIsEnabled: () => this.providerApprovalController.shouldExposeAccounts(originDomain),
})
const configStream = asStream(configStore)
pump( pump(
configStream, configStream,
outStream, outStream,
(err) => { (err) => {
configStore.destroy()
configStream.destroy() configStream.destroy()
if (err) log.error(err) 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 * Handle a KeyringController update
* @param {object} state the KC state * @param {object} state the KC state
@ -1734,7 +1793,6 @@ module.exports = class MetamaskController extends EventEmitter {
* Locks MetaMask * Locks MetaMask
*/ */
setLocked () { setLocked () {
this.providerApprovalController.setLocked()
return this.keyringController.setLocked() return this.keyringController.setLocked()
} }
} }

@ -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) { _showConfirmedTransaction (txMeta) {
this._subscribeToNotificationClicked() this._subscribeToNotificationClicked()

@ -136,7 +136,6 @@
"obs-store": "^3.0.2", "obs-store": "^3.0.2",
"percentile": "^1.2.0", "percentile": "^1.2.0",
"pify": "^3.0.0", "pify": "^3.0.0",
"ping-pong-stream": "^1.0.0",
"pojo-migrator": "^2.1.0", "pojo-migrator": "^2.1.0",
"polyfill-crypto.getrandomvalues": "^1.0.0", "polyfill-crypto.getrandomvalues": "^1.0.0",
"post-message-stream": "^3.0.0", "post-message-stream": "^3.0.0",

@ -5,12 +5,11 @@ import { PageContainerFooter } from '../../ui/page-container'
export default class ProviderPageContainer extends PureComponent { export default class ProviderPageContainer extends PureComponent {
static propTypes = { static propTypes = {
approveProviderRequest: PropTypes.func.isRequired, approveProviderRequestByOrigin: PropTypes.func.isRequired,
rejectProviderRequestByOrigin: PropTypes.func.isRequired,
origin: PropTypes.string.isRequired, origin: PropTypes.string.isRequired,
rejectProviderRequest: PropTypes.func.isRequired,
siteImage: PropTypes.string, siteImage: PropTypes.string,
siteTitle: PropTypes.string.isRequired, siteTitle: PropTypes.string.isRequired,
tabID: PropTypes.string.isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -29,7 +28,7 @@ export default class ProviderPageContainer extends PureComponent {
} }
onCancel = () => { onCancel = () => {
const { tabID, rejectProviderRequest } = this.props const { origin, rejectProviderRequestByOrigin } = this.props
this.context.metricsEvent({ this.context.metricsEvent({
eventOpts: { eventOpts: {
category: 'Auth', category: 'Auth',
@ -37,11 +36,11 @@ export default class ProviderPageContainer extends PureComponent {
name: 'Canceled', name: 'Canceled',
}, },
}) })
rejectProviderRequest(tabID) rejectProviderRequestByOrigin(origin)
} }
onSubmit = () => { onSubmit = () => {
const { approveProviderRequest, tabID } = this.props const { approveProviderRequestByOrigin, origin } = this.props
this.context.metricsEvent({ this.context.metricsEvent({
eventOpts: { eventOpts: {
category: 'Auth', category: 'Auth',
@ -49,7 +48,7 @@ export default class ProviderPageContainer extends PureComponent {
name: 'Confirmed', name: 'Confirmed',
}, },
}) })
approveProviderRequest(tabID) approveProviderRequestByOrigin(origin)
} }
render () { render () {

@ -4,9 +4,9 @@ import ProviderPageContainer from '../../components/app/provider-page-container'
export default class ProviderApproval extends Component { export default class ProviderApproval extends Component {
static propTypes = { static propTypes = {
approveProviderRequest: PropTypes.func.isRequired, approveProviderRequestByOrigin: PropTypes.func.isRequired,
rejectProviderRequestByOrigin: PropTypes.func.isRequired,
providerRequest: PropTypes.object.isRequired, providerRequest: PropTypes.object.isRequired,
rejectProviderRequest: PropTypes.func.isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -14,13 +14,13 @@ export default class ProviderApproval extends Component {
}; };
render () { render () {
const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props const { approveProviderRequestByOrigin, providerRequest, rejectProviderRequestByOrigin } = this.props
return ( return (
<ProviderPageContainer <ProviderPageContainer
approveProviderRequest={approveProviderRequest} approveProviderRequestByOrigin={approveProviderRequestByOrigin}
rejectProviderRequestByOrigin={rejectProviderRequestByOrigin}
origin={providerRequest.origin} origin={providerRequest.origin}
tabID={providerRequest.tabID} tabID={providerRequest.tabID}
rejectProviderRequest={rejectProviderRequest}
siteImage={providerRequest.siteImage} siteImage={providerRequest.siteImage}
siteTitle={providerRequest.siteTitle} siteTitle={providerRequest.siteTitle}
/> />

@ -1,11 +1,11 @@
import { connect } from 'react-redux' import { connect } from 'react-redux'
import ProviderApproval from './provider-approval.component' import ProviderApproval from './provider-approval.component'
import { approveProviderRequest, rejectProviderRequest } from '../../store/actions' import { approveProviderRequestByOrigin, rejectProviderRequestByOrigin } from '../../store/actions'
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
approveProviderRequest: tabID => dispatch(approveProviderRequest(tabID)), approveProviderRequestByOrigin: origin => dispatch(approveProviderRequestByOrigin(origin)),
rejectProviderRequest: tabID => dispatch(rejectProviderRequest(tabID)), rejectProviderRequestByOrigin: origin => dispatch(rejectProviderRequestByOrigin(origin)),
} }
} }

@ -343,8 +343,8 @@ var actions = {
createCancelTransaction, createCancelTransaction,
createSpeedUpTransaction, createSpeedUpTransaction,
approveProviderRequest, approveProviderRequestByOrigin,
rejectProviderRequest, rejectProviderRequestByOrigin,
clearApprovedOrigins, clearApprovedOrigins,
setFirstTimeFlowType, setFirstTimeFlowType,
@ -2680,15 +2680,15 @@ function setPendingTokens (pendingTokens) {
} }
} }
function approveProviderRequest (tabID) { function approveProviderRequestByOrigin (origin) {
return (dispatch) => { return (dispatch) => {
background.approveProviderRequest(tabID) background.approveProviderRequestByOrigin(origin)
} }
} }
function rejectProviderRequest (tabID) { function rejectProviderRequestByOrigin (origin) {
return (dispatch) => { return (dispatch) => {
background.rejectProviderRequest(tabID) background.rejectProviderRequestByOrigin(origin)
} }
} }

Loading…
Cancel
Save