diff options
author | kumavis <kumavis@users.noreply.github.com> | 2017-09-28 07:04:51 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-09-28 07:04:51 +0800 |
commit | 15b2823e546cfefd7e867f078b4385ddc6be3a0f (patch) | |
tree | 448a321264f3d648d6edee7e90c6f487bc31084b /app | |
parent | 734490c58c25587a247e48eea086880bcb6a14fe (diff) | |
parent | ecf909e140b2fc99afbd15f6f0882dd17e3ecb88 (diff) | |
download | tangerine-wallet-browser-15b2823e546cfefd7e867f078b4385ddc6be3a0f.tar tangerine-wallet-browser-15b2823e546cfefd7e867f078b4385ddc6be3a0f.tar.gz tangerine-wallet-browser-15b2823e546cfefd7e867f078b4385ddc6be3a0f.tar.bz2 tangerine-wallet-browser-15b2823e546cfefd7e867f078b4385ddc6be3a0f.tar.lz tangerine-wallet-browser-15b2823e546cfefd7e867f078b4385ddc6be3a0f.tar.xz tangerine-wallet-browser-15b2823e546cfefd7e867f078b4385ddc6be3a0f.tar.zst tangerine-wallet-browser-15b2823e546cfefd7e867f078b4385ddc6be3a0f.zip |
Merge branch 'master' into new-currency-test
Diffstat (limited to 'app')
-rw-r--r-- | app/manifest.json | 2 | ||||
-rw-r--r-- | app/scripts/background.js | 2 | ||||
-rw-r--r-- | app/scripts/contentscript.js | 11 | ||||
-rw-r--r-- | app/scripts/controllers/balance.js | 70 | ||||
-rw-r--r-- | app/scripts/controllers/computed-balances.js | 66 | ||||
-rw-r--r-- | app/scripts/controllers/network.js | 79 | ||||
-rw-r--r-- | app/scripts/controllers/preferences.js | 2 | ||||
-rw-r--r-- | app/scripts/controllers/transactions.js | 372 | ||||
-rw-r--r-- | app/scripts/keyring-controller.js | 595 | ||||
-rw-r--r-- | app/scripts/lib/account-tracker.js | 104 | ||||
-rw-r--r-- | app/scripts/lib/eth-store.js | 138 | ||||
-rw-r--r-- | app/scripts/lib/events-proxy.js | 31 | ||||
-rw-r--r-- | app/scripts/lib/pending-balance-calculator.js | 51 | ||||
-rw-r--r-- | app/scripts/lib/pending-tx-tracker.js | 28 | ||||
-rw-r--r-- | app/scripts/lib/tx-gas-utils.js (renamed from app/scripts/lib/tx-utils.js) | 22 | ||||
-rw-r--r-- | app/scripts/lib/tx-state-manager.js | 245 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 40 | ||||
-rw-r--r-- | app/scripts/migrations/_multi-keyring.js | 2 |
18 files changed, 746 insertions, 1114 deletions
diff --git a/app/manifest.json b/app/manifest.json index fd07f15a9..639f3fb4b 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.10.3", + "version": "3.10.6", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", diff --git a/app/scripts/background.js b/app/scripts/background.js index 1b96d68b5..195881e15 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -114,7 +114,7 @@ function setupController (initState) { // updateBadge() - controller.txController.on('updateBadge', updateBadge) + controller.txController.on('update:badge', updateBadge) controller.messageManager.on('updateBadge', updateBadge) controller.personalMessageManager.on('updateBadge', updateBadge) diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 90a0f1f22..b4708189e 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -42,16 +42,21 @@ function setupStreams () { name: 'contentscript', target: 'inpage', }) - pageStream.on('error', console.error) const pluginPort = extension.runtime.connect({ name: 'contentscript' }) const pluginStream = new PortStream(pluginPort) - pluginStream.on('error', console.error) // forward communication plugin->inpage - pageStream.pipe(pluginStream).pipe(pageStream) + pump( + pageStream, + pluginStream, + pageStream, + (err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err) + ) // setup local multistream channels const mux = new ObjectMultiplex() + mux.setMaxListeners(25) + pump( mux, pageStream, diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js new file mode 100644 index 000000000..4fa4c78fe --- /dev/null +++ b/app/scripts/controllers/balance.js @@ -0,0 +1,70 @@ +const ObservableStore = require('obs-store') +const PendingBalanceCalculator = require('../lib/pending-balance-calculator') +const BN = require('ethereumjs-util').BN + +class BalanceController { + + constructor (opts = {}) { + const { address, accountTracker, txController, blockTracker } = opts + this.address = address + this.accountTracker = accountTracker + this.txController = txController + this.blockTracker = blockTracker + + const initState = { + ethBalance: undefined, + } + this.store = new ObservableStore(initState) + + this.balanceCalc = new PendingBalanceCalculator({ + getBalance: () => this._getBalance(), + getPendingTransactions: this._getPendingTransactions.bind(this), + }) + + this._registerUpdates() + } + + async updateBalance () { + const balance = await this.balanceCalc.getBalance() + this.store.updateState({ + ethBalance: balance, + }) + } + + _registerUpdates () { + const update = this.updateBalance.bind(this) + + this.txController.on('tx:status-update', (txId, status) => { + switch (status) { + case 'submitted': + case 'confirmed': + case 'failed': + update() + return + default: + return + } + }) + this.accountTracker.store.subscribe(update) + this.blockTracker.on('block', update) + } + + async _getBalance () { + const { accounts } = this.accountTracker.store.getState() + const entry = accounts[this.address] + const balance = entry.balance + return balance ? new BN(balance.substring(2), 16) : undefined + } + + async _getPendingTransactions () { + const pending = this.txController.getFilteredTxList({ + from: this.address, + status: 'submitted', + err: undefined, + }) + return pending + } + +} + +module.exports = BalanceController diff --git a/app/scripts/controllers/computed-balances.js b/app/scripts/controllers/computed-balances.js new file mode 100644 index 000000000..2479e1b3a --- /dev/null +++ b/app/scripts/controllers/computed-balances.js @@ -0,0 +1,66 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const BalanceController = require('./balance') + +class ComputedbalancesController { + + constructor (opts = {}) { + const { accountTracker, txController, blockTracker } = opts + this.accountTracker = accountTracker + this.txController = txController + this.blockTracker = blockTracker + + const initState = extend({ + computedBalances: {}, + }, opts.initState) + this.store = new ObservableStore(initState) + this.balances = {} + + this._initBalanceUpdating() + } + + updateAllBalances () { + for (let address in this.accountTracker.store.getState().accounts) { + this.balances[address].updateBalance() + } + } + + _initBalanceUpdating () { + const store = this.accountTracker.store.getState() + this.addAnyAccountsFromStore(store) + this.accountTracker.store.subscribe(this.addAnyAccountsFromStore.bind(this)) + } + + addAnyAccountsFromStore(store) { + const balances = store.accounts + + for (let address in balances) { + this.trackAddressIfNotAlready(address) + } + } + + trackAddressIfNotAlready (address) { + const state = this.store.getState() + if (!(address in state.computedBalances)) { + this.trackAddress(address) + } + } + + trackAddress (address) { + let updater = new BalanceController({ + address, + accountTracker: this.accountTracker, + txController: this.txController, + blockTracker: this.blockTracker, + }) + updater.store.subscribe((accountBalance) => { + let newState = this.store.getState() + newState.computedBalances[address] = accountBalance + this.store.updateState(newState) + }) + this.balances[address] = updater + updater.updateBalance() + } +} + +module.exports = ComputedbalancesController diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js index 0a3e5e26b..2a17cdae8 100644 --- a/app/scripts/controllers/network.js +++ b/app/scripts/controllers/network.js @@ -4,66 +4,43 @@ 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 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.networkStore = new ObservableStore('loading') this.providerStore = new ObservableStore(config.provider) this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore }) - this._providerListeners = {} + this._proxy = createEventEmitterProxy() 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 + this.providerStore.subscribe((state) => this.switchNetwork({ rpcUrl: state.rpcTarget })) } initializeProvider (opts, providerContructor = MetaMaskProvider) { - this.providerInit = opts - this._provider = providerContructor(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 - return value - }, - }) - this.provider.on('block', this._logBlock.bind(this)) - this.provider.on('error', this.verifyNetwork.bind(this)) - this.ethQuery = new EthQuery(this.provider) + this._baseProviderParams = opts + const provider = providerContructor(opts) + this._setProvider(provider) + 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.provider + return this._proxy } - switchNetwork (providerInit) { + switchNetwork (opts) { this.setNetworkState('loading') - const newInit = extend(this.providerInit, providerInit) - this.providerInit = newInit - - this._provider.removeAllListeners() - this._provider.stop() - 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)) - }) + const providerParams = extend(this._baseProviderParams, opts) + this._baseProviderParams = providerParams + const provider = MetaMaskProvider(providerParams) + this._setProvider(provider) this.emit('networkDidChange') } - verifyNetwork () { // Check network when restoring connectivity: if (this.isNetworkLoading()) this.lookupNetwork() @@ -117,14 +94,26 @@ module.exports = class NetworkController extends EventEmitter { return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC } + _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() + } + // override block tracler + provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers) + // set as new provider + this._provider = provider + this._proxy.setTarget(provider) + } + _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/preferences.js b/app/scripts/controllers/preferences.js index e45224593..bc4848421 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -22,7 +22,7 @@ class PreferencesController { }) } - getSelectedAddress (_address) { + getSelectedAddress () { return this.store.getState().selectedAddress } diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index fb3be6073..4f5c94675 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -1,86 +1,97 @@ const EventEmitter = require('events') -const extend = require('xtend') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') +const Transaction = require('ethereumjs-tx') const EthQuery = require('ethjs-query') -const TxProviderUtil = require('../lib/tx-utils') +const TransactionStateManger = require('../lib/tx-state-manager') +const TxGasUtil = require('../lib/tx-gas-utils') const PendingTransactionTracker = require('../lib/pending-tx-tracker') const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') -const txStateHistoryHelper = require('../lib/tx-state-history-helper') + +/* + Transaction Controller is an aggregate of sub-controllers and trackers + composing them in a way to be exposed to the metamask controller + - txStateManager + responsible for the state of a transaction and + storing the transaction + - pendingTxTracker + watching blocks for transactions to be include + and emitting confirmed events + - txGasUtil + gas calculations and safety buffering + - nonceTracker + calculating nonces +*/ module.exports = class TransactionController extends EventEmitter { constructor (opts) { super() - this.store = new ObservableStore(extend({ - transactions: [], - }, opts.initState)) - this.memStore = new ObservableStore({}) this.networkStore = opts.networkStore || new ObservableStore({}) this.preferencesStore = opts.preferencesStore || new ObservableStore({}) - this.txHistoryLimit = opts.txHistoryLimit this.provider = opts.provider this.blockTracker = opts.blockTracker this.signEthTx = opts.signTransaction - this.ethStore = opts.ethStore + this.accountTracker = opts.accountTracker + + this.memStore = new ObservableStore({}) + this.query = new EthQuery(this.provider) + this.txGasUtil = new TxGasUtil(this.provider) + this.txStateManager = new TransactionStateManger({ + initState: opts.initState, + txHistoryLimit: opts.txHistoryLimit, + getNetwork: this.getNetwork.bind(this), + }) + this.store = this.txStateManager.store + this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update')) this.nonceTracker = new NonceTracker({ provider: this.provider, - getPendingTransactions: (address) => { - return this.getFilteredTxList({ - from: address, - status: 'submitted', - err: undefined, - }) - }, + getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), getConfirmedTransactions: (address) => { - return this.getFilteredTxList({ + return this.txStateManager.getFilteredTxList({ from: address, status: 'confirmed', err: undefined, }) }, - giveUpOnTransaction: (txId) => { - const msg = `Gave up submitting after 3500 blocks un-mined.` - this.setTxStatusFailed(txId, msg) - }, }) - this.query = new EthQuery(this.provider) - this.txProviderUtil = new TxProviderUtil(this.provider) this.pendingTxTracker = new PendingTransactionTracker({ provider: this.provider, nonceTracker: this.nonceTracker, + retryLimit: 3500, // Retry 3500 blocks, or about 1 day. getBalance: (address) => { - const account = this.ethStore.getState().accounts[address] + const account = this.accountTracker.store.getState().accounts[address] if (!account) return return account.balance }, - publishTransaction: this.txProviderUtil.publishTransaction.bind(this.txProviderUtil), - getPendingTransactions: () => { - const network = this.getNetwork() - return this.getFilteredTxList({ - status: 'submitted', - metamaskNetworkId: network, - }) - }, + publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx), + getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager), }) - this.pendingTxTracker.on('txWarning', this.updateTx.bind(this)) - this.pendingTxTracker.on('txFailed', this.setTxStatusFailed.bind(this)) - this.pendingTxTracker.on('txConfirmed', this.setTxStatusConfirmed.bind(this)) + this.txStateManager.store.subscribe(() => this.emit('update:badge')) - this.blockTracker.on('rawBlock', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) + this.pendingTxTracker.on('tx:warning', this.txStateManager.updateTx.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager)) + this.pendingTxTracker.on('tx:retry', (txMeta) => { + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + txMeta.retryCount++ + this.txStateManager.updateTx(txMeta) + }) + + this.blockTracker.on('block', this.pendingTxTracker.checkForTxInBlock.bind(this.pendingTxTracker)) // this is a little messy but until ethstore has been either // removed or redone this is to guard against the race condition - // where ethStore hasent been populated by the results yet + // where accountTracker hasent been populated by the results yet this.blockTracker.once('latest', () => { this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker)) }) this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker)) // memstore is computed from a few different stores this._updateMemstore() - this.store.subscribe(() => this._updateMemstore()) + this.txStateManager.store.subscribe(() => this._updateMemstore()) this.networkStore.subscribe(() => this._updateMemstore()) this.preferencesStore.subscribe(() => this._updateMemstore()) } @@ -97,98 +108,31 @@ module.exports = class TransactionController extends EventEmitter { return this.preferencesStore.getState().selectedAddress } - // Returns the number of txs for the current network. - getTxCount () { - return this.getTxList().length - } - - // Returns the full tx list across all networks - getFullTxList () { - return this.store.getState().transactions - } - getUnapprovedTxCount () { - return Object.keys(this.getUnapprovedTxList()).length - } - - getPendingTxCount () { - return this.getTxsByMetaData('status', 'signed').length + return Object.keys(this.txStateManager.getUnapprovedTxList()).length } - // Returns the tx list - getTxList () { - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - return this.getTxsByMetaData('metamaskNetworkId', network, fullTxList) + getPendingTxCount (account) { + return this.txStateManager.getPendingTransactions(account).length } - // gets tx by Id and returns it - getTx (txId) { - const txList = this.getTxList() - const txMeta = txList.find(txData => txData.id === txId) - return txMeta - } - getUnapprovedTxList () { - const txList = this.getTxList() - return txList.filter((txMeta) => txMeta.status === 'unapproved') - .reduce((result, tx) => { - result[tx.id] = tx - return result - }, {}) + getFilteredTxList (opts) { + return this.txStateManager.getFilteredTxList(opts) } - updateTx (txMeta) { - // create txMeta snapshot for history - const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta) - // recover previous tx state obj - const previousState = txStateHistoryHelper.replayHistory(txMeta.history) - // generate history entry and add to history - const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState) - txMeta.history.push(entry) - - // commit txMeta to state - const txId = txMeta.id - const txList = this.getFullTxList() - const index = txList.findIndex(txData => txData.id === txId) - txList[index] = txMeta - this._saveTxList(txList) - this.emit('update') + getChainId () { + const networkState = this.networkStore.getState() + const getChainId = parseInt(networkState) + if (Number.isNaN(getChainId)) { + return 0 + } else { + return getChainId + } } // Adds a tx to the txlist addTx (txMeta) { - // initialize history - txMeta.history = [] - // capture initial snapshot of txMeta for history - const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta) - txMeta.history.push(snapshot) - - // checks if the length of the tx history is - // longer then desired persistence limit - // and then if it is removes only confirmed - // or rejected tx's. - // not tx's that are pending or unapproved - const txCount = this.getTxCount() - const network = this.getNetwork() - const fullTxList = this.getFullTxList() - const txHistoryLimit = this.txHistoryLimit - - if (txCount > txHistoryLimit - 1) { - const index = fullTxList.findIndex((metaTx) => ((metaTx.status === 'confirmed' || metaTx.status === 'rejected') && network === txMeta.metamaskNetworkId)) - fullTxList.splice(index, 1) - } - fullTxList.push(txMeta) - this._saveTxList(fullTxList) - this.emit('update') - - this.once(`${txMeta.id}:signed`, function (txId) { - this.removeAllListeners(`${txMeta.id}:rejected`) - }) - this.once(`${txMeta.id}:rejected`, function (txId) { - this.removeAllListeners(`${txMeta.id}:signed`) - }) - - this.emit('updateBadge') + this.txStateManager.addTx(txMeta) this.emit(`${txMeta.id}:unapproved`, txMeta) } @@ -198,7 +142,7 @@ module.exports = class TransactionController extends EventEmitter { this.emit('newUnaprovedTx', txMeta) // listen for tx completion (success, fail) return new Promise((resolve, reject) => { - this.once(`${txMeta.id}:finished`, (completedTx) => { + this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => { switch (completedTx.status) { case 'submitted': return resolve(completedTx.hash) @@ -213,7 +157,7 @@ module.exports = class TransactionController extends EventEmitter { async addUnapprovedTransaction (txParams) { // validate - await this.txProviderUtil.validateTxParams(txParams) + await this.txGasUtil.validateTxParams(txParams) // construct txMeta const txMeta = { id: createId(), @@ -232,17 +176,15 @@ module.exports = class TransactionController extends EventEmitter { async addTxDefaults (txMeta) { const txParams = txMeta.txParams // ensure value + const gasPrice = txParams.gasPrice || await this.query.gasPrice() txParams.value = txParams.value || '0x0' - if (!txParams.gasPrice) { - const gasPrice = await this.query.gasPrice() - txParams.gasPrice = gasPrice - } + txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16)) // set gasLimit - return await this.txProviderUtil.analyzeGasUsage(txMeta) + return await this.txGasUtil.analyzeGasUsage(txMeta) } async updateAndApproveTransaction (txMeta) { - this.updateTx(txMeta) + this.txStateManager.updateTx(txMeta) await this.approveTransaction(txMeta.id) } @@ -250,24 +192,24 @@ module.exports = class TransactionController extends EventEmitter { let nonceLock try { // approve - this.setTxStatusApproved(txId) + this.txStateManager.setTxStatusApproved(txId) // get next nonce - const txMeta = this.getTx(txId) + const txMeta = this.txStateManager.getTx(txId) const fromAddress = txMeta.txParams.from // wait for a nonce nonceLock = await this.nonceTracker.getNonceLock(fromAddress) // add nonce to txParams - txMeta.txParams.nonce = nonceLock.nextNonce + txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16)) // add nonce debugging information to txMeta txMeta.nonceDetails = nonceLock.nonceDetails - this.updateTx(txMeta) + this.txStateManager.updateTx(txMeta) // sign transaction const rawTx = await this.signTransaction(txId) await this.publishTransaction(txId, rawTx) // must set transaction to submitted/failed before releasing lock nonceLock.releaseLock() } catch (err) { - this.setTxStatusFailed(txId, err) + this.txStateManager.setTxStatusFailed(txId, err) // must set transaction to submitted/failed before releasing lock if (nonceLock) nonceLock.releaseLock() // continue with error chain @@ -276,180 +218,46 @@ module.exports = class TransactionController extends EventEmitter { } async signTransaction (txId) { - const txMeta = this.getTx(txId) + const txMeta = this.txStateManager.getTx(txId) const txParams = txMeta.txParams const fromAddress = txParams.from // add network/chain id - txParams.chainId = this.getChainId() - const ethTx = this.txProviderUtil.buildEthTxFromParams(txParams) + txParams.chainId = ethUtil.addHexPrefix(this.getChainId().toString(16)) + const ethTx = new Transaction(txParams) await this.signEthTx(ethTx, fromAddress) - this.setTxStatusSigned(txMeta.id) + this.txStateManager.setTxStatusSigned(txMeta.id) const rawTx = ethUtil.bufferToHex(ethTx.serialize()) return rawTx } async publishTransaction (txId, rawTx) { - const txMeta = this.getTx(txId) + const txMeta = this.txStateManager.getTx(txId) txMeta.rawTx = rawTx - this.updateTx(txMeta) - const txHash = await this.txProviderUtil.publishTransaction(rawTx) + this.txStateManager.updateTx(txMeta) + const txHash = await this.query.sendRawTransaction(rawTx) this.setTxHash(txId, txHash) - this.setTxStatusSubmitted(txId) + this.txStateManager.setTxStatusSubmitted(txId) } async cancelTransaction (txId) { - this.setTxStatusRejected(txId) - } - - - getChainId () { - const networkState = this.networkStore.getState() - const getChainId = parseInt(networkState) - if (Number.isNaN(getChainId)) { - return 0 - } else { - return getChainId - } + this.txStateManager.setTxStatusRejected(txId) } // receives a txHash records the tx as signed setTxHash (txId, txHash) { // Add the tx hash to the persisted meta-tx object - const txMeta = this.getTx(txId) + const txMeta = this.txStateManager.getTx(txId) txMeta.hash = txHash - this.updateTx(txMeta) - } - - /* - Takes an object of fields to search for eg: - let thingsToLookFor = { - to: '0x0..', - from: '0x0..', - status: 'signed', - err: undefined, - } - and returns a list of tx with all - options matching - - ****************HINT**************** - | `err: undefined` is like looking | - | for a tx with no err | - | so you can also search txs that | - | dont have something as well by | - | setting the value as undefined | - ************************************ - - this is for things like filtering a the tx list - for only tx's from 1 account - or for filltering for all txs from one account - and that have been 'confirmed' - */ - getFilteredTxList (opts) { - let filteredTxList - Object.keys(opts).forEach((key) => { - filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) - }) - return filteredTxList - } - - getTxsByMetaData (key, value, txList = this.getTxList()) { - return txList.filter((txMeta) => { - if (txMeta.txParams[key]) { - return txMeta.txParams[key] === value - } else { - return txMeta[key] === value - } - }) - } - - // STATUS METHODS - // get::set status - - // should return the status of the tx. - getTxStatus (txId) { - const txMeta = this.getTx(txId) - return txMeta.status + this.txStateManager.updateTx(txMeta) } - // should update the status of the tx to 'rejected'. - setTxStatusRejected (txId) { - this._setTxStatus(txId, 'rejected') - } - - // should update the status of the tx to 'approved'. - setTxStatusApproved (txId) { - this._setTxStatus(txId, 'approved') - } - - // should update the status of the tx to 'signed'. - setTxStatusSigned (txId) { - this._setTxStatus(txId, 'signed') - } - - // should update the status of the tx to 'submitted'. - setTxStatusSubmitted (txId) { - this._setTxStatus(txId, 'submitted') - } - - // should update the status of the tx to 'confirmed'. - setTxStatusConfirmed (txId) { - this._setTxStatus(txId, 'confirmed') - } - - setTxStatusFailed (txId, err) { - const txMeta = this.getTx(txId) - txMeta.err = { - message: err.toString(), - stack: err.stack, - } - this.updateTx(txMeta) - this._setTxStatus(txId, 'failed') - } - - // merges txParams obj onto txData.txParams - // use extend to ensure that all fields are filled - updateTxParams (txId, txParams) { - const txMeta = this.getTx(txId) - txMeta.txParams = extend(txMeta.txParams, txParams) - this.updateTx(txMeta) - } - -/* _____________________________________ -| | -| PRIVATE METHODS | -|______________________________________*/ - - - // Should find the tx in the tx list and - // update it. - // should set the status in txData - // - `'unapproved'` the user has not responded - // - `'rejected'` the user has responded no! - // - `'approved'` the user has approved the tx - // - `'signed'` the tx is signed - // - `'submitted'` the tx is sent to a server - // - `'confirmed'` the tx has been included in a block. - // - `'failed'` the tx failed for some reason, included on tx data. - _setTxStatus (txId, status) { - const txMeta = this.getTx(txId) - txMeta.status = status - this.emit(`${txMeta.id}:${status}`, txId) - if (status === 'submitted' || status === 'rejected') { - this.emit(`${txMeta.id}:finished`, txMeta) - } - this.updateTx(txMeta) - this.emit('updateBadge') - } - - // Saves the new/updated txList. - // Function is intended only for internal use - _saveTxList (transactions) { - this.store.updateState({ transactions }) - } +// +// PRIVATE METHODS +// _updateMemstore () { - const unapprovedTxs = this.getUnapprovedTxList() - const selectedAddressTxList = this.getFilteredTxList({ + const unapprovedTxs = this.txStateManager.getUnapprovedTxList() + const selectedAddressTxList = this.txStateManager.getFilteredTxList({ from: this.getSelectedAddress(), metamaskNetworkId: this.getNetwork(), }) diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js deleted file mode 100644 index adfa4a813..000000000 --- a/app/scripts/keyring-controller.js +++ /dev/null @@ -1,595 +0,0 @@ -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN -const bip39 = require('bip39') -const EventEmitter = require('events').EventEmitter -const ObservableStore = require('obs-store') -const filter = require('promise-filter') -const encryptor = require('browser-passworder') -const sigUtil = require('eth-sig-util') -const normalizeAddress = sigUtil.normalize -// Keyrings: -const SimpleKeyring = require('eth-simple-keyring') -const HdKeyring = require('eth-hd-keyring') -const keyringTypes = [ - SimpleKeyring, - HdKeyring, -] - -class KeyringController extends EventEmitter { - - // PUBLIC METHODS - // - // THE FIRST SECTION OF METHODS ARE PUBLIC-FACING, - // MEANING THEY ARE USED BY CONSUMERS OF THIS CLASS. - // - // THEIR SURFACE AREA SHOULD BE CHANGED WITH GREAT CARE. - - constructor (opts) { - super() - const initState = opts.initState || {} - this.keyringTypes = keyringTypes - this.store = new ObservableStore(initState) - this.memStore = new ObservableStore({ - isUnlocked: false, - keyringTypes: this.keyringTypes.map(krt => krt.type), - keyrings: [], - identities: {}, - }) - this.ethStore = opts.ethStore - this.encryptor = opts.encryptor || encryptor - this.keyrings = [] - this.getNetwork = opts.getNetwork - } - - // Full Update - // returns Promise( @object state ) - // - // Emits the `update` event and - // returns a Promise that resolves to the current state. - // - // Frequently used to end asynchronous chains in this class, - // indicating consumers can often either listen for updates, - // or accept a state-resolving promise to consume their results. - // - // Not all methods end with this, that might be a nice refactor. - fullUpdate () { - this.emit('update') - return Promise.resolve(this.memStore.getState()) - } - - // Create New Vault And Keychain - // @string password - The password to encrypt the vault with - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // randomly creates a new HD wallet with 1 account, - // faucets that account on the testnet. - createNewVaultAndKeychain (password) { - return this.persistAllKeyrings(password) - .then(this.createFirstKeyTree.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // CreateNewVaultAndRestore - // @string password - The password to encrypt the vault with - // @string seed - The BIP44-compliant seed phrase. - // - // returns Promise( @object state ) - // - // Destroys any old encrypted storage, - // creates a new encrypted store with the given password, - // creates a new HD wallet from the given seed with 1 account. - createNewVaultAndRestore (password, seed) { - if (typeof password !== 'string') { - return Promise.reject('Password must be text.') - } - - if (!bip39.validateMnemonic(seed)) { - return Promise.reject(new Error('Seed phrase is invalid.')) - } - - this.clearKeyrings() - - return this.persistAllKeyrings(password) - .then(() => { - return this.addNewKeyring('HD Key Tree', { - mnemonic: seed, - numberOfAccounts: 1, - }) - }) - .then((firstKeyring) => { - return firstKeyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - First Account not found.') - const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this, password)) - .then(this.fullUpdate.bind(this)) - } - - // Set Locked - // returns Promise( @object state ) - // - // This method deallocates all secrets, and effectively locks metamask. - setLocked () { - // set locked - this.password = null - this.memStore.updateState({ isUnlocked: false }) - // remove keyrings - this.keyrings = [] - this._updateMemStoreKeyrings() - return this.fullUpdate() - } - - // Submit Password - // @string password - // - // returns Promise( @object state ) - // - // Attempts to decrypt the current vault and load its keyrings - // into memory. - // - // Temporarily also migrates any old-style vaults first, as well. - // (Pre MetaMask 3.0.0) - submitPassword (password) { - return this.unlockKeyrings(password) - .then((keyrings) => { - this.keyrings = keyrings - return this.fullUpdate() - }) - } - - // Add New Keyring - // @string type - // @object opts - // - // returns Promise( @Keyring keyring ) - // - // Adds a new Keyring of the given `type` to the vault - // and the current decrypted Keyrings array. - // - // All Keyring classes implement a unique `type` string, - // and this is used to retrieve them from the keyringTypes array. - addNewKeyring (type, opts) { - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring(opts) - return keyring.deserialize(opts) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.checkForDuplicate(type, accounts) - }) - .then((checkedAccounts) => { - this.keyrings.push(keyring) - return this.setupAccounts(checkedAccounts) - }) - .then(() => this.persistAllKeyrings()) - .then(() => this._updateMemStoreKeyrings()) - .then(() => this.fullUpdate()) - .then(() => { - return keyring - }) - } - - // For now just checks for simple key pairs - // but in the future - // should possibly add HD and other types - // - checkForDuplicate (type, newAccount) { - return this.getAccounts() - .then((accounts) => { - switch (type) { - case 'Simple Key Pair': - const isNotIncluded = !accounts.find((key) => key === newAccount[0] || key === ethUtil.stripHexPrefix(newAccount[0])) - return (isNotIncluded) ? Promise.resolve(newAccount) : Promise.reject(new Error('The account you\'re are trying to import is a duplicate')) - default: - return Promise.resolve(newAccount) - } - }) - } - - - // Add New Account - // @number keyRingNum - // - // returns Promise( @object state ) - // - // Calls the `addAccounts` method on the Keyring - // in the kryings array at index `keyringNum`, - // and then saves those changes. - addNewAccount (selectedKeyring) { - return selectedKeyring.addAccounts(1) - .then(this.setupAccounts.bind(this)) - .then(this.persistAllKeyrings.bind(this)) - .then(this._updateMemStoreKeyrings.bind(this)) - .then(this.fullUpdate.bind(this)) - } - - // Save Account Label - // @string account - // @string label - // - // returns Promise( @string label ) - // - // Persists a nickname equal to `label` for the specified account. - saveAccountLabel (account, label) { - try { - const hexAddress = normalizeAddress(account) - // update state on diskStore - const state = this.store.getState() - const walletNicknames = state.walletNicknames || {} - walletNicknames[hexAddress] = label - this.store.updateState({ walletNicknames }) - // update state on memStore - const identities = this.memStore.getState().identities - identities[hexAddress].name = label - this.memStore.updateState({ identities }) - return Promise.resolve(label) - } catch (err) { - return Promise.reject(err) - } - } - - // Export Account - // @string address - // - // returns Promise( @string privateKey ) - // - // Requests the private key from the keyring controlling - // the specified address. - // - // Returns a Promise that may resolve with the private key string. - exportAccount (address) { - try { - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.exportAccount(normalizeAddress(address)) - }) - } catch (e) { - return Promise.reject(e) - } - } - - - // SIGNING METHODS - // - // This method signs tx and returns a promise for - // TX Manager to update the state after signing - - signTransaction (ethTx, _fromAddress) { - const fromAddress = normalizeAddress(_fromAddress) - return this.getKeyringForAccount(fromAddress) - .then((keyring) => { - return keyring.signTransaction(fromAddress, ethTx) - }) - } - - // Sign Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - signMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signMessage(address, msgParams.data) - }) - } - - // Sign Personal Message - // @object msgParams - // - // returns Promise(@buffer rawSig) - // - // Attempts to sign the provided @object msgParams. - // Prefixes the hash before signing as per the new geth behavior. - signPersonalMessage (msgParams) { - const address = normalizeAddress(msgParams.from) - return this.getKeyringForAccount(address) - .then((keyring) => { - return keyring.signPersonalMessage(address, msgParams.data) - }) - } - - // PRIVATE METHODS - // - // THESE METHODS ARE ONLY USED INTERNALLY TO THE KEYRING-CONTROLLER - // AND SO MAY BE CHANGED MORE LIBERALLY THAN THE ABOVE METHODS. - - // Create First Key Tree - // returns @Promise - // - // Clears the vault, - // creates a new one, - // creates a random new HD Keyring with 1 account, - // makes that account the selected account, - // faucets that account on testnet, - // puts the current seed words into the state tree. - createFirstKeyTree () { - this.clearKeyrings() - return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 }) - .then((keyring) => { - return keyring.getAccounts() - }) - .then((accounts) => { - const firstAccount = accounts[0] - if (!firstAccount) throw new Error('KeyringController - No account found on keychain.') - const hexAccount = normalizeAddress(firstAccount) - this.emit('newAccount', hexAccount) - this.emit('newVault', hexAccount) - return this.setupAccounts(accounts) - }) - .then(this.persistAllKeyrings.bind(this)) - } - - // Setup Accounts - // @array accounts - // - // returns @Promise(@object account) - // - // Initializes the provided account array - // Gives them numerically incremented nicknames, - // and adds them to the ethStore for regular balance checking. - setupAccounts (accounts) { - return this.getAccounts() - .then((loadedAccounts) => { - const arr = accounts || loadedAccounts - return Promise.all(arr.map((account) => { - return this.getBalanceAndNickname(account) - })) - }) - } - - // Get Balance And Nickname - // @string account - // - // returns Promise( @string label ) - // - // Takes an account address and an iterator representing - // the current number of named accounts. - getBalanceAndNickname (account) { - if (!account) { - throw new Error('Problem loading account.') - } - const address = normalizeAddress(account) - this.ethStore.addAccount(address) - return this.createNickname(address) - } - - // Create Nickname - // @string address - // - // returns Promise( @string label ) - // - // Takes an address, and assigns it an incremented nickname, persisting it. - createNickname (address) { - const hexAddress = normalizeAddress(address) - const identities = this.memStore.getState().identities - const currentIdentityCount = Object.keys(identities).length + 1 - const nicknames = this.store.getState().walletNicknames || {} - const existingNickname = nicknames[hexAddress] - const name = existingNickname || `Account ${currentIdentityCount}` - identities[hexAddress] = { - address: hexAddress, - name, - } - this.memStore.updateState({ identities }) - return this.saveAccountLabel(hexAddress, name) - } - - // Persist All Keyrings - // @password string - // - // returns Promise - // - // Iterates the current `keyrings` array, - // serializes each one into a serialized array, - // encrypts that array with the provided `password`, - // and persists that encrypted string to storage. - persistAllKeyrings (password = this.password) { - if (typeof password === 'string') { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - } - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([keyring.type, keyring.serialize()]) - .then((serializedKeyringArray) => { - // Label the output values on each serialized Keyring: - return { - type: serializedKeyringArray[0], - data: serializedKeyringArray[1], - } - }) - })) - .then((serializedKeyrings) => { - return this.encryptor.encrypt(this.password, serializedKeyrings) - }) - .then((encryptedString) => { - this.store.updateState({ vault: encryptedString }) - return true - }) - } - - // Unlock Keyrings - // @string password - // - // returns Promise( @array keyrings ) - // - // Attempts to unlock the persisted encrypted storage, - // initializing the persisted keyrings to RAM. - unlockKeyrings (password) { - const encryptedVault = this.store.getState().vault - if (!encryptedVault) { - throw new Error('Cannot unlock without a previous vault.') - } - - return this.encryptor.decrypt(password, encryptedVault) - .then((vault) => { - this.password = password - this.memStore.updateState({ isUnlocked: true }) - vault.forEach(this.restoreKeyring.bind(this)) - return this.keyrings - }) - } - - // Restore Keyring - // @object serialized - // - // returns Promise( @Keyring deserialized ) - // - // Attempts to initialize a new keyring from the provided - // serialized payload. - // - // On success, returns the resulting @Keyring instance. - restoreKeyring (serialized) { - const { type, data } = serialized - - const Keyring = this.getKeyringClassForType(type) - const keyring = new Keyring() - return keyring.deserialize(data) - .then(() => { - return keyring.getAccounts() - }) - .then((accounts) => { - return this.setupAccounts(accounts) - }) - .then(() => { - this.keyrings.push(keyring) - this._updateMemStoreKeyrings() - return keyring - }) - } - - // Get Keyring Class For Type - // @string type - // - // Returns @class Keyring - // - // Searches the current `keyringTypes` array - // for a Keyring class whose unique `type` property - // matches the provided `type`, - // returning it if it exists. - getKeyringClassForType (type) { - return this.keyringTypes.find(kr => kr.type === type) - } - - getKeyringsByType (type) { - return this.keyrings.filter((keyring) => keyring.type === type) - } - - // Get Accounts - // returns Promise( @Array[ @string accounts ] ) - // - // Returns the public addresses of all current accounts - // managed by all currently unlocked keyrings. - getAccounts () { - const keyrings = this.keyrings || [] - return Promise.all(keyrings.map(kr => kr.getAccounts())) - .then((keyringArrays) => { - return keyringArrays.reduce((res, arr) => { - return res.concat(arr) - }, []) - }) - } - - // Get Keyring For Account - // @string address - // - // returns Promise(@Keyring keyring) - // - // Returns the currently initialized keyring that manages - // the specified `address` if one exists. - getKeyringForAccount (address) { - const hexed = normalizeAddress(address) - log.debug(`KeyringController - getKeyringForAccount: ${hexed}`) - - return Promise.all(this.keyrings.map((keyring) => { - return Promise.all([ - keyring, - keyring.getAccounts(), - ]) - })) - .then(filter((candidate) => { - const accounts = candidate[1].map(normalizeAddress) - return accounts.includes(hexed) - })) - .then((winners) => { - if (winners && winners.length > 0) { - return winners[0][0] - } else { - throw new Error('No keyring found for the requested account.') - } - }) - } - - // Display For Keyring - // @Keyring keyring - // - // returns Promise( @Object { type:String, accounts:Array } ) - // - // Is used for adding the current keyrings to the state object. - displayForKeyring (keyring) { - return keyring.getAccounts() - .then((accounts) => { - return { - type: keyring.type, - accounts: accounts, - } - }) - } - - // Add Gas Buffer - // @string gas (as hexadecimal value) - // - // returns @string bufferedGas (as hexadecimal value) - // - // Adds a healthy buffer of gas to an initial gas estimate. - addGasBuffer (gas) { - const gasBuffer = new BN('100000', 10) - const bnGas = new BN(ethUtil.stripHexPrefix(gas), 16) - const correct = bnGas.add(gasBuffer) - return ethUtil.addHexPrefix(correct.toString(16)) - } - - // Clear Keyrings - // - // Deallocates all currently managed keyrings and accounts. - // Used before initializing a new vault. - clearKeyrings () { - let accounts - try { - accounts = Object.keys(this.ethStore.getState()) - } catch (e) { - accounts = [] - } - accounts.forEach((address) => { - this.ethStore.removeAccount(address) - }) - - // clear keyrings from memory - this.keyrings = [] - this.memStore.updateState({ - keyrings: [], - identities: {}, - }) - } - - _updateMemStoreKeyrings () { - Promise.all(this.keyrings.map(this.displayForKeyring)) - .then((keyrings) => { - this.memStore.updateState({ keyrings }) - }) - } - -} - -module.exports = KeyringController diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js new file mode 100644 index 000000000..cdc21282d --- /dev/null +++ b/app/scripts/lib/account-tracker.js @@ -0,0 +1,104 @@ +/* Account Tracker + * + * This module is responsible for tracking any number of accounts + * and caching their current balances & transaction counts. + * + * It also tracks transaction hashes, and checks their inclusion status + * on each new block. + */ + +const async = require('async') +const EthQuery = require('eth-query') +const ObservableStore = require('obs-store') +const EventEmitter = require('events').EventEmitter +function noop () {} + + +class AccountTracker extends EventEmitter { + + constructor (opts = {}) { + super() + + const initState = { + accounts: {}, + currentBlockGasLimit: '', + } + this.store = new ObservableStore(initState) + + this._provider = opts.provider + this._query = new EthQuery(this._provider) + this._blockTracker = opts.blockTracker + // subscribe to latest block + this._blockTracker.on('block', this._updateForBlock.bind(this)) + // blockTracker.currentBlock may be null + this._currentBlockNumber = this._blockTracker.currentBlock + } + + // + // public + // + + addAccount (address) { + const accounts = this.store.getState().accounts + accounts[address] = {} + this.store.updateState({ accounts }) + if (!this._currentBlockNumber) return + this._updateAccount(address) + } + + removeAccount (address) { + const accounts = this.store.getState().accounts + delete accounts[address] + this.store.updateState({ accounts }) + } + + // + // private + // + + _updateForBlock (block) { + this._currentBlockNumber = block.number + const currentBlockGasLimit = block.gasLimit + + this.store.updateState({ currentBlockGasLimit }) + + async.parallel([ + this._updateAccounts.bind(this), + ], (err) => { + if (err) return console.error(err) + this.emit('block', this.store.getState()) + }) + } + + _updateAccounts (cb = noop) { + const accounts = this.store.getState().accounts + const addresses = Object.keys(accounts) + async.each(addresses, this._updateAccount.bind(this), cb) + } + + _updateAccount (address, cb = noop) { + this._getAccount(address, (err, result) => { + if (err) return cb(err) + result.address = address + const accounts = this.store.getState().accounts + // only populate if the entry is still present + if (accounts[address]) { + accounts[address] = result + this.store.updateState({ accounts }) + } + cb(null, result) + }) + } + + _getAccount (address, cb = noop) { + const query = this._query + async.parallel({ + balance: query.getBalance.bind(query, address), + nonce: query.getTransactionCount.bind(query, address), + code: query.getCode.bind(query, address), + }, cb) + } + +} + +module.exports = AccountTracker diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/eth-store.js deleted file mode 100644 index ebba98f5c..000000000 --- a/app/scripts/lib/eth-store.js +++ /dev/null @@ -1,138 +0,0 @@ -/* Ethereum Store - * - * This module is responsible for tracking any number of accounts - * and caching their current balances & transaction counts. - * - * It also tracks transaction hashes, and checks their inclusion status - * on each new block. - */ - -const async = require('async') -const EthQuery = require('eth-query') -const ObservableStore = require('obs-store') -function noop () {} - - -class EthereumStore extends ObservableStore { - - constructor (opts = {}) { - super({ - accounts: {}, - transactions: {}, - currentBlockNumber: '0', - currentBlockHash: '', - currentBlockGasLimit: '', - }) - this._provider = opts.provider - this._query = new EthQuery(this._provider) - this._blockTracker = opts.blockTracker - // subscribe to latest block - this._blockTracker.on('block', this._updateForBlock.bind(this)) - // blockTracker.currentBlock may be null - this._currentBlockNumber = this._blockTracker.currentBlock - } - - // - // public - // - - addAccount (address) { - const accounts = this.getState().accounts - accounts[address] = {} - this.updateState({ accounts }) - if (!this._currentBlockNumber) return - this._updateAccount(address) - } - - removeAccount (address) { - const accounts = this.getState().accounts - delete accounts[address] - this.updateState({ accounts }) - } - - addTransaction (txHash) { - const transactions = this.getState().transactions - transactions[txHash] = {} - this.updateState({ transactions }) - if (!this._currentBlockNumber) return - this._updateTransaction(this._currentBlockNumber, txHash, noop) - } - - removeTransaction (txHash) { - const transactions = this.getState().transactions - delete transactions[txHash] - this.updateState({ transactions }) - } - - - // - // private - // - - _updateForBlock (block) { - const blockNumber = '0x' + block.number.toString('hex') - this._currentBlockNumber = blockNumber - this.updateState({ currentBlockNumber: parseInt(blockNumber) }) - this.updateState({ currentBlockHash: `0x${block.hash.toString('hex')}`}) - this.updateState({ currentBlockGasLimit: `0x${block.gasLimit.toString('hex')}` }) - async.parallel([ - this._updateAccounts.bind(this), - this._updateTransactions.bind(this, blockNumber), - ], (err) => { - if (err) return console.error(err) - this.emit('block', this.getState()) - }) - } - - _updateAccounts (cb = noop) { - const accounts = this.getState().accounts - const addresses = Object.keys(accounts) - async.each(addresses, this._updateAccount.bind(this), cb) - } - - _updateAccount (address, cb = noop) { - const accounts = this.getState().accounts - this._getAccount(address, (err, result) => { - if (err) return cb(err) - result.address = address - // only populate if the entry is still present - if (accounts[address]) { - accounts[address] = result - this.updateState({ accounts }) - } - cb(null, result) - }) - } - - _updateTransactions (block, cb = noop) { - const transactions = this.getState().transactions - const txHashes = Object.keys(transactions) - async.each(txHashes, this._updateTransaction.bind(this, block), cb) - } - - _updateTransaction (block, txHash, cb = noop) { - // would use the block here to determine how many confirmations the tx has - const transactions = this.getState().transactions - this._query.getTransaction(txHash, (err, result) => { - if (err) return cb(err) - // only populate if the entry is still present - if (transactions[txHash]) { - transactions[txHash] = result - this.updateState({ transactions }) - } - cb(null, result) - }) - } - - _getAccount (address, cb = noop) { - const query = this._query - async.parallel({ - balance: query.getBalance.bind(query, address), - nonce: query.getTransactionCount.bind(query, address), - code: query.getCode.bind(query, address), - }, cb) - } - -} - -module.exports = EthereumStore diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js new file mode 100644 index 000000000..d1199a278 --- /dev/null +++ b/app/scripts/lib/events-proxy.js @@ -0,0 +1,31 @@ +module.exports = function createEventEmitterProxy(eventEmitter, listeners) { + let target = eventEmitter + const eventHandlers = listeners || {} + const proxy = new Proxy({}, { + get: (obj, name) => { + // intercept listeners + if (name === 'on') return addListener + if (name === 'setTarget') return setTarget + if (name === 'proxyEventHandlers') return eventHandlers + return target[name] + }, + set: (obj, name, value) => { + target[name] = value + return true + }, + }) + function setTarget (eventEmitter) { + target = eventEmitter + // migrate listeners + Object.keys(eventHandlers).forEach((name) => { + eventHandlers[name].forEach((handler) => target.on(name, handler)) + }) + } + function addListener (name, handler) { + if (!eventHandlers[name]) eventHandlers[name] = [] + eventHandlers[name].push(handler) + target.on(name, handler) + } + if (listeners) proxy.setTarget(eventEmitter) + return proxy +}
\ No newline at end of file diff --git a/app/scripts/lib/pending-balance-calculator.js b/app/scripts/lib/pending-balance-calculator.js new file mode 100644 index 000000000..cea642f1a --- /dev/null +++ b/app/scripts/lib/pending-balance-calculator.js @@ -0,0 +1,51 @@ +const BN = require('ethereumjs-util').BN +const normalize = require('eth-sig-util').normalize + +class PendingBalanceCalculator { + + // Must be initialized with two functions: + // getBalance => Returns a promise of a BN of the current balance in Wei + // getPendingTransactions => Returns an array of TxMeta Objects, + // which have txParams properties, which include value, gasPrice, and gas, + // all in a base=16 hex format. + constructor ({ getBalance, getPendingTransactions }) { + this.getPendingTransactions = getPendingTransactions + this.getNetworkBalance = getBalance + } + + async getBalance() { + const results = await Promise.all([ + this.getNetworkBalance(), + this.getPendingTransactions(), + ]) + + const [ balance, pending ] = results + if (!balance) return undefined + + const pendingValue = pending.reduce((total, tx) => { + return total.add(this.calculateMaxCost(tx)) + }, new BN(0)) + + return `0x${balance.sub(pendingValue).toString(16)}` + } + + calculateMaxCost (tx) { + const txValue = tx.txParams.value + const value = this.hexToBn(txValue) + const gasPrice = this.hexToBn(tx.txParams.gasPrice) + + const gas = tx.txParams.gas + const gasLimit = tx.txParams.gasLimit + const gasLimitBn = this.hexToBn(gas || gasLimit) + + const gasCost = gasPrice.mul(gasLimitBn) + return value.add(gasCost) + } + + hexToBn (hex) { + return new BN(normalize(hex).substring(2), 16) + } + +} + +module.exports = PendingBalanceCalculator diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js index 44e9d50fa..b97cec9ce 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -1,7 +1,6 @@ const EventEmitter = require('events') const EthQuery = require('ethjs-query') const sufficientBalance = require('./util').sufficientBalance -const RETRY_LIMIT = 3500 // Retry 3500 blocks, or about 1 day. /* Utility class for tracking the transactions as they @@ -25,11 +24,10 @@ module.exports = class PendingTransactionTracker extends EventEmitter { super() this.query = new EthQuery(config.provider) this.nonceTracker = config.nonceTracker - + this.retryLimit = config.retryLimit || Infinity this.getBalance = config.getBalance this.getPendingTransactions = config.getPendingTransactions this.publishTransaction = config.publishTransaction - this.giveUpOnTransaction = config.giveUpOnTransaction } // checks if a signed tx is in a block and @@ -44,18 +42,18 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (!txHash) { const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') noTxHashErr.name = 'NoTxHashError' - this.emit('txFailed', txId, noTxHashErr) + this.emit('tx:failed', txId, noTxHashErr) return } block.transactions.forEach((tx) => { - if (tx.hash === txHash) this.emit('txConfirmed', txId) + if (tx.hash === txHash) this.emit('tx:confirmed', txId) }) }) } - queryPendingTxs ({oldBlock, newBlock}) { + queryPendingTxs ({ oldBlock, newBlock }) { // check pending transactions on start if (!oldBlock) { this._checkPendingTxs() @@ -96,7 +94,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { // ignore resubmit warnings, return early if (isKnownTx) return // encountered real error - transition to error state - this.emit('txFailed', txMeta.id, err) + this.emit('tx:failed', txMeta.id, err) })) } @@ -104,16 +102,16 @@ module.exports = class PendingTransactionTracker extends EventEmitter { const address = txMeta.txParams.from const balance = this.getBalance(address) if (balance === undefined) return - if (!('retryCount' in txMeta)) txMeta.retryCount = 0 - if (txMeta.retryCount > RETRY_LIMIT) { - return this.giveUpOnTransaction(txMeta.id) + if (txMeta.retryCount > this.retryLimit) { + const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`) + return this.emit('tx:failed', txMeta.id, err) } // if the value of the transaction is greater then the balance, fail. if (!sufficientBalance(txMeta.txParams, balance)) { const insufficientFundsError = new Error('Insufficient balance during rebroadcast.') - this.emit('txFailed', txMeta.id, insufficientFundsError) + this.emit('tx:failed', txMeta.id, insufficientFundsError) log.error(insufficientFundsError) return } @@ -125,7 +123,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { const txHash = await this.publishTransaction(rawTx) // Increment successful tries: - txMeta.retryCount++ + this.emit('tx:retry', txMeta) return txHash } @@ -137,7 +135,7 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (!txHash) { const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') noTxHashErr.name = 'NoTxHashError' - this.emit('txFailed', txId, noTxHashErr) + this.emit('tx:failed', txId, noTxHashErr) return } // get latest transaction status @@ -146,14 +144,14 @@ module.exports = class PendingTransactionTracker extends EventEmitter { txParams = await this.query.getTransactionByHash(txHash) if (!txParams) return if (txParams.blockNumber) { - this.emit('txConfirmed', txId) + this.emit('tx:confirmed', txId) } } catch (err) { txMeta.warning = { error: err, message: 'There was a problem loading this transaction.', } - this.emit('txWarning', txMeta) + this.emit('tx:warning', txMeta) throw err } } diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-gas-utils.js index 5af078dc4..41f67e230 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-gas-utils.js @@ -1,6 +1,4 @@ const EthQuery = require('ethjs-query') -const Transaction = require('ethereumjs-tx') -const normalize = require('eth-sig-util').normalize const { hexToBn, BnMultiplyByFraction, @@ -78,26 +76,6 @@ module.exports = class txProvideUtil { return bnToHex(upperGasLimitBn) } - // builds ethTx from txParams object - buildEthTxFromParams (txParams) { - // normalize values - txParams.to = normalize(txParams.to) - txParams.from = normalize(txParams.from) - txParams.value = normalize(txParams.value) - txParams.data = normalize(txParams.data) - txParams.gas = normalize(txParams.gas || txParams.gasLimit) - txParams.gasPrice = normalize(txParams.gasPrice) - txParams.nonce = normalize(txParams.nonce) - // build ethTx - log.info(`Prepared tx for signing: ${JSON.stringify(txParams)}`) - const ethTx = new Transaction(txParams) - return ethTx - } - - async publishTransaction (rawTx) { - return await this.query.sendRawTransaction(rawTx) - } - async validateTxParams (txParams) { if (('value' in txParams) && txParams.value.indexOf('-') === 0) { throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js new file mode 100644 index 000000000..abb9d7910 --- /dev/null +++ b/app/scripts/lib/tx-state-manager.js @@ -0,0 +1,245 @@ +const extend = require('xtend') +const EventEmitter = require('events') +const ObservableStore = require('obs-store') +const ethUtil = require('ethereumjs-util') +const txStateHistoryHelper = require('./tx-state-history-helper') + +module.exports = class TransactionStateManger extends EventEmitter { + constructor ({ initState, txHistoryLimit, getNetwork }) { + super() + + this.store = new ObservableStore( + extend({ + transactions: [], + }, initState)) + this.txHistoryLimit = txHistoryLimit + this.getNetwork = getNetwork + } + + // Returns the number of txs for the current network. + getTxCount () { + return this.getTxList().length + } + + getTxList () { + const network = this.getNetwork() + const fullTxList = this.getFullTxList() + return fullTxList.filter((txMeta) => txMeta.metamaskNetworkId === network) + } + + getFullTxList () { + return this.store.getState().transactions + } + + // Returns the tx list + getUnapprovedTxList () { + const txList = this.getTxsByMetaData('status', 'unapproved') + return txList.reduce((result, tx) => { + result[tx.id] = tx + return result + }, {}) + } + + getPendingTransactions (address) { + const opts = { status: 'submitted' } + if (address) opts.from = address + return this.getFilteredTxList(opts) + } + + addTx (txMeta) { + this.once(`${txMeta.id}:signed`, function (txId) { + this.removeAllListeners(`${txMeta.id}:rejected`) + }) + this.once(`${txMeta.id}:rejected`, function (txId) { + this.removeAllListeners(`${txMeta.id}:signed`) + }) + // initialize history + txMeta.history = [] + // capture initial snapshot of txMeta for history + const snapshot = txStateHistoryHelper.snapshotFromTxMeta(txMeta) + txMeta.history.push(snapshot) + + const transactions = this.getFullTxList() + const txCount = this.getTxCount() + const txHistoryLimit = this.txHistoryLimit + + // checks if the length of the tx history is + // longer then desired persistence limit + // and then if it is removes only confirmed + // or rejected tx's. + // not tx's that are pending or unapproved + if (txCount > txHistoryLimit - 1) { + const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') + transactions.splice(index, 1) + } + transactions.push(txMeta) + this._saveTxList(transactions) + return txMeta + } + // gets tx by Id and returns it + getTx (txId) { + const txMeta = this.getTxsByMetaData('id', txId)[0] + return txMeta + } + + updateTx (txMeta) { + if (txMeta.txParams) { + Object.keys(txMeta.txParams).forEach((key) => { + let value = txMeta.txParams[key] + if (typeof value !== 'string') console.error(`${key}: ${value} in txParams is not a string`) + if (!ethUtil.isHexPrefixed(value)) console.error('is not hex prefixed, anything on txParams must be hex prefixed') + }) + } + + // create txMeta snapshot for history + const currentState = txStateHistoryHelper.snapshotFromTxMeta(txMeta) + // recover previous tx state obj + const previousState = txStateHistoryHelper.replayHistory(txMeta.history) + // generate history entry and add to history + const entry = txStateHistoryHelper.generateHistoryEntry(previousState, currentState) + txMeta.history.push(entry) + + // commit txMeta to state + const txId = txMeta.id + const txList = this.getFullTxList() + const index = txList.findIndex(txData => txData.id === txId) + txList[index] = txMeta + this._saveTxList(txList) + } + + + // merges txParams obj onto txData.txParams + // use extend to ensure that all fields are filled + updateTxParams (txId, txParams) { + const txMeta = this.getTx(txId) + txMeta.txParams = extend(txMeta.txParams, txParams) + this.updateTx(txMeta) + } + +/* + Takes an object of fields to search for eg: + let thingsToLookFor = { + to: '0x0..', + from: '0x0..', + status: 'signed', + err: undefined, + } + and returns a list of tx with all + options matching + + ****************HINT**************** + | `err: undefined` is like looking | + | for a tx with no err | + | so you can also search txs that | + | dont have something as well by | + | setting the value as undefined | + ************************************ + + this is for things like filtering a the tx list + for only tx's from 1 account + or for filltering for all txs from one account + and that have been 'confirmed' + */ + getFilteredTxList (opts, initialList) { + let filteredTxList = initialList + Object.keys(opts).forEach((key) => { + filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList) + }) + return filteredTxList + } + + getTxsByMetaData (key, value, txList = this.getTxList()) { + return txList.filter((txMeta) => { + if (txMeta.txParams[key]) { + return txMeta.txParams[key] === value + } else { + return txMeta[key] === value + } + }) + } + + // STATUS METHODS + // statuses: + // - `'unapproved'` the user has not responded + // - `'rejected'` the user has responded no! + // - `'approved'` the user has approved the tx + // - `'signed'` the tx is signed + // - `'submitted'` the tx is sent to a server + // - `'confirmed'` the tx has been included in a block. + // - `'failed'` the tx failed for some reason, included on tx data. + + // get::set status + + // should return the status of the tx. + getTxStatus (txId) { + const txMeta = this.getTx(txId) + return txMeta.status + } + + // should update the status of the tx to 'rejected'. + setTxStatusRejected (txId) { + this._setTxStatus(txId, 'rejected') + } + + // should update the status of the tx to 'approved'. + setTxStatusApproved (txId) { + this._setTxStatus(txId, 'approved') + } + + // should update the status of the tx to 'signed'. + setTxStatusSigned (txId) { + this._setTxStatus(txId, 'signed') + } + + // should update the status of the tx to 'submitted'. + setTxStatusSubmitted (txId) { + this._setTxStatus(txId, 'submitted') + } + + // should update the status of the tx to 'confirmed'. + setTxStatusConfirmed (txId) { + this._setTxStatus(txId, 'confirmed') + } + + setTxStatusFailed (txId, err) { + const txMeta = this.getTx(txId) + txMeta.err = { + message: err.toString(), + stack: err.stack, + } + this.updateTx(txMeta) + this._setTxStatus(txId, 'failed') + } + +// +// PRIVATE METHODS +// + + // Should find the tx in the tx list and + // update it. + // should set the status in txData + // - `'unapproved'` the user has not responded + // - `'rejected'` the user has responded no! + // - `'approved'` the user has approved the tx + // - `'signed'` the tx is signed + // - `'submitted'` the tx is sent to a server + // - `'confirmed'` the tx has been included in a block. + // - `'failed'` the tx failed for some reason, included on tx data. + _setTxStatus (txId, status) { + const txMeta = this.getTx(txId) + txMeta.status = status + this.emit(`${txMeta.id}:${status}`, txId) + this.emit(`tx:status-update`, txId, status) + if (status === 'submitted' || status === 'rejected') { + this.emit(`${txMeta.id}:finished`, txMeta) + } + this.updateTx(txMeta) + this.emit('update:badge') + } + + // Saves the new/updated txList. + // Function is intended only for internal use + _saveTxList (transactions) { + this.store.updateState({ transactions }) + } +}
\ No newline at end of file diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 42248827f..5b3161bc6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -4,7 +4,7 @@ const promiseToCallback = require('promise-to-callback') const pump = require('pump') const Dnode = require('dnode') const ObservableStore = require('obs-store') -const EthStore = require('./lib/eth-store') +const AccountTracker = require('./lib/account-tracker') const EthQuery = require('eth-query') const RpcEngine = require('json-rpc-engine') const debounce = require('debounce') @@ -14,7 +14,7 @@ const createOriginMiddleware = require('./lib/createOriginMiddleware') const createLoggerMiddleware = require('./lib/createLoggerMiddleware') const createProviderMiddleware = require('./lib/createProviderMiddleware') const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex -const KeyringController = require('./keyring-controller') +const KeyringController = require('eth-keyring-controller') const NetworkController = require('./controllers/network') const PreferencesController = require('./controllers/preferences') const CurrencyController = require('./controllers/currency') @@ -26,6 +26,7 @@ const BlacklistController = require('./controllers/blacklist') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TransactionController = require('./controllers/transactions') +const BalancesController = require('./controllers/computed-balances') const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') @@ -81,11 +82,12 @@ module.exports = class MetamaskController extends EventEmitter { // rpc provider this.provider = this.initializeProvider() - this.blockTracker = this.provider + this.blockTracker = this.provider._blockTracker // eth data query tools this.ethQuery = new EthQuery(this.provider) - this.ethStore = new EthStore({ + // account tracker watches balances, nonces, and any code at their address. + this.accountTracker = new AccountTracker({ provider: this.provider, blockTracker: this.blockTracker, }) @@ -93,12 +95,17 @@ module.exports = class MetamaskController extends EventEmitter { // key mgmt this.keyringController = new KeyringController({ initState: initState.KeyringController, - ethStore: this.ethStore, + accountTracker: this.accountTracker, getNetwork: this.networkController.getNetworkState.bind(this.networkController), encryptor: opts.encryptor || undefined, }) + this.keyringController.on('newAccount', (address) => { this.preferencesController.setSelectedAddress(address) + this.accountTracker.addAccount(address) + }) + this.keyringController.on('removedAccount', (address) => { + this.accountTracker.removeAccount(address) }) // address book controller @@ -117,10 +124,21 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, ethQuery: this.ethQuery, - ethStore: this.ethStore, + accountTracker: this.accountTracker, }) this.txController.on('newUnaprovedTx', opts.showUnapprovedTx.bind(opts)) + // computed balances (accounting for pending transactions) + this.balancesController = new BalancesController({ + accountTracker: this.accountTracker, + txController: this.txController, + blockTracker: this.blockTracker, + }) + this.networkController.on('networkDidChange', () => { + this.balancesController.updateAllBalances() + }) + this.balancesController.updateAllBalances() + // notices this.noticeController = new NoticeController({ initState: initState.NoticeController, @@ -172,8 +190,9 @@ module.exports = class MetamaskController extends EventEmitter { // manual mem state subscriptions this.networkController.store.subscribe(this.sendUpdate.bind(this)) - this.ethStore.subscribe(this.sendUpdate.bind(this)) + this.accountTracker.store.subscribe(this.sendUpdate.bind(this)) this.txController.memStore.subscribe(this.sendUpdate.bind(this)) + this.balancesController.store.subscribe(this.sendUpdate.bind(this)) this.messageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this)) this.keyringController.memStore.subscribe(this.sendUpdate.bind(this)) @@ -213,8 +232,7 @@ module.exports = class MetamaskController extends EventEmitter { processTransaction: nodeify(async (txParams) => await this.txController.newUnapprovedTransaction(txParams), this), // old style msg signing processMessage: this.newUnsignedMessage.bind(this), - - // new style msg signing + // personal_sign msg signing processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), }) } @@ -248,16 +266,18 @@ module.exports = class MetamaskController extends EventEmitter { const wallet = this.configManager.getWallet() const vault = this.keyringController.store.getState().vault const isInitialized = (!!wallet || !!vault) + return extend( { isInitialized, }, this.networkController.store.getState(), - this.ethStore.getState(), + this.accountTracker.store.getState(), this.txController.memStore.getState(), this.messageManager.memStore.getState(), this.personalMessageManager.memStore.getState(), this.keyringController.memStore.getState(), + this.balancesController.store.getState(), this.preferencesController.store.getState(), this.addressBookController.store.getState(), this.currencyController.store.getState(), diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js index 253aa3d9d..7a4578ea7 100644 --- a/app/scripts/migrations/_multi-keyring.js +++ b/app/scripts/migrations/_multi-keyring.js @@ -10,7 +10,7 @@ which we dont have access to at the time of this writing. const ObservableStore = require('obs-store') const ConfigManager = require('../../app/scripts/lib/config-manager') const IdentityStoreMigrator = require('../../app/scripts/lib/idStore-migrator') -const KeyringController = require('../../app/scripts/lib/keyring-controller') +const KeyringController = require('eth-keyring-controller') const password = 'obviously not correct' |