aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorWhymarrh Whitby <whymarrh.whitby@gmail.com>2019-08-01 21:24:33 +0800
committerGitHub <noreply@github.com>2019-08-01 21:24:33 +0800
commite9a63d5d5b428e8ace6423652d8691205bb129f0 (patch)
tree69c214f85143b61f22b6f19bf313a608c32c0999 /app
parent4d88e1cf862c3ae174780cd888d7703685db23e7 (diff)
downloadtangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar.gz
tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.tar.zst
tangerine-wallet-browser-e9a63d5d5b428e8ace6423652d8691205bb129f0.zip
Default Privacy Mode to ON, allow force sharing address (#6904)
Diffstat (limited to 'app')
-rw-r--r--app/_locales/en/messages.json12
-rw-r--r--app/images/icons/connect.svg7
-rw-r--r--app/images/icons/info.svg5
-rw-r--r--app/scripts/contentscript.js8
-rw-r--r--app/scripts/controllers/preferences.js8
-rw-r--r--app/scripts/controllers/provider-approval.js74
-rw-r--r--app/scripts/metamask-controller.js8
-rw-r--r--app/scripts/migrations/034.js33
-rw-r--r--app/scripts/popup-core.js77
-rw-r--r--app/scripts/ui.js125
10 files changed, 245 insertions, 112 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 1f60bfa57..f15dff386 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -1,4 +1,16 @@
{
+ "shareAddress": {
+ "message": "Share Address"
+ },
+ "shareAddressToConnect": {
+ "message": "Share your address to connect to $1?"
+ },
+ "shareAddressInfo": {
+ "message": "Sharing your address with $1 will allow you to interact with this dapp. This permission is to protect your privacy by default."
+ },
+ "privacyModeDefault": {
+ "message": "Privacy Mode is now enabled by default"
+ },
"privacyMode": {
"message": "Privacy Mode"
},
diff --git a/app/images/icons/connect.svg b/app/images/icons/connect.svg
new file mode 100644
index 000000000..24543e8d8
--- /dev/null
+++ b/app/images/icons/connect.svg
@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M8.00002 9.57037C8.93767 9.57037 9.69778 8.81026 9.69778 7.8726C9.69778 6.93495 8.93767 6.17484 8.00002 6.17484C7.06236 6.17484 6.30225 6.93495 6.30225 7.8726C6.30225 8.81026 7.06236 9.57037 8.00002 9.57037Z" fill="white"/>
+ <path d="M11.0582 11.6586C10.872 11.6586 10.6857 11.5876 10.5437 11.4455C10.2595 11.1614 10.2595 10.7007 10.5437 10.4165C11.2232 9.73704 11.5975 8.83356 11.5975 7.87259C11.5975 6.91161 11.2232 6.00813 10.5437 5.32865C10.2595 5.04448 10.2595 4.58381 10.5437 4.29964C10.8278 4.01554 11.2886 4.01554 11.5727 4.29964C12.527 5.25398 13.0527 6.52293 13.0527 7.87259C13.0527 9.22224 12.527 10.4912 11.5727 11.4455C11.4306 11.5876 11.2444 11.6586 11.0582 11.6586Z" fill="white"/>
+ <path d="M4.94175 11.6586C4.75553 11.6586 4.56929 11.5876 4.42724 11.4455C3.4729 10.4912 2.94727 9.22224 2.94727 7.87259C2.94727 6.52293 3.4729 5.25398 4.42724 4.29964C4.71135 4.01554 5.17215 4.01554 5.45626 4.29964C5.74043 4.58381 5.74043 5.04448 5.45626 5.32865C4.77672 6.00813 4.4025 6.91161 4.4025 7.87259C4.4025 8.83356 4.77672 9.73704 5.45626 10.4165C5.74043 10.7007 5.74043 11.1614 5.45626 11.4455C5.3142 11.5876 5.12798 11.6586 4.94175 11.6586Z" fill="white"/>
+ <path d="M13.1451 13.7453C12.9589 13.7453 12.7727 13.6742 12.6306 13.5322C12.3464 13.248 12.3464 12.7873 12.6306 12.5031C15.1839 9.94985 15.1839 5.79538 12.6306 3.24209C12.3464 2.95792 12.3464 2.49725 12.6306 2.21308C12.9147 1.92897 13.3755 1.92897 13.6596 2.21308C16.7803 5.33374 16.7803 10.4115 13.6596 13.5322C13.5176 13.6742 13.3313 13.7453 13.1451 13.7453Z" fill="white"/>
+ <path d="M2.855 13.7453C2.66878 13.7453 2.48255 13.6742 2.3405 13.5322C-0.780166 10.4115 -0.780166 5.33374 2.3405 2.21308C2.62461 1.92897 3.08541 1.92897 3.36951 2.21308C3.65368 2.49725 3.65368 2.95792 3.36951 3.24209C0.816221 5.79538 0.816221 9.94985 3.36951 12.5031C3.65368 12.7873 3.65368 13.248 3.36951 13.5322C3.22745 13.6742 3.04123 13.7453 2.855 13.7453Z" fill="white"/>
+</svg>
diff --git a/app/images/icons/info.svg b/app/images/icons/info.svg
new file mode 100644
index 000000000..138811bae
--- /dev/null
+++ b/app/images/icons/info.svg
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.99984 2.00001C4.68613 2.00001 1.99984 4.6863 1.99984 8C1.99984 11.3137 4.68613 14 7.99984 14C11.3135 14 13.9998 11.3137 13.9998 8C13.9998 4.6863 11.3135 2.00001 7.99984 2.00001ZM0.666504 8C0.666504 3.94992 3.94975 0.666672 7.99984 0.666672C12.0499 0.666672 15.3332 3.94992 15.3332 8C15.3332 12.0501 12.0499 15.3333 7.99984 15.3333C3.94975 15.3333 0.666504 12.0501 0.666504 8Z" fill="#6A737D"/>
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.99984 7.33334C8.36803 7.33334 8.6665 7.63181 8.6665 8V10.6667C8.6665 11.0349 8.36803 11.3333 7.99984 11.3333C7.63165 11.3333 7.33317 11.0349 7.33317 10.6667V8C7.33317 7.63181 7.63165 7.33334 7.99984 7.33334Z" fill="#6A737D"/>
+ <path d="M8.6665 5.33334C8.6665 5.70153 8.36803 6 7.99984 6C7.63165 6 7.33317 5.70153 7.33317 5.33334C7.33317 4.96515 7.63165 4.66667 7.99984 4.66667C8.36803 4.66667 8.6665 4.96515 8.6665 5.33334Z" fill="#6A737D"/>
+</svg>
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index db4d5fd63..7415c5fe9 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -114,6 +114,7 @@ function forwardTrafficBetweenMuxers (channelName, muxA, muxB) {
async function setupPublicApi (outStream) {
const api = {
+ forceReloadSite: (cb) => cb(null, forceReloadSite()),
getSiteMetadata: (cb) => cb(null, getSiteMetadata()),
}
const dnode = Dnode(api)
@@ -306,3 +307,10 @@ async function domIsReady () {
// wait for load
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve, { once: true }))
}
+
+/**
+ * Reloads the site
+ */
+function forceReloadSite () {
+ window.location.reload()
+}
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 24df29c1d..d480834f5 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -54,6 +54,7 @@ class PreferencesController {
useNativeCurrencyAsPrimaryCurrency: true,
},
completedOnboarding: false,
+ migratedPrivacyMode: false,
metaMetricsId: null,
metaMetricsSendCount: 0,
}, opts.initState)
@@ -603,6 +604,13 @@ class PreferencesController {
return Promise.resolve(true)
}
+ unsetMigratedPrivacyMode () {
+ this.store.updateState({
+ migratedPrivacyMode: false,
+ })
+ return Promise.resolve()
+ }
+
//
// PRIVATE METHODS
//
diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js
index 06c499780..00ec0aea1 100644
--- a/app/scripts/controllers/provider-approval.js
+++ b/app/scripts/controllers/provider-approval.js
@@ -18,12 +18,12 @@ class ProviderApprovalController extends SafeEventEmitter {
*/
constructor ({ closePopup, keyringController, openPopup, preferencesController } = {}) {
super()
- this.approvedOrigins = {}
this.closePopup = closePopup
this.keyringController = keyringController
this.openPopup = openPopup
this.preferencesController = preferencesController
this.store = new ObservableStore({
+ approvedOrigins: {},
providerRequests: [],
})
}
@@ -45,7 +45,7 @@ class ProviderApprovalController extends SafeEventEmitter {
}
// register the provider request
const metadata = await getSiteMetadata(origin)
- this._handleProviderRequest(origin, metadata.name, metadata.icon, false, null)
+ this._handleProviderRequest(origin, metadata.name, metadata.icon)
// wait for resolution of request
const approved = await new Promise(resolve => this.once(`resolvedRequest:${origin}`, ({ approved }) => resolve(approved)))
if (approved) {
@@ -63,10 +63,10 @@ class ProviderApprovalController extends SafeEventEmitter {
* @param {string} siteTitle - The title of the document requesting full provider access
* @param {string} siteImage - The icon of the window requesting full provider access
*/
- _handleProviderRequest (origin, siteTitle, siteImage, force, tabID) {
- this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage, tabID }] })
+ _handleProviderRequest (origin, siteTitle, siteImage) {
+ this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] })
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
- if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) {
+ if (this.store.getState().approvedOrigins[origin] && this.caching && isUnlocked) {
return
}
this.openPopup && this.openPopup()
@@ -78,11 +78,19 @@ class ProviderApprovalController extends SafeEventEmitter {
* @param {string} origin - origin of the domain that had provider access approved
*/
approveProviderRequestByOrigin (origin) {
- this.closePopup && this.closePopup()
- const requests = this.store.getState().providerRequests
- const providerRequests = requests.filter(request => request.origin !== origin)
- this.store.updateState({ providerRequests })
- this.approvedOrigins[origin] = true
+ if (this.closePopup) {
+ this.closePopup()
+ }
+
+ const { approvedOrigins, providerRequests } = this.store.getState()
+ const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin)
+ this.store.updateState({
+ approvedOrigins: {
+ ...approvedOrigins,
+ [origin]: true,
+ },
+ providerRequests: remainingProviderRequests,
+ })
this.emit(`resolvedRequest:${origin}`, { approved: true })
}
@@ -92,19 +100,50 @@ class ProviderApprovalController extends SafeEventEmitter {
* @param {string} origin - origin of the domain that had provider access approved
*/
rejectProviderRequestByOrigin (origin) {
- this.closePopup && this.closePopup()
- const requests = this.store.getState().providerRequests
- const providerRequests = requests.filter(request => request.origin !== origin)
- this.store.updateState({ providerRequests })
- delete this.approvedOrigins[origin]
+ if (this.closePopup) {
+ this.closePopup()
+ }
+
+ const { approvedOrigins, providerRequests } = this.store.getState()
+ const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin)
+
+ // We're cloning and deleting keys here because we don't want to keep unneeded keys
+ const _approvedOrigins = Object.assign({}, approvedOrigins)
+ delete _approvedOrigins[origin]
+
+ this.store.putState({
+ approvedOrigins: _approvedOrigins,
+ providerRequests: remainingProviderRequests,
+ })
this.emit(`resolvedRequest:${origin}`, { approved: false })
}
/**
+ * Silently approves access to a full Ethereum provider API for the origin
+ *
+ * @param {string} origin - origin of the domain that had provider access approved
+ */
+ forceApproveProviderRequestByOrigin (origin) {
+ const { approvedOrigins, providerRequests } = this.store.getState()
+ const remainingProviderRequests = providerRequests.filter(request => request.origin !== origin)
+ this.store.updateState({
+ approvedOrigins: {
+ ...approvedOrigins,
+ [origin]: true,
+ },
+ providerRequests: remainingProviderRequests,
+ })
+
+ this.emit(`forceResolvedRequest:${origin}`, { approved: true, forced: true })
+ }
+
+ /**
* Clears any cached approvals for user-approved origins
*/
clearApprovedOrigins () {
- this.approvedOrigins = {}
+ this.store.updateState({
+ approvedOrigins: {},
+ })
}
/**
@@ -115,8 +154,7 @@ class ProviderApprovalController extends SafeEventEmitter {
*/
shouldExposeAccounts (origin) {
const privacyMode = this.preferencesController.getFeatureFlags().privacyMode
- const result = !privacyMode || Boolean(this.approvedOrigins[origin])
- return result
+ return !privacyMode || Boolean(this.store.getState().approvedOrigins[origin])
}
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 26dde8288..158fb3079 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -454,6 +454,7 @@ module.exports = class MetamaskController extends EventEmitter {
setPreference: nodeify(preferencesController.setPreference, preferencesController),
completeOnboarding: nodeify(preferencesController.completeOnboarding, preferencesController),
addKnownMethodData: nodeify(preferencesController.addKnownMethodData, preferencesController),
+ unsetMigratedPrivacyMode: nodeify(preferencesController.unsetMigratedPrivacyMode, preferencesController),
// BlacklistController
whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),
@@ -498,6 +499,7 @@ module.exports = class MetamaskController extends EventEmitter {
// provider approval
approveProviderRequestByOrigin: providerApprovalController.approveProviderRequestByOrigin.bind(providerApprovalController),
rejectProviderRequestByOrigin: providerApprovalController.rejectProviderRequestByOrigin.bind(providerApprovalController),
+ forceApproveProviderRequestByOrigin: providerApprovalController.forceApproveProviderRequestByOrigin.bind(providerApprovalController),
clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController),
}
}
@@ -1285,6 +1287,8 @@ module.exports = class MetamaskController extends EventEmitter {
const publicApi = this.setupPublicApi(mux.createStream('publicApi'), originDomain)
this.setupProviderConnection(mux.createStream('provider'), originDomain, publicApi)
this.setupPublicConfig(mux.createStream('publicConfig'), originDomain)
+
+ this.providerApprovalController.on(`forceResolvedRequest:${originDomain}`, publicApi.forceReloadSite)
}
/**
@@ -1465,6 +1469,10 @@ module.exports = class MetamaskController extends EventEmitter {
const publicApi = {
// wrap with an await remote
+ forceReloadSite: async () => {
+ const remote = await getRemote()
+ return await pify(remote.forceReloadSite)()
+ },
getSiteMetadata: async () => {
const remote = await getRemote()
return await pify(remote.getSiteMetadata)()
diff --git a/app/scripts/migrations/034.js b/app/scripts/migrations/034.js
new file mode 100644
index 000000000..7c852de96
--- /dev/null
+++ b/app/scripts/migrations/034.js
@@ -0,0 +1,33 @@
+const version = 34
+const clone = require('clone')
+
+/**
+ * The purpose of this migration is to enable the {@code privacyMode} feature flag and set the user as being migrated
+ * if it was {@code false}.
+ */
+module.exports = {
+ version,
+ migrate: async function (originalVersionedData) {
+ const versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ const state = versionedData.data
+ versionedData.data = transformState(state)
+ return versionedData
+ },
+}
+
+function transformState (state) {
+ const { PreferencesController } = state
+
+ if (PreferencesController) {
+ const featureFlags = PreferencesController.featureFlags || {}
+
+ if (!featureFlags.privacyMode && typeof PreferencesController.migratedPrivacyMode === 'undefined') {
+ // Mark the state has being migrated and enable Privacy Mode
+ PreferencesController.migratedPrivacyMode = true
+ featureFlags.privacyMode = true
+ }
+ }
+
+ return state
+}
diff --git a/app/scripts/popup-core.js b/app/scripts/popup-core.js
deleted file mode 100644
index c08e9fa24..000000000
--- a/app/scripts/popup-core.js
+++ /dev/null
@@ -1,77 +0,0 @@
-const {EventEmitter} = require('events')
-const async = require('async')
-const Dnode = require('dnode')
-const Eth = require('ethjs')
-const EthQuery = require('eth-query')
-const launchMetamaskUi = require('../../ui')
-const StreamProvider = require('web3-stream-provider')
-const {setupMultiplex} = require('./lib/stream-utils.js')
-
-module.exports = initializePopup
-
-/**
- * Asynchronously initializes the MetaMask popup UI
- *
- * @param {{ container: Element, connectionStream: * }} config Popup configuration object
- * @param {Function} cb Called when initialization is complete
- */
-function initializePopup ({ container, connectionStream }, cb) {
- // setup app
- async.waterfall([
- (cb) => connectToAccountManager(connectionStream, cb),
- (backgroundConnection, cb) => launchMetamaskUi({ container, backgroundConnection }, cb),
- ], cb)
-}
-
-/**
- * Establishes streamed connections to background scripts and a Web3 provider
- *
- * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
- * @param {Function} cb Called when controller connection is established
- */
-function connectToAccountManager (connectionStream, cb) {
- // setup communication with background
- // setup multiplexing
- const mx = setupMultiplex(connectionStream)
- // connect features
- setupControllerConnection(mx.createStream('controller'), cb)
- setupWeb3Connection(mx.createStream('provider'))
-}
-
-/**
- * Establishes a streamed connection to a Web3 provider
- *
- * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
- */
-function setupWeb3Connection (connectionStream) {
- const providerStream = new StreamProvider()
- providerStream.pipe(connectionStream).pipe(providerStream)
- connectionStream.on('error', console.error.bind(console))
- providerStream.on('error', console.error.bind(console))
- global.ethereumProvider = providerStream
- global.ethQuery = new EthQuery(providerStream)
- global.eth = new Eth(providerStream)
-}
-
-/**
- * Establishes a streamed connection to the background account manager
- *
- * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
- * @param {Function} cb Called when the remote account manager connection is established
- */
-function setupControllerConnection (connectionStream, cb) {
- // this is a really sneaky way of adding EventEmitter api
- // to a bi-directional dnode instance
- const eventEmitter = new EventEmitter()
- const backgroundDnode = Dnode({
- sendUpdate: function (state) {
- eventEmitter.emit('update', state)
- },
- })
- connectionStream.pipe(backgroundDnode).pipe(connectionStream)
- backgroundDnode.once('remote', function (backgroundConnection) {
- // setup push events
- backgroundConnection.on = eventEmitter.on.bind(eventEmitter)
- cb(null, backgroundConnection)
- })
-}
diff --git a/app/scripts/ui.js b/app/scripts/ui.js
index 2dde14b48..a1f904f61 100644
--- a/app/scripts/ui.js
+++ b/app/scripts/ui.js
@@ -1,12 +1,19 @@
-const startPopup = require('./popup-core')
const PortStream = require('extension-port-stream')
const { getEnvironmentType } = require('./lib/util')
-const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN } = require('./lib/enums')
+const { ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_FULLSCREEN, ENVIRONMENT_TYPE_POPUP } = require('./lib/enums')
const extension = require('extensionizer')
const ExtensionPlatform = require('./platforms/extension')
const NotificationManager = require('./lib/notification-manager')
const notificationManager = new NotificationManager()
const setupSentry = require('./lib/setupSentry')
+const {EventEmitter} = require('events')
+const Dnode = require('dnode')
+const Eth = require('ethjs')
+const EthQuery = require('eth-query')
+const urlUtil = require('url')
+const launchMetaMaskUi = require('../../ui')
+const StreamProvider = require('web3-stream-provider')
+const {setupMultiplex} = require('./lib/stream-utils.js')
const log = require('loglevel')
start().catch(log.error)
@@ -39,20 +46,8 @@ async function start () {
const extensionPort = extension.runtime.connect({ name: windowType })
const connectionStream = new PortStream(extensionPort)
- // start ui
- const container = document.getElementById('app-content')
- startPopup({ container, connectionStream }, (err, store) => {
- if (err) return displayCriticalError(err)
-
- const state = store.getState()
- const { metamask: { completedOnboarding } = {} } = state
-
- if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) {
- global.platform.openExtensionInBrowser()
- return
- }
- })
-
+ const activeTab = await queryCurrentActiveTab(windowType)
+ initializeUiWithTab(activeTab)
function closePopupIfOpen (windowType) {
if (windowType !== ENVIRONMENT_TYPE_NOTIFICATION) {
@@ -61,11 +56,107 @@ async function start () {
}
}
- function displayCriticalError (err) {
+ function displayCriticalError (container, err) {
container.innerHTML = '<div class="critical-error">The MetaMask app failed to load: please open and close MetaMask again to restart.</div>'
container.style.height = '80px'
log.error(err.stack)
throw err
}
+ function initializeUiWithTab (tab) {
+ const container = document.getElementById('app-content')
+ initializeUi(tab, container, connectionStream, (err, store) => {
+ if (err) {
+ return displayCriticalError(container, err)
+ }
+
+ const state = store.getState()
+ const { metamask: { completedOnboarding } = {} } = state
+
+ if (!completedOnboarding && windowType !== ENVIRONMENT_TYPE_FULLSCREEN) {
+ global.platform.openExtensionInBrowser()
+ }
+ })
+ }
+}
+
+async function queryCurrentActiveTab (windowType) {
+ return new Promise((resolve) => {
+ // At the time of writing we only have the `activeTab` permission which means
+ // that this query will only succeed in the popup context (i.e. after a "browserAction")
+ if (windowType !== ENVIRONMENT_TYPE_POPUP) {
+ resolve({})
+ return
+ }
+
+ extension.tabs.query({active: true, currentWindow: true}, (tabs) => {
+ const [activeTab] = tabs
+ const {title, url} = activeTab
+ const origin = url ? urlUtil.parse(url).hostname : null
+ resolve({
+ title, origin, url,
+ })
+ })
+ })
+}
+
+function initializeUi (activeTab, container, connectionStream, cb) {
+ connectToAccountManager(connectionStream, (err, backgroundConnection) => {
+ if (err) {
+ return cb(err)
+ }
+
+ launchMetaMaskUi({
+ activeTab,
+ container,
+ backgroundConnection,
+ }, cb)
+ })
+}
+
+/**
+ * Establishes a connection to the background and a Web3 provider
+ *
+ * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
+ * @param {Function} cb Called when controller connection is established
+ */
+function connectToAccountManager (connectionStream, cb) {
+ const mx = setupMultiplex(connectionStream)
+ setupControllerConnection(mx.createStream('controller'), cb)
+ setupWeb3Connection(mx.createStream('provider'))
+}
+
+/**
+ * Establishes a streamed connection to a Web3 provider
+ *
+ * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
+ */
+function setupWeb3Connection (connectionStream) {
+ const providerStream = new StreamProvider()
+ providerStream.pipe(connectionStream).pipe(providerStream)
+ connectionStream.on('error', console.error.bind(console))
+ providerStream.on('error', console.error.bind(console))
+ global.ethereumProvider = providerStream
+ global.ethQuery = new EthQuery(providerStream)
+ global.eth = new Eth(providerStream)
+}
+
+/**
+ * Establishes a streamed connection to the background account manager
+ *
+ * @param {PortDuplexStream} connectionStream PortStream instance establishing a background connection
+ * @param {Function} cb Called when the remote account manager connection is established
+ */
+function setupControllerConnection (connectionStream, cb) {
+ const eventEmitter = new EventEmitter()
+ const backgroundDnode = Dnode({
+ sendUpdate: function (state) {
+ eventEmitter.emit('update', state)
+ },
+ })
+ connectionStream.pipe(backgroundDnode).pipe(connectionStream)
+ backgroundDnode.once('remote', function (backgroundConnection) {
+ backgroundConnection.on = eventEmitter.on.bind(eventEmitter)
+ cb(null, backgroundConnection)
+ })
}