From 2f7d4494278ad809c1cc9fcc0d9438182003b22d Mon Sep 17 00:00:00 2001 From: Paul Bouchon Date: Tue, 19 Feb 2019 19:42:08 -0500 Subject: EIP-1193: standard provider API (#6170) * EIP-1193: Implement new provider API * EIP-1193: Updated implementation * Remove test file * Fix tests * Update ping check * Update logic * PR feedback --- app/scripts/contentscript.js | 7 +- .../controllers/network/createBlockTracker.js | 19 +++++ .../controllers/network/createInfuraClient.js | 6 +- .../controllers/network/createJsonRpcClient.js | 6 +- .../controllers/network/createLocalhostClient.js | 6 +- app/scripts/controllers/network/network.js | 9 ++- app/scripts/createStandardProvider.js | 92 ++++++++++++++++++++++ app/scripts/inpage.js | 14 ++-- app/scripts/metamask-controller.js | 2 +- 9 files changed, 138 insertions(+), 23 deletions(-) create mode 100644 app/scripts/controllers/network/createBlockTracker.js create mode 100644 app/scripts/createStandardProvider.js diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 65e2ec523..68b6117e5 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -158,7 +158,7 @@ function listenForProviderRequest () { window.postMessage({ type: 'ethereumproviderlegacy', selectedAddress }, '*') break case 'reject-provider-request': - window.postMessage({ type: 'ethereumprovider', error: 'User rejected provider access' }, '*') + window.postMessage({ type: 'ethereumprovider', error: 'User denied account authorization' }, '*') break case 'answer-is-approved': window.postMessage({ type: 'ethereumisapproved', isApproved, caching }, '*') @@ -170,6 +170,11 @@ function listenForProviderRequest () { isEnabled = false window.postMessage({ type: 'metamasksetlocked' }, '*') break + case 'ethereum-ping-success': + window.postMessage({ type: 'ethereumpingsuccess' }, '*') + break + case 'ethereum-ping-error': + window.postMessage({ type: 'ethereumpingerror' }, '*') } }) } diff --git a/app/scripts/controllers/network/createBlockTracker.js b/app/scripts/controllers/network/createBlockTracker.js new file mode 100644 index 000000000..6573b18a1 --- /dev/null +++ b/app/scripts/controllers/network/createBlockTracker.js @@ -0,0 +1,19 @@ +const BlockTracker = require('eth-block-tracker') + +/** + * Creates a block tracker that sends platform events on success and failure + */ +module.exports = function createBlockTracker (args, platform) { + const blockTracker = new BlockTracker(args) + blockTracker.on('latest', () => { + if (platform && platform.sendMessage) { + platform.sendMessage({ action: 'ethereum-ping-success' }) + } + }) + blockTracker.on('error', () => { + if (platform && platform.sendMessage) { + platform.sendMessage({ action: 'ethereum-ping-error' }) + } + }) + return blockTracker +} diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js index 5281dc4c1..884b94db3 100644 --- a/app/scripts/controllers/network/createInfuraClient.js +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -7,14 +7,14 @@ const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') const createInfuraMiddleware = require('eth-json-rpc-infura') -const BlockTracker = require('eth-block-tracker') +const createBlockTracker = require('./createBlockTracker') module.exports = createInfuraClient -function createInfuraClient ({ network }) { +function createInfuraClient ({ network, platform }) { const infuraMiddleware = createInfuraMiddleware({ network, maxAttempts: 5, source: 'metamask' }) const infuraProvider = providerFromMiddleware(infuraMiddleware) - const blockTracker = new BlockTracker({ provider: infuraProvider }) + const blockTracker = createBlockTracker({ provider: infuraProvider }, platform) const networkMiddleware = mergeMiddleware([ createNetworkAndChainIdMiddleware({ network }), diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js index a8cbf2aaf..369dcd299 100644 --- a/app/scripts/controllers/network/createJsonRpcClient.js +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -5,14 +5,14 @@ const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache' const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const BlockTracker = require('eth-block-tracker') +const createBlockTracker = require('./createBlockTracker') module.exports = createJsonRpcClient -function createJsonRpcClient ({ rpcUrl }) { +function createJsonRpcClient ({ rpcUrl, platform }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = new BlockTracker({ provider: blockProvider }) + const blockTracker = createBlockTracker({ provider: blockProvider }, platform) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js index 09b1d3c1c..36593dc70 100644 --- a/app/scripts/controllers/network/createLocalhostClient.js +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -3,14 +3,14 @@ const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite') const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector') const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware') -const BlockTracker = require('eth-block-tracker') +const createBlockTracker = require('./createBlockTracker') module.exports = createLocalhostClient -function createLocalhostClient () { +function createLocalhostClient ({ platform }) { const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) const blockProvider = providerFromMiddleware(fetchMiddleware) - const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) + const blockTracker = createBlockTracker({ provider: blockProvider, pollingInterval: 1000 }, platform) const networkMiddleware = mergeMiddleware([ createBlockRefRewriteMiddleware({ blockTracker }), diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 2958ba3b0..0c6327f6e 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -37,8 +37,9 @@ const defaultNetworkConfig = { module.exports = class NetworkController extends EventEmitter { - constructor (opts = {}) { + constructor (opts = {}, platform) { super() + this.platform = platform // parse options const providerConfig = opts.provider || defaultProviderConfig @@ -180,7 +181,7 @@ module.exports = class NetworkController extends EventEmitter { _configureInfuraProvider ({ type }) { log.info('NetworkController - configureInfuraProvider', type) - const networkClient = createInfuraClient({ network: type }) + const networkClient = createInfuraClient({ network: type, platform: this.platform }) this._setNetworkClient(networkClient) // setup networkConfig var settings = { @@ -191,13 +192,13 @@ module.exports = class NetworkController extends EventEmitter { _configureLocalhostProvider () { log.info('NetworkController - configureLocalhostProvider') - const networkClient = createLocalhostClient() + const networkClient = createLocalhostClient({ platform: this.platform }) this._setNetworkClient(networkClient) } _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) { log.info('NetworkController - configureStandardProvider', rpcUrl) - const networkClient = createJsonRpcClient({ rpcUrl }) + const networkClient = createJsonRpcClient({ rpcUrl, platform: this.platform }) // hack to add a 'rpc' network with chainId networks.networkList['rpc'] = { chainId: chainId, diff --git a/app/scripts/createStandardProvider.js b/app/scripts/createStandardProvider.js new file mode 100644 index 000000000..a5f9c5d03 --- /dev/null +++ b/app/scripts/createStandardProvider.js @@ -0,0 +1,92 @@ +class StandardProvider { + _isConnected + _provider + + 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) + }) + } + + _onClose () { + if (this._isConnected === undefined || this._isConnected) { + this._provider.emit('close', { + code: 1011, + reason: 'Network connection error', + }) + } + this._isConnected = false + } + + _onConnect () { + !this._isConnected && this._provider.emit('connect') + 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') { + this._provider.emit('notification', params.result) + } + }) + } + + /** + * Initiate an RPC method call + * + * @param {string} method - RPC method name to call + * @param {string[]} params - Array of RPC method parameters + * @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) => { + error = error || response.error + error ? reject(error) : resolve(response) + }) + } catch (error) { + reject(error) + } + }) + } +} + +/** + * Converts a legacy provider into an EIP-1193-compliant standard provider + * @param {Object} provider - Legacy provider to convert + * @returns {Object} Standard provider + */ +export default function createStandardProvider (provider) { + const standardProvider = new StandardProvider(provider) + const sendLegacy = provider.send + provider.send = (methodOrPayload, callbackOrArgs) => { + if (typeof methodOrPayload === 'string' && !callbackOrArgs || Array.isArray(callbackOrArgs)) { + return standardProvider.send(methodOrPayload, callbackOrArgs) + } + return sendLegacy.call(provider, methodOrPayload, callbackOrArgs) + } + return provider +} diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index ae5a375b0..c7f0c5669 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -5,6 +5,7 @@ const log = require('loglevel') const LocalMessageDuplexStream = require('post-message-stream') const setupDappAutoReload = require('./lib/auto-reload.js') const MetamaskInpageProvider = require('metamask-inpage-provider') +const createStandardProvider = require('./createStandardProvider').default let isEnabled = false let warned = false @@ -16,12 +17,6 @@ restoreContextAfterImports() log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn') -console.warn('ATTENTION: In an effort to improve user privacy, MetaMask ' + -'stopped exposing user accounts to dapps if "privacy mode" is enabled on ' + -'November 2nd, 2018. Dapps should now call provider.enable() in order to view and use ' + -'accounts. Please see https://bit.ly/2QQHXvF for complete information and up-to-date ' + -'example code.') - /** * Adds a postMessage listener for a specific message type * @@ -70,7 +65,10 @@ inpageProvider.enable = function ({ force } = {}) { return new Promise((resolve, reject) => { providerHandle = ({ data: { error, selectedAddress } }) => { if (typeof error !== 'undefined') { - reject(error) + reject({ + message: error, + code: 4001, + }) } else { window.removeEventListener('message', providerHandle) setTimeout(() => { @@ -155,7 +153,7 @@ const proxiedInpageProvider = new Proxy(inpageProvider, { deleteProperty: () => true, }) -window.ethereum = proxiedInpageProvider +window.ethereum = createStandardProvider(proxiedInpageProvider) // detect eth_requestAccounts and pipe to enable for now function detectAccountRequest (method) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 65cc2d3eb..b75f95d01 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -86,7 +86,7 @@ module.exports = class MetamaskController extends EventEmitter { this.createVaultMutex = new Mutex() // network store - this.networkController = new NetworkController(initState.NetworkController) + this.networkController = new NetworkController(initState.NetworkController, this.platform) // preferences controller this.preferencesController = new PreferencesController({ -- cgit v1.2.3