1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

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
This commit is contained in:
kumavis 2019-05-04 01:32:05 +08:00 committed by Frankie
parent 2ff522604b
commit 2845398c3d
17 changed files with 300 additions and 411 deletions

View File

@ -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()
}
/**
@ -47,148 +44,107 @@ function injectScript (content) {
}
/**
* Sets up two-way communication streams between the
* browser extension and local per-page browser context
* Sets up the stream communication and submits site metadata
*
*/
function setupStreams () {
// setup communication to page and plugin
async function start () {
await setupStreams()
await domIsReady()
}
/**
* Sets up two-way communication streams between the
* browser extension and local per-page browser context.
*
*/
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)
)
// setup local multistream channels
const mux = new ObjectMultiplex()
mux.setMaxListeners(25)
pump(
mux,
pageStream,
mux,
(err) => logStreamDisconnectWarning('MetaMask Inpage', err)
pageMux,
(err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', 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)
)
}
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)
}
})
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' }, '*')
}
})
)
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 }))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<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 () {
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<boolean>} - 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
//

View File

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

View File

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

View File

@ -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()

View File

@ -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",

View File

@ -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 () {

View File

@ -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 (
<ProviderPageContainer
approveProviderRequest={approveProviderRequest}
approveProviderRequestByOrigin={approveProviderRequestByOrigin}
rejectProviderRequestByOrigin={rejectProviderRequestByOrigin}
origin={providerRequest.origin}
tabID={providerRequest.tabID}
rejectProviderRequest={rejectProviderRequest}
siteImage={providerRequest.siteImage}
siteTitle={providerRequest.siteTitle}
/>

View File

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

View File

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