From 088d7930e0895ef1802823c5fc843dd1c19b9661 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 16 May 2018 20:46:34 -0700 Subject: [PATCH] network - create provider and block-tracker via json-rpc-engine --- .../controllers/network/createInfuraClient.js | 34 +++++ .../network/createJsonRpcClient.js | 35 ++++++ .../network/createLocalhostClient.js | 33 +++++ .../network/createMetamaskMiddleware.js | 43 +++++++ app/scripts/controllers/network/network.js | 116 ++++++++++-------- app/scripts/controllers/transactions/index.js | 1 + .../controllers/transactions/nonce-tracker.js | 18 +-- app/scripts/metamask-controller.js | 7 +- package.json | 4 +- test/unit/nonce-tracker-test.js | 8 +- 10 files changed, 224 insertions(+), 75 deletions(-) create mode 100644 app/scripts/controllers/network/createInfuraClient.js create mode 100644 app/scripts/controllers/network/createJsonRpcClient.js create mode 100644 app/scripts/controllers/network/createLocalhostClient.js create mode 100644 app/scripts/controllers/network/createMetamaskMiddleware.js diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js new file mode 100644 index 000000000..e346f4bcb --- /dev/null +++ b/app/scripts/controllers/network/createInfuraClient.js @@ -0,0 +1,34 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-cache') +const BlockTracker = require('eth-block-tracker') + +module.exports = createInfuraClient + +function createInfuraClient({ network }) { + const infuraMiddleware = createInfuraMiddleware({ network }) + const blockProvider = providerFromMiddleware(infuraMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider }) + + const networkMiddleware = mergeMiddleware([ + createBlockRefMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + infuraMiddleware, + ]) + return { networkMiddleware, blockTracker } +} + +// inspect if response contains a block ref higher than our latest block +const futureBlockRefRequests = ['eth_getTransactionByHash', 'eth_getTransactionReceipt'] +function createBlockTrackerInspectorMiddleware ({ blockTracker }) { + return createAsyncMiddleware(async (req, res, next) => { + if (!futureBlockRefRequests.includes(req.method)) return next() + await next() + const blockNumber = Number.parseInt(res.result.blockNumber, 16) + const currentBlockNumber = Number.parseInt(blockTracker.getCurrentBlock(), 16) + if (blockNumber > currentBlockNumber) await blockTracker.checkForLatestBlock() + }) +} diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js new file mode 100644 index 000000000..5a8e85c23 --- /dev/null +++ b/app/scripts/controllers/network/createJsonRpcClient.js @@ -0,0 +1,35 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-cache') +const BlockTracker = require('eth-block-tracker') + +module.exports = createJsonRpcClient + +function createJsonRpcClient({ rpcUrl }) { + const fetchMiddleware = createFetchMiddleware({ rpcUrl }) + const blockProvider = providerFromMiddleware(fetchMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider }) + + const networkMiddleware = mergeMiddleware([ + createBlockRefMiddleware({ blockTracker }), + createBlockCacheMiddleware({ blockTracker }), + createInflightMiddleware(), + createBlockTrackerInspectorMiddleware({ blockTracker }), + fetchMiddleware, + ]) + return { networkMiddleware, blockTracker } +} + +// inspect if response contains a block ref higher than our latest block +const futureBlockRefRequests = ['eth_getTransactionByHash', 'eth_getTransactionReceipt'] +function createBlockTrackerInspectorMiddleware ({ blockTracker }) { + return createAsyncMiddleware(async (req, res, next) => { + if (!futureBlockRefRequests.includes(req.method)) return next() + await next() + const blockNumber = Number.parseInt(res.result.blockNumber, 16) + const currentBlockNumber = Number.parseInt(blockTracker.getCurrentBlock(), 16) + if (blockNumber > currentBlockNumber) await blockTracker.checkForLatestBlock() + }) +} diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js new file mode 100644 index 000000000..404415532 --- /dev/null +++ b/app/scripts/controllers/network/createLocalhostClient.js @@ -0,0 +1,33 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const createFetchMiddleware = require('eth-json-rpc-middleware/fetch') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-ref') +const createBlockRefMiddleware = require('eth-json-rpc-middleware/block-cache') +const BlockTracker = require('eth-block-tracker') + +module.exports = createLocalhostClient + +function createLocalhostClient() { + const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' }) + const blockProvider = providerFromMiddleware(fetchMiddleware) + const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 }) + + const networkMiddleware = mergeMiddleware([ + createBlockRefMiddleware({ blockTracker }), + createBlockTrackerInspectorMiddleware({ blockTracker }), + fetchMiddleware, + ]) + return { networkMiddleware, blockTracker } +} + +// inspect if response contains a block ref higher than our latest block +const futureBlockRefRequests = ['eth_getTransactionByHash', 'eth_getTransactionReceipt'] +function createBlockTrackerInspectorMiddleware ({ blockTracker }) { + return createAsyncMiddleware(async (req, res, next) => { + if (!futureBlockRefRequests.includes(req.method)) return next() + await next() + const blockNumber = Number.parseInt(res.result.blockNumber, 16) + const currentBlockNumber = Number.parseInt(blockTracker.getCurrentBlock(), 16) + if (blockNumber > currentBlockNumber) await blockTracker.checkForLatestBlock() + }) +} diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js new file mode 100644 index 000000000..1974c231d --- /dev/null +++ b/app/scripts/controllers/network/createMetamaskMiddleware.js @@ -0,0 +1,43 @@ +const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware') +const createScaffoldMiddleware = require('json-rpc-engine/src/scaffold') +const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware') +const createWalletSubprovider = require('eth-json-rpc-middleware/wallet') + +module.exports = createMetamaskMiddleware + +function createMetamaskMiddleware({ + version, + getAccounts, + processTransaction, + processEthSignMessage, + processTypedMessage, + processPersonalMessage, + getPendingNonce +}) { + const metamaskMiddleware = mergeMiddleware([ + createScaffoldMiddleware({ + // staticSubprovider + eth_syncing: false, + web3_clientVersion: `MetaMask/v${version}`, + }), + createWalletSubprovider({ + getAccounts, + processTransaction, + processEthSignMessage, + processTypedMessage, + processPersonalMessage, + }), + createPendingNonceMiddleware({ getPendingNonce }), + }) + return metamaskMiddleware +} + +function createPendingNonceMiddleware ({ getPendingNonce }) { + return createAsyncMiddleware(async (req, res, next) => { + if (req.method !== 'eth_getTransactionCount') return next() + const address = req.params[0] + const blockRef = req.params[1] + if (blockRef !== 'pending') return next() + req.result = await getPendingNonce(address) + }) +} diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js index 93fde7c57..c882c7d75 100644 --- a/app/scripts/controllers/network/network.js +++ b/app/scripts/controllers/network/network.js @@ -1,14 +1,17 @@ const assert = require('assert') const EventEmitter = require('events') -const createMetamaskProvider = require('web3-provider-engine/zero.js') -const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js') -const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider') const ObservableStore = require('obs-store') const ComposedStore = require('obs-store/lib/composed') const extend = require('xtend') const EthQuery = require('eth-query') -const createEventEmitterProxy = require('../../lib/events-proxy.js') const log = require('loglevel') +const createMetamaskMiddleware = require('./createMetamaskMiddleware') +const createInfuraClient = require('./createInfuraClient') +const createJsonRpcClient = require('./createJsonRpcClient') +const createLocalhostClient = require('./createLocalhostClient') +// const createEventEmitterProxy = require('../../lib/events-proxy.js') +const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy') + const { ROPSTEN, RINKEBY, @@ -38,21 +41,27 @@ module.exports = class NetworkController extends EventEmitter { this.providerStore = new ObservableStore(providerConfig) this.networkStore = new ObservableStore('loading') this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - // create event emitter proxy - this._proxy = createEventEmitterProxy() - this.on('networkDidChange', this.lookupNetwork) + // provider and block tracker + this._provider = null + this._blockTracker = null + // provider and block tracker proxies - because the network changes + this._providerProxy = null + this._blockTrackerProxy = null } - initializeProvider (_providerParams) { - this._baseProviderParams = _providerParams + initializeProvider (providerParams) { + this._baseProviderParams = providerParams const { type, rpcTarget } = this.providerStore.getState() this._configureProvider({ type, rpcTarget }) - this._proxy.on('block', this._logBlock.bind(this)) - this._proxy.on('error', this.verifyNetwork.bind(this)) - this.ethQuery = new EthQuery(this._proxy) this.lookupNetwork() - return this._proxy + } + + // return the proxies so the references will always be good + getProviderAndBlockTracker() { + const provider = this._providerProxy + const blockTracker = this._blockTracker + return { provider, blockTracker } } verifyNetwork () { @@ -74,10 +83,11 @@ module.exports = class NetworkController extends EventEmitter { lookupNetwork () { // Prevent firing when provider is not defined. - if (!this.ethQuery || !this.ethQuery.sendAsync) { - return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery') + if (!this._provider) { + return log.warn('NetworkController - lookupNetwork aborted due to missing provider') } - this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { + const ethQuery = new EthQuery(this._provider) + ethQuery.sendAsync({ method: 'net_version' }, (err, network) => { if (err) return this.setNetworkState('loading') log.info('web3.getNetwork returned ' + network) this.setNetworkState(network) @@ -123,7 +133,7 @@ module.exports = class NetworkController extends EventEmitter { this._configureInfuraProvider(opts) // other type-based rpc endpoints } else if (type === LOCALHOST) { - this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL }) + this._configureLocalhostProvider() // url-based rpc endpoints } else if (type === 'rpc'){ this._configureStandardProvider({ rpcUrl: rpcTarget }) @@ -133,47 +143,47 @@ module.exports = class NetworkController extends EventEmitter { } _configureInfuraProvider ({ type }) { - log.info('_configureInfuraProvider', type) - const infuraProvider = createInfuraProvider({ network: type }) - const infuraSubprovider = new SubproviderFromProvider(infuraProvider) - const providerParams = extend(this._baseProviderParams, { - engineParams: { - pollingInterval: 8000, - blockTrackerProvider: infuraProvider, - }, - dataSubprovider: infuraSubprovider, - }) - const provider = createMetamaskProvider(providerParams) - this._setProvider(provider) + log.info('NetworkController - configureInfuraProvider', type) + const networkClient = createInfuraClient({ network: type }) + this._setNetworkClient(networkClient) + } + + _configureLocalhostProvider () { + log.info('NetworkController - configureLocalhostProvider') + const networkClient = createLocalhostClient() + this._setNetworkClient(networkClient) } _configureStandardProvider ({ rpcUrl }) { - const providerParams = extend(this._baseProviderParams, { - rpcUrl, - engineParams: { - pollingInterval: 8000, - }, - }) - const provider = createMetamaskProvider(providerParams) - this._setProvider(provider) - } - - _setProvider (provider) { - // collect old block tracker events - const oldProvider = this._provider - let blockTrackerHandlers - if (oldProvider) { - // capture old block handlers - blockTrackerHandlers = oldProvider._blockTracker.proxyEventHandlers - // tear down - oldProvider.removeAllListeners() - oldProvider.stop() + log.info('NetworkController - configureStandardProvider', rpcUrl) + const networkClient = createJsonRpcClient({ rpcUrl }) + this._setNetworkClient(networkClient) + } + + _setNetworkClient ({ networkMiddleware, blockTracker }) { + const metamaskMiddleware = createMetamaskMiddleware(this._baseProviderParams) + const engine = new JsonRpcEngine() + engine.push(metamaskMiddleware) + engine.push(networkMiddleware) + const provider = providerFromEngine(engine) + this._setProviderAndBlockTracker({ provider, blockTracker }) + } + + _setProviderAndBlockTracker ({ provider, blockTracker }) { + // update or intialize proxies + if (this._providerProxy) { + this._providerProxy.setTarget(provider) + } else { + this._providerProxy = createSwappableProxy(provider) + } + if (this._blockTrackerProxy) { + this._blockTrackerProxy.setTarget(blockTracker) + } else { + this._blockTrackerProxy = createEventEmitterProxy(blockTracker) } - // override block tracler - provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) - // set as new provider + // set new provider and blockTracker this._provider = provider - this._proxy.setTarget(provider) + this._blockTracker = blockTracker } _logBlock (block) { diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 3886db104..cb3d28f1d 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -63,6 +63,7 @@ class TransactionController extends EventEmitter { this.store = this.txStateManager.store this.nonceTracker = new NonceTracker({ provider: this.provider, + blockTracker: this.blockTracker, getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager), }) diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js index f8cdc5523..490118c89 100644 --- a/app/scripts/controllers/transactions/nonce-tracker.js +++ b/app/scripts/controllers/transactions/nonce-tracker.js @@ -12,8 +12,9 @@ const Mutex = require('await-semaphore').Mutex */ class NonceTracker { - constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) { + constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) { this.provider = provider + this.blockTracker = blockTracker this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.getConfirmedTransactions = getConfirmedTransactions @@ -75,11 +76,10 @@ class NonceTracker { } async _getCurrentBlock () { - const blockTracker = this._getBlockTracker() - const currentBlock = blockTracker.getCurrentBlock() + const currentBlock = this.blockTracker.getCurrentBlock() if (currentBlock) return currentBlock return await new Promise((reject, resolve) => { - blockTracker.once('latest', resolve) + this.blockTracker.once('latest', resolve) }) } @@ -171,16 +171,6 @@ class NonceTracker { return { name: 'local', nonce: highest, details: { startPoint, highest } } } - - // this is a hotfix for the fact that the blockTracker will - // change when the network changes - - /** - @returns {Object} the current blockTracker - */ - _getBlockTracker () { - return this.provider._blockTracker - } } module.exports = NonceTracker diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a6b5d3453..7f4f2fc9b 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -103,8 +103,9 @@ module.exports = class MetamaskController extends EventEmitter { this.blacklistController.scheduleUpdates() // rpc provider - this.provider = this.initializeProvider() - this.blockTracker = this.provider._blockTracker + this.initializeProvider() + this.provider = this.networkController.getProviderAndBlockTracker().provider + this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker // token exchange rate tracker this.tokenRatesController = new TokenRatesController({ @@ -1033,7 +1034,7 @@ module.exports = class MetamaskController extends EventEmitter { // create filter polyfill middleware const filterMiddleware = createFilterMiddleware({ provider: this.provider, - blockTracker: this.provider._blockTracker, + blockTracker: this.blockTracker, }) engine.push(createOriginMiddleware({ origin })) diff --git a/package.json b/package.json index f6338c542..4580d0722 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "ensnare": "^1.0.0", "eslint-plugin-react": "^7.4.0", "eth-bin-to-ops": "^1.0.1", + "eth-block-tracker": "github:metamask/eth-block-tracker#acbcfda348c309ece0fb96fad78d4861d08f5a91", "eth-contract-metadata": "^1.1.5", "eth-hd-keyring": "^1.2.1", "eth-json-rpc-filters": "^1.2.6", @@ -129,7 +130,7 @@ "iframe-stream": "^3.0.0", "inject-css": "^0.1.1", "jazzicon": "^1.2.0", - "json-rpc-engine": "^3.6.1", + "json-rpc-engine": "^3.7.0", "json-rpc-middleware-stream": "^1.0.1", "lodash.debounce": "^4.0.8", "lodash.memoize": "^4.1.2", @@ -185,6 +186,7 @@ "shallow-copy": "0.0.1", "sw-controller": "^1.0.3", "sw-stream": "^2.0.2", + "swappable-obj-proxy": "^1.0.0", "textarea-caret": "^3.0.1", "through2": "^2.0.3", "valid-url": "^1.0.9", diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index cf26945d3..b9e6a5947 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -226,14 +226,14 @@ function generateNonceTrackerWith (pending, confirmed, providerStub = '0x0') { providerResultStub.result = providerStub const provider = { sendAsync: (_, cb) => { cb(undefined, providerResultStub) }, - _blockTracker: { - getCurrentBlock: () => '0x11b568', - }, + } + const blockTracker = { + getCurrentBlock: () => '0x11b568', } return new NonceTracker({ provider, + blockTracker, getPendingTransactions, getConfirmedTransactions, }) } -