diff options
Diffstat (limited to 'app/scripts')
-rw-r--r-- | app/scripts/controllers/network.js | 128 | ||||
-rw-r--r-- | app/scripts/controllers/transactions.js | 66 | ||||
-rw-r--r-- | app/scripts/first-time-state.js | 3 | ||||
-rw-r--r-- | app/scripts/lib/config-manager.js | 62 | ||||
-rw-r--r-- | app/scripts/lib/inpage-provider.js | 22 | ||||
-rw-r--r-- | app/scripts/lib/tx-utils.js | 8 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 98 | ||||
-rw-r--r-- | app/scripts/migrations/014.js | 34 | ||||
-rw-r--r-- | app/scripts/migrations/index.js | 1 |
9 files changed, 297 insertions, 125 deletions
diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js new file mode 100644 index 000000000..4fdd92921 --- /dev/null +++ b/app/scripts/controllers/network.js @@ -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) + } +} diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 21dd25b30..faccf1ab1 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -4,11 +4,14 @@ const extend = require('xtend') const Semaphore = require('semaphore') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') -const EthQuery = require('eth-query') const TxProviderUtil = require('../lib/tx-utils') const createId = require('../lib/random-id') +const denodeify = require('denodeify') -module.exports = class TransactionManager extends EventEmitter { +const RETRY_LIMIT = 200 +const RESUBMIT_INTERVAL = 10000 // Ten seconds + +module.exports = class TransactionController extends EventEmitter { constructor (opts) { super() this.store = new ObservableStore(extend({ @@ -20,8 +23,8 @@ module.exports = class TransactionManager extends EventEmitter { this.txHistoryLimit = opts.txHistoryLimit this.provider = opts.provider this.blockTracker = opts.blockTracker - this.query = new EthQuery(this.provider) - this.txProviderUtils = new TxProviderUtil(this.provider) + this.query = opts.ethQuery + this.txProviderUtils = new TxProviderUtil(this.query) this.blockTracker.on('block', this.checkForTxInBlock.bind(this)) this.signEthTx = opts.signTransaction this.nonceLock = Semaphore(1) @@ -31,6 +34,8 @@ module.exports = class TransactionManager extends EventEmitter { this.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) + + this.continuallyResubmitPendingTxs() } getState () { @@ -38,7 +43,7 @@ module.exports = class TransactionManager extends EventEmitter { } getNetwork () { - return this.networkStore.getState().network + return this.networkStore.getState() } getSelectedAddress () { @@ -230,7 +235,11 @@ module.exports = class TransactionManager extends EventEmitter { }) } - publishTransaction (txId, rawTx, cb) { + publishTransaction (txId, rawTx, cb = warn) { + const txMeta = this.getTx(txId) + txMeta.rawTx = rawTx + this.updateTx(txMeta) + this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => { if (err) return cb(err) this.setTxHash(txId, txHash) @@ -353,7 +362,7 @@ module.exports = class TransactionManager extends EventEmitter { message: 'There was a problem loading this transaction.', } this.updateTx(txMeta) - return console.error(err) + return log.error(err) } if (txParams.blockNumber) { this.setTxStatusConfirmed(txId) @@ -379,6 +388,7 @@ module.exports = class TransactionManager extends EventEmitter { this.emit(`${txMeta.id}:${status}`, txId) if (status === 'submitted' || status === 'rejected') { this.emit(`${txMeta.id}:finished`, txMeta) + } this.updateTx(txMeta) this.emit('updateBadge') @@ -398,7 +408,47 @@ module.exports = class TransactionManager extends EventEmitter { }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } + + continuallyResubmitPendingTxs () { + const pending = this.getTxsByMetaData('status', 'submitted') + const resubmit = denodeify(this.resubmitTx.bind(this)) + Promise.all(pending.map(txMeta => resubmit(txMeta))) + .catch((reason) => { + log.info('Problem resubmitting tx', reason) + }) + .then(() => { + global.setTimeout(() => { + this.continuallyResubmitPendingTxs() + }, RESUBMIT_INTERVAL) + }) + } + + resubmitTx (txMeta, cb) { + // Increment a try counter. + if (!('retryCount' in txMeta)) { + txMeta.retryCount = 0 + } + + // Only auto-submit already-signed txs: + if (!('rawTx' in txMeta)) { + return cb() + } + + if (txMeta.retryCount > RETRY_LIMIT) { + txMeta.err = { + isWarning: true, + message: 'Gave up submitting tx.', + } + this.updateTx(txMeta) + return log.error(txMeta.err.message) + } + + txMeta.retryCount++ + const rawTx = txMeta.rawTx + this.txProviderUtils.publishTransaction(rawTx, cb) + } + } -const warn = () => console.warn('warn was used no cb provided') +const warn = () => log.warn('warn was used no cb provided') diff --git a/app/scripts/first-time-state.js b/app/scripts/first-time-state.js index 29ec1d8d3..dc7788311 100644 --- a/app/scripts/first-time-state.js +++ b/app/scripts/first-time-state.js @@ -3,7 +3,8 @@ // module.exports = { - config: { + config: {}, + NetworkController: { provider: { type: 'rinkeby', }, diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js index d77cd2126..9c0dffe9c 100644 --- a/app/scripts/lib/config-manager.js +++ b/app/scripts/lib/config-manager.js @@ -1,6 +1,7 @@ -const MetamaskConfig = require('../config.js') const ethUtil = require('ethereumjs-util') const normalize = require('eth-sig-util').normalize +const MetamaskConfig = require('../config.js') + const MAINNET_RPC = MetamaskConfig.network.mainnet const ROPSTEN_RPC = MetamaskConfig.network.ropsten @@ -33,36 +34,6 @@ ConfigManager.prototype.getConfig = function () { 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) { this.store.putState(data) } @@ -136,6 +107,35 @@ ConfigManager.prototype.getSeedWords = function () { var data = this.getData() 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 () { var provider = this.getProvider() diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index e5e398e24..e54f547bd 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,5 +1,7 @@ const pipe = require('pump') -const StreamProvider = require('web3-stream-provider') +const ProviderEngine = require('web3-provider-engine') +const FilterSubprovider = require('web3-provider-engine/subproviders/filters') +const StreamSubprovider = require('web3-provider-engine/subproviders/stream') const LocalStorageStore = require('obs-store') const ObjectMultiplex = require('./obj-multiplex') const createRandomId = require('./random-id') @@ -27,14 +29,24 @@ function MetamaskInpageProvider (connectionStream) { ) // connect to async provider - const asyncProvider = self.asyncProvider = new StreamProvider() + const engine = new ProviderEngine() + + const filterSubprovider = new FilterSubprovider() + engine.addProvider(filterSubprovider) + + const streamSubprovider = new StreamSubprovider() + engine.addProvider(streamSubprovider) + pipe( - asyncProvider, + streamSubprovider, multiStream.createStream('provider'), - asyncProvider, + streamSubprovider, (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) + // start polling + engine.start() + self.idMap = {} // handle sendAsync requests via asyncProvider self.sendAsync = function (payload, cb) { @@ -46,7 +58,7 @@ function MetamaskInpageProvider (connectionStream) { return message }) // forward to asyncProvider - asyncProvider.sendAsync(request, function (err, res) { + engine.sendAsync(request, function (err, res) { if (err) return cb(err) // transform messages to original ids eachJsonMessage(res, (message) => { diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 084ca3721..76b311653 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -1,5 +1,4 @@ const async = require('async') -const EthQuery = require('eth-query') const ethUtil = require('ethereumjs-util') const Transaction = require('ethereumjs-tx') const normalize = require('eth-sig-util').normalize @@ -7,15 +6,14 @@ const BN = ethUtil.BN /* 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. */ module.exports = class txProviderUtils { - constructor (provider) { - this.provider = provider - this.query = new EthQuery(provider) + constructor (ethQuery) { + this.query = ethQuery } analyzeGasUsage (txMeta, cb) { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index f18da9033..a7eb3d056 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -7,9 +7,9 @@ const ObservableStore = require('obs-store') const EthStore = require('./lib/eth-store') const EthQuery = require('eth-query') const streamIntoProvider = require('web3-stream-provider/handler') -const MetaMaskProvider = require('web3-provider-engine/zero.js') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex const KeyringController = require('./keyring-controller') +const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const CurrencyController = require('./controllers/currency') const NoticeController = require('./notice-controller') @@ -40,8 +40,8 @@ module.exports = class MetamaskController extends EventEmitter { this.store = new ObservableStore(initState) // network store - this.networkStore = new ObservableStore({ network: 'loading' }) + this.networkController = new NetworkController(initState.NetworkController) // config manager this.configManager = new ConfigManager({ store: this.store, @@ -61,8 +61,6 @@ module.exports = class MetamaskController extends EventEmitter { // rpc provider this.provider = this.initializeProvider() - this.provider.on('block', this.logBlock.bind(this)) - this.provider.on('error', this.verifyNetwork.bind(this)) // eth data query tools this.ethQuery = new EthQuery(this.provider) @@ -75,7 +73,7 @@ module.exports = class MetamaskController extends EventEmitter { this.keyringController = new KeyringController({ initState: initState.KeyringController, ethStore: this.ethStore, - getNetwork: this.getNetworkState.bind(this), + getNetwork: this.networkController.getNetworkState.bind(this.networkController), }) this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) @@ -92,13 +90,14 @@ module.exports = class MetamaskController extends EventEmitter { // tx mgmt this.txController = new TransactionController({ initState: initState.TransactionController || initState.TransactionManager, - networkStore: this.networkStore, + networkStore: this.networkController.networkStore, preferencesStore: this.preferencesController.store, txHistoryLimit: 40, - getNetwork: this.getNetworkState.bind(this), + getNetwork: this.networkController.getNetworkState.bind(this), signTransaction: this.keyringController.signTransaction.bind(this.keyringController), provider: this.provider, blockTracker: this.provider, + ethQuery: this.ethQuery, }) // notices @@ -113,7 +112,7 @@ module.exports = class MetamaskController extends EventEmitter { initState: initState.ShapeShiftController, }) - this.lookupNetwork() + this.networkController.lookupNetwork() this.messageManager = new MessageManager() this.personalMessageManager = new PersonalMessageManager() this.publicConfigStore = this.initPublicConfigStore() @@ -140,9 +139,12 @@ module.exports = class MetamaskController extends EventEmitter { this.shapeshiftController.store.subscribe((state) => { this.store.updateState({ ShapeShiftController: state }) }) + this.networkController.store.subscribe((state) => { + this.store.updateState({ NetworkController: state }) + }) // 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.txController.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 () { - const provider = MetaMaskProvider({ + return this.networkController.initializeProvider({ static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, - rpcUrl: this.configManager.getCurrentRpcAddress(), + rpcUrl: this.networkController.getCurrentRpcAddress(), // account mgmt getAccounts: (cb) => { const isUnlocked = this.keyringController.memStore.getState().isUnlocked @@ -185,7 +187,6 @@ module.exports = class MetamaskController extends EventEmitter { // new style msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), }) - return provider } initPublicConfigStore () { @@ -221,7 +222,7 @@ module.exports = class MetamaskController extends EventEmitter { { isInitialized, }, - this.networkStore.getState(), + this.networkController.store.getState(), this.ethStore.getState(), this.txController.memStore.getState(), this.messageManager.memStore.getState(), @@ -255,8 +256,7 @@ module.exports = class MetamaskController extends EventEmitter { return { // etc getState: (cb) => cb(null, this.getState()), - setProviderType: this.setProviderType.bind(this), - useEtherscanProvider: this.useEtherscanProvider.bind(this), + setProviderType: this.networkController.setProviderType.bind(this.networkController), setCurrentCurrency: this.setCurrentCurrency.bind(this), markAccountsFound: this.markAccountsFound.bind(this), // coinbase @@ -590,10 +590,6 @@ module.exports = class MetamaskController extends EventEmitter { // // Log blocks - logBlock (block) { - log.info(`BLOCK CHANGED: #${block.number.toString('hex')} 0x${block.hash.toString('hex')}`) - this.verifyNetwork() - } setCurrentCurrency (currencyCode, cb) { try { @@ -612,7 +608,7 @@ module.exports = class MetamaskController extends EventEmitter { buyEth (address, amount) { if (!amount) amount = '5' - const network = this.getNetworkState() + const network = this.networkController.getNetworkState() const url = getBuyEthUrl({ network, address, amount }) if (url) this.platform.openWindow({ url }) } @@ -620,69 +616,21 @@ module.exports = class MetamaskController extends EventEmitter { createShapeShiftTx (depositAddress, depositType) { this.shapeshiftController.createShapeShiftTx(depositAddress, depositType) } - - // - // network - // - - verifyNetwork () { - // Check network when restoring connectivity: - if (this.isNetworkLoading()) this.lookupNetwork() - } +// network setDefaultRpc () { - this.configManager.setRpcTarget('http://localhost:8545') - this.platform.reload() - this.lookupNetwork() + this.networkController.setRpcTarget('http://localhost:8545') return Promise.resolve('http://localhost:8545') } setCustomRpc (rpcTarget, rpcList) { - this.configManager.setRpcTarget(rpcTarget) - return this.preferencesController.updateFrequentRpcList(rpcTarget) - .then(() => { - this.platform.reload() - this.lookupNetwork() - return Promise.resolve(rpcTarget) - }) - } + this.networkController.setRpcTarget(rpcTarget) - setProviderType (type) { - this.configManager.setProviderType(type) - this.platform.reload() - 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) + return this.preferencesController.updateFrequentRpcList(rpcTarget) + .then(() => { + return Promise.resolve(rpcTarget) }) } + } diff --git a/app/scripts/migrations/014.js b/app/scripts/migrations/014.js new file mode 100644 index 000000000..0fe92125b --- /dev/null +++ b/app/scripts/migrations/014.js @@ -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 +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 3a95cf88e..fb1ad7863 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -24,4 +24,5 @@ module.exports = [ require('./011'), require('./012'), require('./013'), + require('./014'), ] |