From 0dfdd44ae7728ed02cbf32c564c75b74f37acf77 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Tue, 12 Jan 2021 17:43:45 -0800 Subject: [PATCH] Restore support for @metamask/inpage provider@"< 8.0.0" (#10179) This restores support for versions of the inpage provider prior to v8. This is intended to support dapps and extensions that directly instantiated their own provider rather than using the injected provider. * Forward traffic between old and new provider streams * Ignore publicConfig stream for non-legacy muxes * Transform accountsChanged notification for legacy streams * Convert publicConfigStore to singleton Co-authored-by: Mark Stacey --- app/scripts/contentscript.js | 77 ++++++++++++++++++++++++++++++ app/scripts/metamask-controller.js | 63 ++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 7e55faec4..116cad02d 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -4,6 +4,7 @@ import LocalMessageDuplexStream from 'post-message-stream' import ObjectMultiplex from 'obj-multiplex' import extension from 'extensionizer' import PortStream from 'extension-port-stream' +import { obj as createThoughStream } from 'through2' // These require calls need to use require to be statically recognized by browserify const fs = require('fs') @@ -20,6 +21,12 @@ const CONTENT_SCRIPT = 'metamask-contentscript' const INPAGE = 'metamask-inpage' const PROVIDER = 'metamask-provider' +// TODO:LegacyProvider: Delete +const LEGACY_CONTENT_SCRIPT = 'contentscript' +const LEGACY_INPAGE = 'inpage' +const LEGACY_PROVIDER = 'provider' +const LEGACY_PUBLIC_CONFIG = 'publicConfig' + if (shouldInjectProvider()) { injectScript(inpageBundle) setupStreams() @@ -63,6 +70,7 @@ async function setupStreams() { pageMux.setMaxListeners(25) const extensionMux = new ObjectMultiplex() extensionMux.setMaxListeners(25) + extensionMux.ignoreStream(LEGACY_PUBLIC_CONFIG) // TODO:LegacyProvider: Delete pump(pageMux, pageStream, pageMux, (err) => logStreamDisconnectWarning('MetaMask Inpage Multiplex', err), @@ -78,6 +86,44 @@ async function setupStreams() { // connect "phishing" channel to warning system const phishingStream = extensionMux.createStream('phishing') phishingStream.once('data', redirectToPhishingWarning) + + // TODO:LegacyProvider: Delete + // handle legacy provider + const legacyPageStream = new LocalMessageDuplexStream({ + name: LEGACY_CONTENT_SCRIPT, + target: LEGACY_INPAGE, + }) + + const legacyPageMux = new ObjectMultiplex() + legacyPageMux.setMaxListeners(25) + const legacyExtensionMux = new ObjectMultiplex() + legacyExtensionMux.setMaxListeners(25) + + pump(legacyPageMux, legacyPageStream, legacyPageMux, (err) => + logStreamDisconnectWarning('MetaMask Legacy Inpage Multiplex', err), + ) + pump( + legacyExtensionMux, + extensionStream, + getNotificationTransformStream(), + legacyExtensionMux, + (err) => { + logStreamDisconnectWarning('MetaMask Background Legacy Multiplex', err) + notifyInpageOfStreamFailure() + }, + ) + + forwardNamedTrafficBetweenMuxes( + LEGACY_PROVIDER, + PROVIDER, + legacyPageMux, + legacyExtensionMux, + ) + forwardTrafficBetweenMuxes( + LEGACY_PUBLIC_CONFIG, + legacyPageMux, + legacyExtensionMux, + ) } function forwardTrafficBetweenMuxes(channelName, muxA, muxB) { @@ -91,6 +137,37 @@ function forwardTrafficBetweenMuxes(channelName, muxA, muxB) { ) } +// TODO:LegacyProvider: Delete +function forwardNamedTrafficBetweenMuxes( + channelAName, + channelBName, + muxA, + muxB, +) { + const channelA = muxA.createStream(channelAName) + const channelB = muxB.createStream(channelBName) + pump(channelA, channelB, channelA, (error) => + console.debug( + `MetaMask: Muxed traffic between channels "${channelAName}" and "${channelBName}" failed.`, + error, + ), + ) +} + +// TODO:LegacyProvider: Delete +function getNotificationTransformStream() { + return createThoughStream((chunk, _, cb) => { + if (chunk?.name === PROVIDER) { + if (chunk.data?.method === 'metamask_accountsChanged') { + chunk.data.method = 'wallet_accountsChanged' + chunk.data.result = chunk.data.params + delete chunk.data.params + } + } + cb(null, chunk) + }) +} + /** * Error handler for page to extension stream disconnections * diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 0608ac2b8..2e76464aa 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1,6 +1,8 @@ import EventEmitter from 'events' import pump from 'pump' import Dnode from 'dnode' +import { ObservableStore } from '@metamask/obs-store' +import { storeAsStream } from '@metamask/obs-store/dist/asStream' import { JsonRpcEngine } from 'json-rpc-engine' import { debounce } from 'lodash' import createEngineStream from 'json-rpc-middleware-stream/engineStream' @@ -413,6 +415,9 @@ export default class MetamaskController extends EventEmitter { ) { this.submitPassword(password) } + + // TODO:LegacyProvider: Delete + this.publicConfigStore = this.createPublicConfigStore() } /** @@ -459,6 +464,38 @@ export default class MetamaskController extends EventEmitter { return providerProxy } + /** + * TODO:LegacyProvider: Delete + * Constructor helper: initialize a public config store. + * This store is used to make some config info available to Dapps synchronously. + */ + createPublicConfigStore() { + // subset of state for metamask inpage provider + const publicConfigStore = new ObservableStore() + const { networkController } = this + + // setup memStore subscription hooks + this.on('update', updatePublicConfigStore) + updatePublicConfigStore(this.getState()) + + function updatePublicConfigStore(memState) { + const chainId = networkController.getCurrentChainId() + if (memState.network !== 'loading') { + publicConfigStore.putState(selectPublicState(chainId, memState)) + } + } + + function selectPublicState(chainId, { isUnlocked, network }) { + return { + isUnlocked, + chainId, + networkVersion: network, + } + } + + return publicConfigStore + } + /** * Gets relevant state for the provider of an external origin. * @@ -1831,6 +1868,10 @@ export default class MetamaskController extends EventEmitter { // messages between inpage and background this.setupProviderConnection(mux.createStream('metamask-provider'), sender) + + // TODO:LegacyProvider: Delete + // legacy streams + this.setupPublicConfig(mux.createStream('publicConfig')) } /** @@ -2016,6 +2057,28 @@ export default class MetamaskController extends EventEmitter { return engine } + /** + * TODO:LegacyProvider: Delete + * A method for providing our public config info over a stream. + * This includes info we like to be synchronous if possible, like + * the current selected account, and network ID. + * + * Since synchronous methods have been deprecated in web3, + * this is a good candidate for deprecation. + * + * @param {*} outStream - The stream to provide public config over. + */ + setupPublicConfig(outStream) { + const configStream = storeAsStream(this.publicConfigStore) + + pump(configStream, outStream, (err) => { + configStream.destroy() + if (err) { + log.error(err) + } + }) + } + /** * Adds a reference to a connection by origin. Ignores the 'metamask' origin. * Caller must ensure that the returned id is stored such that the reference