Merge pull request #1455 from MetaMask/networkController

Create a network controller to manage switching networks an updating t…
feature/default_network_editable
kumavis 8 years ago committed by GitHub
commit e8288ad4bf
  1. 2
      CHANGELOG.md
  2. 128
      app/scripts/controllers/network.js
  3. 7
      app/scripts/controllers/transactions.js
  4. 3
      app/scripts/first-time-state.js
  5. 62
      app/scripts/lib/config-manager.js
  6. 8
      app/scripts/lib/tx-utils.js
  7. 98
      app/scripts/metamask-controller.js
  8. 34
      app/scripts/migrations/014.js
  9. 1
      app/scripts/migrations/index.js
  10. 74
      test/unit/network-contoller-test.js
  11. 4
      test/unit/tx-controller-test.js
  12. 6
      test/unit/tx-utils-test.js

@ -2,6 +2,8 @@
## Current Master ## Current Master
- Now when switching networks the extension does not restart
## 3.7.0 2017-5-23 ## 3.7.0 2017-5-23
- Add Transaction Number (nonce) to transaction list. - Add Transaction Number (nonce) to transaction list.

@ -0,0 +1,128 @@
const EventEmitter = require('events')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed')
const extend = require('xtend')
const EthQuery = require('eth-query')
const RPC_ADDRESS_LIST = require('../config.js').network
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
module.exports = class NetworkController extends EventEmitter {
constructor (config) {
super()
this.networkStore = new ObservableStore('loading')
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
this.providerStore = new ObservableStore(config.provider)
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
this._providerListeners = {}
this.on('networkDidChange', this.lookupNetwork)
this.providerStore.subscribe((state) => this.switchNetwork({rpcUrl: state.rpcTarget}))
}
get provider () {
return this._proxy
}
set provider (provider) {
this._provider = provider
}
initializeProvider (opts) {
this.providerInit = opts
this._provider = MetaMaskProvider(opts)
this._proxy = new Proxy(this._provider, {
get: (obj, name) => {
if (name === 'on') return this._on.bind(this)
return this._provider[name]
},
set: (obj, name, value) => {
this._provider[name] = value
},
})
this.provider.on('block', this._logBlock.bind(this))
this.provider.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this.provider)
this.lookupNetwork()
return this.provider
}
switchNetwork (providerInit) {
this.setNetworkState('loading')
const newInit = extend(this.providerInit, providerInit)
this.providerInit = newInit
this._provider.removeAllListeners()
this.provider = MetaMaskProvider(newInit)
// apply the listners created by other controllers
Object.keys(this._providerListeners).forEach((key) => {
this._providerListeners[key].forEach((handler) => this._provider.addListener(key, handler))
})
this.emit('networkDidChange')
}
verifyNetwork () {
// Check network when restoring connectivity:
if (this.isNetworkLoading()) this.lookupNetwork()
}
getNetworkState () {
return this.networkStore.getState()
}
setNetworkState (network) {
return this.networkStore.putState(network)
}
isNetworkLoading () {
return this.getNetworkState() === 'loading'
}
lookupNetwork () {
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading')
log.info('web3.getNetwork returned ' + network)
this.setNetworkState(network)
})
}
setRpcTarget (rpcUrl) {
this.providerStore.updateState({
type: 'rpc',
rpcTarget: rpcUrl,
})
}
getCurrentRpcAddress () {
const provider = this.getProviderConfig()
if (!provider) return null
return this.getRpcAddressForType(provider.type)
}
setProviderType (type) {
if (type === this.getProviderConfig().type) return
const rpcTarget = this.getRpcAddressForType(type)
this.providerStore.updateState({type, rpcTarget})
}
getProviderConfig () {
return this.providerStore.getState()
}
getRpcAddressForType (type, provider = this.getProviderConfig()) {
if (RPC_ADDRESS_LIST[type]) return RPC_ADDRESS_LIST[type]
return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC
}
_logBlock (block) {
log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
this.verifyNetwork()
}
_on (event, handler) {
if (!this._providerListeners[event]) this._providerListeners[event] = []
this._providerListeners[event].push(handler)
this._provider.on(event, handler)
}
}

@ -4,7 +4,6 @@ const extend = require('xtend')
const Semaphore = require('semaphore') const Semaphore = require('semaphore')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EthQuery = require('eth-query')
const TxProviderUtil = require('../lib/tx-utils') const TxProviderUtil = require('../lib/tx-utils')
const createId = require('../lib/random-id') const createId = require('../lib/random-id')
const denodeify = require('denodeify') const denodeify = require('denodeify')
@ -24,8 +23,8 @@ module.exports = class TransactionController extends EventEmitter {
this.txHistoryLimit = opts.txHistoryLimit this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider this.provider = opts.provider
this.blockTracker = opts.blockTracker this.blockTracker = opts.blockTracker
this.query = new EthQuery(this.provider) this.query = opts.ethQuery
this.txProviderUtils = new TxProviderUtil(this.provider) this.txProviderUtils = new TxProviderUtil(this.query)
this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) this.blockTracker.on('block', this.checkForTxInBlock.bind(this))
this.signEthTx = opts.signTransaction this.signEthTx = opts.signTransaction
this.nonceLock = Semaphore(1) this.nonceLock = Semaphore(1)
@ -44,7 +43,7 @@ module.exports = class TransactionController extends EventEmitter {
} }
getNetwork () { getNetwork () {
return this.networkStore.getState().network return this.networkStore.getState()
} }
getSelectedAddress () { getSelectedAddress () {

@ -3,7 +3,8 @@
// //
module.exports = { module.exports = {
config: { config: {},
NetworkController: {
provider: { provider: {
type: 'rinkeby', type: 'rinkeby',
}, },

@ -1,6 +1,7 @@
const MetamaskConfig = require('../config.js')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const normalize = require('eth-sig-util').normalize const normalize = require('eth-sig-util').normalize
const MetamaskConfig = require('../config.js')
const MAINNET_RPC = MetamaskConfig.network.mainnet const MAINNET_RPC = MetamaskConfig.network.mainnet
const ROPSTEN_RPC = MetamaskConfig.network.ropsten const ROPSTEN_RPC = MetamaskConfig.network.ropsten
@ -33,36 +34,6 @@ ConfigManager.prototype.getConfig = function () {
return data.config return data.config
} }
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
var config = this.getConfig()
config.provider = {
type: 'rpc',
rpcTarget: rpcUrl,
}
this.setConfig(config)
}
ConfigManager.prototype.setProviderType = function (type) {
var config = this.getConfig()
config.provider = {
type: type,
}
this.setConfig(config)
}
ConfigManager.prototype.useEtherscanProvider = function () {
var config = this.getConfig()
config.provider = {
type: 'etherscan',
}
this.setConfig(config)
}
ConfigManager.prototype.getProvider = function () {
var config = this.getConfig()
return config.provider
}
ConfigManager.prototype.setData = function (data) { ConfigManager.prototype.setData = function (data) {
this.store.putState(data) this.store.putState(data)
} }
@ -136,6 +107,35 @@ ConfigManager.prototype.getSeedWords = function () {
var data = this.getData() var data = this.getData()
return data.seedWords return data.seedWords
} }
ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
var config = this.getConfig()
config.provider = {
type: 'rpc',
rpcTarget: rpcUrl,
}
this.setConfig(config)
}
ConfigManager.prototype.setProviderType = function (type) {
var config = this.getConfig()
config.provider = {
type: type,
}
this.setConfig(config)
}
ConfigManager.prototype.useEtherscanProvider = function () {
var config = this.getConfig()
config.provider = {
type: 'etherscan',
}
this.setConfig(config)
}
ConfigManager.prototype.getProvider = function () {
var config = this.getConfig()
return config.provider
}
ConfigManager.prototype.getCurrentRpcAddress = function () { ConfigManager.prototype.getCurrentRpcAddress = function () {
var provider = this.getProvider() var provider = this.getProvider()

@ -1,5 +1,4 @@
const async = require('async') const async = require('async')
const EthQuery = require('eth-query')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx') const Transaction = require('ethereumjs-tx')
const normalize = require('eth-sig-util').normalize const normalize = require('eth-sig-util').normalize
@ -7,15 +6,14 @@ const BN = ethUtil.BN
/* /*
tx-utils are utility methods for Transaction manager tx-utils are utility methods for Transaction manager
its passed a provider and that is passed to ethquery its passed ethquery
and used to do things like calculate gas of a tx. and used to do things like calculate gas of a tx.
*/ */
module.exports = class txProviderUtils { module.exports = class txProviderUtils {
constructor (provider) { constructor (ethQuery) {
this.provider = provider this.query = ethQuery
this.query = new EthQuery(provider)
} }
analyzeGasUsage (txMeta, cb) { analyzeGasUsage (txMeta, cb) {

@ -7,9 +7,9 @@ const ObservableStore = require('obs-store')
const EthStore = require('./lib/eth-store') const EthStore = require('./lib/eth-store')
const EthQuery = require('eth-query') const EthQuery = require('eth-query')
const streamIntoProvider = require('web3-stream-provider/handler') const streamIntoProvider = require('web3-stream-provider/handler')
const MetaMaskProvider = require('web3-provider-engine/zero.js')
const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
const KeyringController = require('./keyring-controller') const KeyringController = require('./keyring-controller')
const NetworkController = require('./controllers/network')
const PreferencesController = require('./controllers/preferences') const PreferencesController = require('./controllers/preferences')
const CurrencyController = require('./controllers/currency') const CurrencyController = require('./controllers/currency')
const NoticeController = require('./notice-controller') const NoticeController = require('./notice-controller')
@ -40,8 +40,8 @@ module.exports = class MetamaskController extends EventEmitter {
this.store = new ObservableStore(initState) this.store = new ObservableStore(initState)
// network store // network store
this.networkStore = new ObservableStore({ network: 'loading' })
this.networkController = new NetworkController(initState.NetworkController)
// config manager // config manager
this.configManager = new ConfigManager({ this.configManager = new ConfigManager({
store: this.store, store: this.store,
@ -61,8 +61,6 @@ module.exports = class MetamaskController extends EventEmitter {
// rpc provider // rpc provider
this.provider = this.initializeProvider() this.provider = this.initializeProvider()
this.provider.on('block', this.logBlock.bind(this))
this.provider.on('error', this.verifyNetwork.bind(this))
// eth data query tools // eth data query tools
this.ethQuery = new EthQuery(this.provider) this.ethQuery = new EthQuery(this.provider)
@ -75,7 +73,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.keyringController = new KeyringController({ this.keyringController = new KeyringController({
initState: initState.KeyringController, initState: initState.KeyringController,
ethStore: this.ethStore, ethStore: this.ethStore,
getNetwork: this.getNetworkState.bind(this), getNetwork: this.networkController.getNetworkState.bind(this.networkController),
}) })
this.keyringController.on('newAccount', (address) => { this.keyringController.on('newAccount', (address) => {
this.preferencesController.setSelectedAddress(address) this.preferencesController.setSelectedAddress(address)
@ -92,13 +90,14 @@ module.exports = class MetamaskController extends EventEmitter {
// tx mgmt // tx mgmt
this.txController = new TransactionController({ this.txController = new TransactionController({
initState: initState.TransactionController || initState.TransactionManager, initState: initState.TransactionController || initState.TransactionManager,
networkStore: this.networkStore, networkStore: this.networkController.networkStore,
preferencesStore: this.preferencesController.store, preferencesStore: this.preferencesController.store,
txHistoryLimit: 40, txHistoryLimit: 40,
getNetwork: this.getNetworkState.bind(this), getNetwork: this.networkController.getNetworkState.bind(this),
signTransaction: this.keyringController.signTransaction.bind(this.keyringController), signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider, provider: this.provider,
blockTracker: this.provider, blockTracker: this.provider,
ethQuery: this.ethQuery,
}) })
// notices // notices
@ -113,7 +112,7 @@ module.exports = class MetamaskController extends EventEmitter {
initState: initState.ShapeShiftController, initState: initState.ShapeShiftController,
}) })
this.lookupNetwork() this.networkController.lookupNetwork()
this.messageManager = new MessageManager() this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager() this.personalMessageManager = new PersonalMessageManager()
this.publicConfigStore = this.initPublicConfigStore() this.publicConfigStore = this.initPublicConfigStore()
@ -140,9 +139,12 @@ module.exports = class MetamaskController extends EventEmitter {
this.shapeshiftController.store.subscribe((state) => { this.shapeshiftController.store.subscribe((state) => {
this.store.updateState({ ShapeShiftController: state }) this.store.updateState({ ShapeShiftController: state })
}) })
this.networkController.store.subscribe((state) => {
this.store.updateState({ NetworkController: state })
})
// manual mem state subscriptions // manual mem state subscriptions
this.networkStore.subscribe(this.sendUpdate.bind(this)) this.networkController.store.subscribe(this.sendUpdate.bind(this))
this.ethStore.subscribe(this.sendUpdate.bind(this)) this.ethStore.subscribe(this.sendUpdate.bind(this))
this.txController.memStore.subscribe(this.sendUpdate.bind(this)) this.txController.memStore.subscribe(this.sendUpdate.bind(this))
this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
@ -160,12 +162,12 @@ module.exports = class MetamaskController extends EventEmitter {
// //
initializeProvider () { initializeProvider () {
const provider = MetaMaskProvider({ return this.networkController.initializeProvider({
static: { static: {
eth_syncing: false, eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`, web3_clientVersion: `MetaMask/v${version}`,
}, },
rpcUrl: this.configManager.getCurrentRpcAddress(), rpcUrl: this.networkController.getCurrentRpcAddress(),
// account mgmt // account mgmt
getAccounts: (cb) => { getAccounts: (cb) => {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked const isUnlocked = this.keyringController.memStore.getState().isUnlocked
@ -185,7 +187,6 @@ module.exports = class MetamaskController extends EventEmitter {
// new style msg signing // new style msg signing
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
}) })
return provider
} }
initPublicConfigStore () { initPublicConfigStore () {
@ -221,7 +222,7 @@ module.exports = class MetamaskController extends EventEmitter {
{ {
isInitialized, isInitialized,
}, },
this.networkStore.getState(), this.networkController.store.getState(),
this.ethStore.getState(), this.ethStore.getState(),
this.txController.memStore.getState(), this.txController.memStore.getState(),
this.messageManager.memStore.getState(), this.messageManager.memStore.getState(),
@ -255,8 +256,7 @@ module.exports = class MetamaskController extends EventEmitter {
return { return {
// etc // etc
getState: (cb) => cb(null, this.getState()), getState: (cb) => cb(null, this.getState()),
setProviderType: this.setProviderType.bind(this), setProviderType: this.networkController.setProviderType.bind(this.networkController),
useEtherscanProvider: this.useEtherscanProvider.bind(this),
setCurrentCurrency: this.setCurrentCurrency.bind(this), setCurrentCurrency: this.setCurrentCurrency.bind(this),
markAccountsFound: this.markAccountsFound.bind(this), markAccountsFound: this.markAccountsFound.bind(this),
// coinbase // coinbase
@ -590,10 +590,6 @@ module.exports = class MetamaskController extends EventEmitter {
// //
// Log blocks // Log blocks
logBlock (block) {
log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`)
this.verifyNetwork()
}
setCurrentCurrency (currencyCode, cb) { setCurrentCurrency (currencyCode, cb) {
try { try {
@ -612,7 +608,7 @@ module.exports = class MetamaskController extends EventEmitter {
buyEth (address, amount) { buyEth (address, amount) {
if (!amount) amount = '5' if (!amount) amount = '5'
const network = this.getNetworkState() const network = this.networkController.getNetworkState()
const url = getBuyEthUrl({ network, address, amount }) const url = getBuyEthUrl({ network, address, amount })
if (url) this.platform.openWindow({ url }) if (url) this.platform.openWindow({ url })
} }
@ -620,69 +616,21 @@ module.exports = class MetamaskController extends EventEmitter {
createShapeShiftTx (depositAddress, depositType) { createShapeShiftTx (depositAddress, depositType) {
this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) this.shapeshiftController.createShapeShiftTx(depositAddress, depositType)
} }
// network
//
// network
//
verifyNetwork () {
// Check network when restoring connectivity:
if (this.isNetworkLoading()) this.lookupNetwork()
}
setDefaultRpc () { setDefaultRpc () {
this.configManager.setRpcTarget('http://localhost:8545') this.networkController.setRpcTarget('http://localhost:8545')
this.platform.reload()
this.lookupNetwork()
return Promise.resolve('http://localhost:8545') return Promise.resolve('http://localhost:8545')
} }
setCustomRpc (rpcTarget, rpcList) { setCustomRpc (rpcTarget, rpcList) {
this.configManager.setRpcTarget(rpcTarget) this.networkController.setRpcTarget(rpcTarget)
return this.preferencesController.updateFrequentRpcList(rpcTarget)
.then(() => {
this.platform.reload()
this.lookupNetwork()
return Promise.resolve(rpcTarget)
})
}
setProviderType (type) { return this.preferencesController.updateFrequentRpcList(rpcTarget)
this.configManager.setProviderType(type) .then(() => {
this.platform.reload() return Promise.resolve(rpcTarget)
this.lookupNetwork()
}
useEtherscanProvider () {
this.configManager.useEtherscanProvider()
this.platform.reload()
}
getNetworkState () {
return this.networkStore.getState().network
}
setNetworkState (network) {
return this.networkStore.updateState({ network })
}
isNetworkLoading () {
return this.getNetworkState() === 'loading'
}
lookupNetwork (err) {
if (err) {
this.setNetworkState('loading')
}
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) {
this.setNetworkState('loading')
return
}
log.info('web3.getNetwork returned ' + network)
this.setNetworkState(network)
}) })
} }
} }

@ -0,0 +1,34 @@
const version = 14
/*
This migration removes provider from config and moves it too NetworkController.
*/
const clone = require('clone')
module.exports = {
version,
migrate: function (originalVersionedData) {
const versionedData = clone(originalVersionedData)
versionedData.meta.version = version
try {
const state = versionedData.data
const newState = transformState(state)
versionedData.data = newState
} catch (err) {
console.warn(`MetaMask Migration #${version}` + err.stack)
}
return Promise.resolve(versionedData)
},
}
function transformState (state) {
const newState = state
newState.NetworkController = {}
newState.NetworkController.provider = newState.config.provider
delete newState.config.provider
return newState
}

@ -24,4 +24,5 @@ module.exports = [
require('./011'), require('./011'),
require('./012'), require('./012'),
require('./013'), require('./013'),
require('./014'),
] ]

@ -0,0 +1,74 @@
const EventEmitter = require('events')
const assert = require('assert')
const NetworkController = require('../../app/scripts/controllers/network')
describe('# Network Controller', function () {
let networkController
beforeEach(function () {
networkController = new NetworkController({
provider: {
type: 'rinkeby',
},
})
// stub out provider
networkController._provider = new EventEmitter()
networkController.providerInit = {
getAccounts: () => {},
}
networkController.ethQuery = new Proxy({}, {
get: (obj, name) => {
return () => {}
},
})
})
describe('network', function () {
describe('#provider', function() {
it('provider should be updatable without reassignment', function () {
networkController.initializeProvider(networkController.providerInit)
const provider = networkController.provider
networkController._provider = {test: true}
assert.ok(provider.test)
})
})
describe('#getNetworkState', function () {
it('should return loading when new', function () {
let networkState = networkController.getNetworkState()
assert.equal(networkState, 'loading', 'network is loading')
})
})
describe('#setNetworkState', function () {
it('should update the network', function () {
networkController.setNetworkState(1)
let networkState = networkController.getNetworkState()
assert.equal(networkState, 1, 'network is 1')
})
})
describe('#getRpcAddressForType', function () {
it('should return the right rpc address', function () {
let rpcTarget = networkController.getRpcAddressForType('mainnet')
assert.equal(rpcTarget, 'https://mainnet.infura.io/metamask', 'returns the right rpcAddress')
})
})
describe('#setProviderType', function () {
it('should update provider.type', function () {
networkController.setProviderType('mainnet')
const type = networkController.getProviderConfig().type
assert.equal(type, 'mainnet', 'provider type is updated')
})
it('should set the network to loading', function () {
networkController.setProviderType('mainnet')
const loading = networkController.isNetworkLoading()
assert.ok(loading, 'network is loading')
})
it('should set the right rpcTarget', function () {
networkController.setProviderType('mainnet')
const rpcTarget = networkController.getProviderConfig().rpcTarget
assert.equal(rpcTarget, 'https://mainnet.infura.io/metamask', 'returns the right rpcAddress')
})
})
})
})

@ -2,6 +2,7 @@ const assert = require('assert')
const EventEmitter = require('events') const EventEmitter = require('events')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const EthTx = require('ethereumjs-tx') const EthTx = require('ethereumjs-tx')
const EthQuery = require('eth-query')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const clone = require('clone') const clone = require('clone')
const sinon = require('sinon') const sinon = require('sinon')
@ -16,9 +17,10 @@ describe('Transaction Controller', function () {
beforeEach(function () { beforeEach(function () {
txController = new TransactionController({ txController = new TransactionController({
networkStore: new ObservableStore({ network: currentNetworkId }), networkStore: new ObservableStore(currentNetworkId),
txHistoryLimit: 10, txHistoryLimit: 10,
blockTracker: new EventEmitter(), blockTracker: new EventEmitter(),
ethQuery: new EthQuery(new EventEmitter()),
signTransaction: (ethTx) => new Promise((resolve) => { signTransaction: (ethTx) => new Promise((resolve) => {
ethTx.sign(privKey) ethTx.sign(privKey)
resolve() resolve()

@ -9,7 +9,11 @@ describe('txUtils', function () {
let txUtils let txUtils
before(function () { before(function () {
txUtils = new TxUtils() txUtils = new TxUtils(new Proxy({}, {
get: (obj, name) => {
return () => {}
},
}))
}) })
describe('chain Id', function () { describe('chain Id', function () {

Loading…
Cancel
Save