diff options
Diffstat (limited to 'app/scripts/lib')
-rw-r--r-- | app/scripts/lib/account-tracker.js (renamed from app/scripts/lib/eth-store.js) | 76 | ||||
-rw-r--r-- | app/scripts/lib/createLoggerMiddleware.js | 15 | ||||
-rw-r--r-- | app/scripts/lib/createOriginMiddleware.js | 9 | ||||
-rw-r--r-- | app/scripts/lib/createProviderMiddleware.js | 13 | ||||
-rw-r--r-- | app/scripts/lib/inpage-provider.js | 83 | ||||
-rw-r--r-- | app/scripts/lib/obj-multiplex.js | 48 | ||||
-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/port-stream.js | 16 | ||||
-rw-r--r-- | app/scripts/lib/stream-utils.js | 24 | ||||
-rw-r--r-- | app/scripts/lib/tx-state-manager.js | 20 |
11 files changed, 185 insertions, 198 deletions
diff --git a/app/scripts/lib/eth-store.js b/app/scripts/lib/account-tracker.js index ebba98f5c..e2892b1ce 100644 --- a/app/scripts/lib/eth-store.js +++ b/app/scripts/lib/account-tracker.js @@ -1,4 +1,4 @@ -/* Ethereum Store +/* Account Tracker * * This module is responsible for tracking any number of accounts * and caching their current balances & transaction counts. @@ -10,19 +10,21 @@ const async = require('async') const EthQuery = require('eth-query') const ObservableStore = require('obs-store') +const EventEmitter = require('events').EventEmitter function noop () {} -class EthereumStore extends ObservableStore { +class AccountTracker extends EventEmitter { constructor (opts = {}) { - super({ + super() + + const initState = { accounts: {}, - transactions: {}, - currentBlockNumber: '0', - currentBlockHash: '', currentBlockGasLimit: '', - }) + } + this.store = new ObservableStore(initState) + this._provider = opts.provider this._query = new EthQuery(this._provider) this._blockTracker = opts.blockTracker @@ -37,34 +39,19 @@ class EthereumStore extends ObservableStore { // addAccount (address) { - const accounts = this.getState().accounts + const accounts = this.store.getState().accounts accounts[address] = {} - this.updateState({ accounts }) + this.store.updateState({ accounts }) if (!this._currentBlockNumber) return this._updateAccount(address) } removeAccount (address) { - const accounts = this.getState().accounts + const accounts = this.store.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 }) + this.store.updateState({ accounts }) } - // // private // @@ -72,53 +59,32 @@ class EthereumStore extends ObservableStore { _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')}` }) + + this.store.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()) + this.emit('block', this.store.getState()) }) } _updateAccounts (cb = noop) { - const accounts = this.getState().accounts + const accounts = this.store.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 + const accounts = this.store.getState().accounts // 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 }) + this.store.updateState({ accounts }) } cb(null, result) }) @@ -135,4 +101,4 @@ class EthereumStore extends ObservableStore { } -module.exports = EthereumStore +module.exports = AccountTracker diff --git a/app/scripts/lib/createLoggerMiddleware.js b/app/scripts/lib/createLoggerMiddleware.js new file mode 100644 index 000000000..b92a965de --- /dev/null +++ b/app/scripts/lib/createLoggerMiddleware.js @@ -0,0 +1,15 @@ +// log rpc activity +module.exports = createLoggerMiddleware + +function createLoggerMiddleware({ origin }) { + return function loggerMiddleware (req, res, next, end) { + next((cb) => { + if (res.error) { + log.error('Error in RPC response:\n', res) + } + if (req.isMetamaskInternal) return + log.info(`RPC (${origin}):`, req, '->', res) + cb() + }) + } +}
\ No newline at end of file diff --git a/app/scripts/lib/createOriginMiddleware.js b/app/scripts/lib/createOriginMiddleware.js new file mode 100644 index 000000000..e1e097cc4 --- /dev/null +++ b/app/scripts/lib/createOriginMiddleware.js @@ -0,0 +1,9 @@ +// append dapp origin domain to request +module.exports = createOriginMiddleware + +function createOriginMiddleware({ origin }) { + return function originMiddleware (req, res, next, end) { + req.origin = origin + next() + } +}
\ No newline at end of file diff --git a/app/scripts/lib/createProviderMiddleware.js b/app/scripts/lib/createProviderMiddleware.js new file mode 100644 index 000000000..6dd192411 --- /dev/null +++ b/app/scripts/lib/createProviderMiddleware.js @@ -0,0 +1,13 @@ + +module.exports = createProviderMiddleware + +// forward requests to provider +function createProviderMiddleware({ provider }) { + return (req, res, next, end) => { + provider.sendAsync(req, (err, _res) => { + if (err) return end(err) + res.result = _res.result + end() + }) + } +}
\ No newline at end of file diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index c63af06dc..da75c4be2 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -1,8 +1,9 @@ -const pipe = require('pump') -const StreamProvider = require('web3-stream-provider') +const pump = require('pump') +const RpcEngine = require('json-rpc-engine') +const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware') +const createStreamMiddleware = require('json-rpc-middleware-stream') const LocalStorageStore = require('obs-store') -const ObjectMultiplex = require('./obj-multiplex') -const createRandomId = require('./random-id') +const ObjectMultiplex = require('obj-multiplex') module.exports = MetamaskInpageProvider @@ -10,61 +11,49 @@ function MetamaskInpageProvider (connectionStream) { const self = this // setup connectionStream multiplexing - var multiStream = self.multiStream = ObjectMultiplex() - pipe( + const mux = self.mux = new ObjectMultiplex() + pump( connectionStream, - multiStream, + mux, connectionStream, (err) => logStreamDisconnectWarning('MetaMask', err) ) // subscribe to metamask public config (one-way) self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' }) - pipe( - multiStream.createStream('publicConfig'), + pump( + mux.createStream('publicConfig'), self.publicConfigStore, (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) ) // ignore phishing warning message (handled elsewhere) - multiStream.ignoreStream('phishing') + mux.ignoreStream('phishing') // connect to async provider - const asyncProvider = self.asyncProvider = new StreamProvider() - pipe( - asyncProvider, - multiStream.createStream('provider'), - asyncProvider, + const streamMiddleware = createStreamMiddleware() + pump( + streamMiddleware.stream, + mux.createStream('provider'), + streamMiddleware.stream, (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err) ) - // start and stop polling to unblock first block lock - - self.idMap = {} - // handle sendAsync requests via asyncProvider - self.sendAsync = function (payload, cb) { - // rewrite request ids - var request = eachJsonMessage(payload, (_message) => { - const message = Object.assign({}, _message) - const newId = createRandomId() - self.idMap[newId] = message.id - message.id = newId - return message - }) - // forward to asyncProvider - asyncProvider.sendAsync(request, function (err, res) { - if (err) return cb(err) - // transform messages to original ids - eachJsonMessage(res, (message) => { - var oldId = self.idMap[message.id] - delete self.idMap[message.id] - message.id = oldId - return message - }) - cb(null, res) - }) - } + + // handle sendAsync requests via dapp-side rpc engine + const rpcEngine = new RpcEngine() + rpcEngine.push(createIdRemapMiddleware()) + rpcEngine.push(streamMiddleware) + self.rpcEngine = rpcEngine +} + +// handle sendAsync requests via asyncProvider +// also remap ids inbound and outbound +MetamaskInpageProvider.prototype.sendAsync = function (payload, cb) { + const self = this + self.rpcEngine.handle(payload, cb) } + MetamaskInpageProvider.prototype.send = function (payload) { const self = this @@ -110,10 +99,6 @@ MetamaskInpageProvider.prototype.send = function (payload) { } } -MetamaskInpageProvider.prototype.sendAsync = function () { - throw new Error('MetamaskInpageProvider - sendAsync not overwritten') -} - MetamaskInpageProvider.prototype.isConnected = function () { return true } @@ -122,14 +107,6 @@ MetamaskInpageProvider.prototype.isMetaMask = true // util -function eachJsonMessage (payload, transformFn) { - if (Array.isArray(payload)) { - return payload.map(transformFn) - } else { - return transformFn(payload) - } -} - function logStreamDisconnectWarning (remoteLabel, err) { let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}` if (err) warningMsg += '\n' + err.stack diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js deleted file mode 100644 index 0034febe0..000000000 --- a/app/scripts/lib/obj-multiplex.js +++ /dev/null @@ -1,48 +0,0 @@ -const through = require('through2') - -module.exports = ObjectMultiplex - -function ObjectMultiplex (opts) { - opts = opts || {} - // create multiplexer - const mx = through.obj(function (chunk, enc, cb) { - const name = chunk.name - const data = chunk.data - if (!name) { - console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`) - return cb() - } - const substream = mx.streams[name] - if (!substream) { - console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`) - } else { - if (substream.push) substream.push(data) - } - return cb() - }) - mx.streams = {} - // create substreams - mx.createStream = function (name) { - const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { - mx.push({ - name: name, - data: chunk, - }) - return cb() - }) - mx.on('end', function () { - return substream.emit('end') - }) - if (opts.error) { - mx.on('error', function () { - return substream.emit('error') - }) - } - return substream - } - // ignore streams (dont display orphaned data warning) - mx.ignoreStream = function (name) { - mx.streams[name] = true - } - return mx -} 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 cbc3f47e6..31cf9babb 100644 --- a/app/scripts/lib/pending-tx-tracker.js +++ b/app/scripts/lib/pending-tx-tracker.js @@ -42,13 +42,13 @@ 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) }) }) } @@ -74,6 +74,9 @@ module.exports = class PendingTransactionTracker extends EventEmitter { Dont marked as failed if the error is a "known" transaction warning "there is already a transaction with the same sender-nonce but higher/same gas price" + + Also don't mark as failed if it has ever been broadcast successfully. + A successful broadcast means it may still be mined. */ const errorMessage = err.message.toLowerCase() const isKnownTx = ( @@ -86,11 +89,12 @@ module.exports = class PendingTransactionTracker extends EventEmitter { // other || errorMessage.includes('gateway timeout') || errorMessage.includes('nonce too low') + || txMeta.retryCount > 1 ) // 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) })) } @@ -102,13 +106,13 @@ module.exports = class PendingTransactionTracker extends EventEmitter { if (txMeta.retryCount > this.retryLimit) { const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`) - return this.emit('txFailed', txMeta.id, err) + 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 } @@ -116,10 +120,12 @@ module.exports = class PendingTransactionTracker extends EventEmitter { // Only auto-submit already-signed txs: if (!('rawTx' in txMeta)) return - // Increment a try counter. - txMeta.retryCount++ const rawTx = txMeta.rawTx - return await this.publishTransaction(rawTx) + const txHash = await this.publishTransaction(rawTx) + + // Increment successful tries: + txMeta.retryCount++ + return txHash } async _checkPendingTx (txMeta) { @@ -130,7 +136,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 @@ -139,14 +145,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/port-stream.js b/app/scripts/lib/port-stream.js index 607a9c9ed..648d88087 100644 --- a/app/scripts/lib/port-stream.js +++ b/app/scripts/lib/port-stream.js @@ -1,5 +1,6 @@ const Duplex = require('readable-stream').Duplex const inherits = require('util').inherits +const noop = function(){} module.exports = PortDuplexStream @@ -20,20 +21,14 @@ PortDuplexStream.prototype._onMessage = function (msg) { if (Buffer.isBuffer(msg)) { delete msg._isBuffer var data = new Buffer(msg) - // console.log('PortDuplexStream - saw message as buffer', data) this.push(data) } else { - // console.log('PortDuplexStream - saw message', msg) this.push(msg) } } PortDuplexStream.prototype._onDisconnect = function () { - try { - this.push(null) - } catch (err) { - this.emit('error', err) - } + this.destroy() } // stream plumbing @@ -45,19 +40,12 @@ PortDuplexStream.prototype._write = function (msg, encoding, cb) { if (Buffer.isBuffer(msg)) { var data = msg.toJSON() data._isBuffer = true - // console.log('PortDuplexStream - sent message as buffer', data) this._port.postMessage(data) } else { - // console.log('PortDuplexStream - sent message', msg) this._port.postMessage(msg) } } catch (err) { - // console.error(err) return cb(new Error('PortDuplexStream - disconnected')) } cb() } - -// util - -function noop () {} diff --git a/app/scripts/lib/stream-utils.js b/app/scripts/lib/stream-utils.js index ba79990cc..8bb0b4f3c 100644 --- a/app/scripts/lib/stream-utils.js +++ b/app/scripts/lib/stream-utils.js @@ -1,6 +1,6 @@ const Through = require('through2') -const endOfStream = require('end-of-stream') -const ObjectMultiplex = require('./obj-multiplex') +const ObjectMultiplex = require('obj-multiplex') +const pump = require('pump') module.exports = { jsonParseStream: jsonParseStream, @@ -23,14 +23,14 @@ function jsonStringifyStream () { } function setupMultiplex (connectionStream) { - var mx = ObjectMultiplex() - connectionStream.pipe(mx).pipe(connectionStream) - endOfStream(mx, function (err) { - if (err) console.error(err) - }) - endOfStream(connectionStream, function (err) { - if (err) console.error(err) - mx.destroy() - }) - return mx + const mux = new ObjectMultiplex() + pump( + connectionStream, + mux, + connectionStream, + (err) => { + if (err) console.error(err) + } + ) + return mux } diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js index 843592504..abb9d7910 100644 --- a/app/scripts/lib/tx-state-manager.js +++ b/app/scripts/lib/tx-state-manager.js @@ -1,10 +1,11 @@ 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}) { + constructor ({ initState, txHistoryLimit, getNetwork }) { super() this.store = new ObservableStore( @@ -14,7 +15,8 @@ module.exports = class TransactionStateManger extends EventEmitter { this.txHistoryLimit = txHistoryLimit this.getNetwork = getNetwork } - // Returns the number of txs for the current network. + + // Returns the number of txs for the current network. getTxCount () { return this.getTxList().length } @@ -30,7 +32,6 @@ module.exports = class TransactionStateManger extends EventEmitter { } // Returns the tx list - getUnapprovedTxList () { const txList = this.getTxsByMetaData('status', 'unapproved') return txList.reduce((result, tx) => { @@ -68,7 +69,7 @@ module.exports = class TransactionStateManger extends EventEmitter { // 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'))) + const index = transactions.findIndex((metaTx) => metaTx.status === 'confirmed' || metaTx.status === 'rejected') transactions.splice(index, 1) } transactions.push(txMeta) @@ -82,6 +83,14 @@ module.exports = class TransactionStateManger extends EventEmitter { } 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 @@ -220,11 +229,12 @@ module.exports = class TransactionStateManger extends EventEmitter { 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('updateBadge') + this.emit('update:badge') } // Saves the new/updated txList. |