aboutsummaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorfrankiebee <frankie.diamond@gmail.com>2017-09-27 07:24:43 +0800
committerfrankiebee <frankie.diamond@gmail.com>2017-09-27 07:24:43 +0800
commit8ab23c713db1f5d45abb0ba433450591b8759809 (patch)
treec192fcf5eec3c7b422f3d0a91a4a96037c7f2fa6 /app
parent77a48fb0b16d7415377b02a3b7d383c9ef86fcb6 (diff)
parent6ca519e97c4c282023ab6b7788715ff8d7ec8189 (diff)
downloadtangerine-wallet-browser-8ab23c713db1f5d45abb0ba433450591b8759809.tar
tangerine-wallet-browser-8ab23c713db1f5d45abb0ba433450591b8759809.tar.gz
tangerine-wallet-browser-8ab23c713db1f5d45abb0ba433450591b8759809.tar.bz2
tangerine-wallet-browser-8ab23c713db1f5d45abb0ba433450591b8759809.tar.lz
tangerine-wallet-browser-8ab23c713db1f5d45abb0ba433450591b8759809.tar.xz
tangerine-wallet-browser-8ab23c713db1f5d45abb0ba433450591b8759809.tar.zst
tangerine-wallet-browser-8ab23c713db1f5d45abb0ba433450591b8759809.zip
Merge branch 'master' into transactionControllerRefractorPt3
Diffstat (limited to 'app')
-rw-r--r--app/manifest.json7
-rw-r--r--app/scripts/background.js8
-rw-r--r--app/scripts/contentscript.js45
-rw-r--r--app/scripts/controllers/balance.js61
-rw-r--r--app/scripts/controllers/computed-balances.js66
-rw-r--r--app/scripts/controllers/transactions.js6
-rw-r--r--app/scripts/keyring-controller.js16
-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.js15
-rw-r--r--app/scripts/lib/createOriginMiddleware.js9
-rw-r--r--app/scripts/lib/createProviderMiddleware.js13
-rw-r--r--app/scripts/lib/inpage-provider.js83
-rw-r--r--app/scripts/lib/obj-multiplex.js48
-rw-r--r--app/scripts/lib/pending-balance-calculator.js51
-rw-r--r--app/scripts/lib/pending-tx-tracker.js12
-rw-r--r--app/scripts/lib/port-stream.js16
-rw-r--r--app/scripts/lib/stream-utils.js24
-rw-r--r--app/scripts/lib/tx-state-manager.js1
-rw-r--r--app/scripts/metamask-controller.js127
19 files changed, 430 insertions, 254 deletions
diff --git a/app/manifest.json b/app/manifest.json
index 7ec32ea78..fd07f15a9 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.9.11",
+ "version": "3.10.3",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
@@ -57,9 +57,8 @@
"permissions": [
"storage",
"clipboardWrite",
- "http://localhost:8545/",
- "https://api.cryptonator.com/"
- ],
+ "http://localhost:8545/"
+ ],
"web_accessible_resources": [
"scripts/inpage.js"
],
diff --git a/app/scripts/background.js b/app/scripts/background.js
index f077ca7a8..1b96d68b5 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -1,6 +1,8 @@
const urlUtil = require('url')
const endOfStream = require('end-of-stream')
const pipe = require('pump')
+const log = require('loglevel')
+const extension = require('extensionizer')
const LocalStorageStore = require('obs-store/lib/localStorage')
const storeTransform = require('obs-store/lib/transform')
const ExtensionPlatform = require('./platforms/extension')
@@ -9,13 +11,11 @@ const migrations = require('./migrations/')
const PortStream = require('./lib/port-stream.js')
const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller')
-const extension = require('extensionizer')
const firstTimeState = require('./first-time-state')
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
-const log = require('loglevel')
window.log = log
log.setDefaultLevel(METAMASK_DEBUG ? 'debug' : 'warn')
@@ -29,12 +29,12 @@ let popupIsOpen = false
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
// initialization flow
-initialize().catch(console.error)
+initialize().catch(log.error)
async function initialize () {
const initState = await loadStateFromPersistence()
await setupController(initState)
- console.log('MetaMask initialization complete.')
+ log.debug('MetaMask initialization complete.')
}
//
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index acacf5d4c..90a0f1f22 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -1,11 +1,12 @@
+const fs = require('fs')
+const path = require('path')
+const pump = require('pump')
const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
-const PortStream = require('./lib/port-stream.js')
-const ObjectMultiplex = require('./lib/obj-multiplex')
+const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
+const PortStream = require('./lib/port-stream.js')
-const fs = require('fs')
-const path = require('path')
const inpageText = fs.readFileSync(path.join(__dirname, 'inpage.js')).toString()
// Eventually this streaming injection could be replaced with:
@@ -50,22 +51,42 @@ function setupStreams () {
pageStream.pipe(pluginStream).pipe(pageStream)
// setup local multistream channels
- const mx = ObjectMultiplex()
- mx.on('error', console.error)
- mx.pipe(pageStream).pipe(mx)
- mx.pipe(pluginStream).pipe(mx)
+ const mux = new ObjectMultiplex()
+ pump(
+ mux,
+ pageStream,
+ mux,
+ (err) => logStreamDisconnectWarning('MetaMask Inpage', err)
+ )
+ pump(
+ mux,
+ pluginStream,
+ mux,
+ (err) => logStreamDisconnectWarning('MetaMask Background', err)
+ )
// connect ping stream
const pongStream = new PongStream({ objectMode: true })
- pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream)
+ pump(
+ mux,
+ pongStream,
+ mux,
+ (err) => logStreamDisconnectWarning('MetaMask PingPongStream', err)
+ )
// connect phishing warning stream
- const phishingStream = mx.createStream('phishing')
+ const phishingStream = mux.createStream('phishing')
phishingStream.once('data', redirectToPhishingWarning)
// ignore unused channels (handled by background, inpage)
- mx.ignoreStream('provider')
- mx.ignoreStream('publicConfig')
+ mux.ignoreStream('provider')
+ mux.ignoreStream('publicConfig')
+}
+
+function logStreamDisconnectWarning (remoteLabel, err) {
+ let warningMsg = `MetamaskContentscript - lost connection to ${remoteLabel}`
+ if (err) warningMsg += '\n' + err.stack
+ console.warn(warningMsg)
}
function shouldInjectWeb3 () {
diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js
new file mode 100644
index 000000000..964dff0df
--- /dev/null
+++ b/app/scripts/controllers/balance.js
@@ -0,0 +1,61 @@
+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('submitted', update)
+ this.txController.on('confirmed', update)
+ this.txController.on('failed', update)
+ 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/transactions.js b/app/scripts/controllers/transactions.js
index 3ee1c22aa..de5fa5b20 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -32,7 +32,7 @@ module.exports = class TransactionController extends EventEmitter {
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)
@@ -61,7 +61,7 @@ module.exports = class TransactionController extends EventEmitter {
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.getState().accounts[address]
if (!account) return
return account.balance
},
@@ -78,7 +78,7 @@ module.exports = class TransactionController extends EventEmitter {
this.blockTracker.on('rawBlock', 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))
})
diff --git a/app/scripts/keyring-controller.js b/app/scripts/keyring-controller.js
index 2edc8060e..34e008ec4 100644
--- a/app/scripts/keyring-controller.js
+++ b/app/scripts/keyring-controller.js
@@ -35,8 +35,9 @@ class KeyringController extends EventEmitter {
keyrings: [],
identities: {},
})
- this.ethStore = opts.ethStore
- this.encryptor = encryptor
+
+ this.accountTracker = opts.accountTracker
+ this.encryptor = opts.encryptor || encryptor
this.keyrings = []
this.getNetwork = opts.getNetwork
}
@@ -171,9 +172,9 @@ class KeyringController extends EventEmitter {
return this.setupAccounts(checkedAccounts)
})
.then(() => this.persistAllKeyrings())
+ .then(() => this._updateMemStoreKeyrings())
.then(() => this.fullUpdate())
.then(() => {
- this._updateMemStoreKeyrings()
return keyring
})
}
@@ -208,6 +209,7 @@ class KeyringController extends EventEmitter {
return selectedKeyring.addAccounts(1)
.then(this.setupAccounts.bind(this))
.then(this.persistAllKeyrings.bind(this))
+ .then(this._updateMemStoreKeyrings.bind(this))
.then(this.fullUpdate.bind(this))
}
@@ -337,7 +339,7 @@ class KeyringController extends EventEmitter {
//
// Initializes the provided account array
// Gives them numerically incremented nicknames,
- // and adds them to the ethStore for regular balance checking.
+ // and adds them to the accountTracker for regular balance checking.
setupAccounts (accounts) {
return this.getAccounts()
.then((loadedAccounts) => {
@@ -360,7 +362,7 @@ class KeyringController extends EventEmitter {
throw new Error('Problem loading account.')
}
const address = normalizeAddress(account)
- this.ethStore.addAccount(address)
+ this.accountTracker.addAccount(address)
return this.createNickname(address)
}
@@ -566,12 +568,12 @@ class KeyringController extends EventEmitter {
clearKeyrings () {
let accounts
try {
- accounts = Object.keys(this.ethStore.getState())
+ accounts = Object.keys(this.accountTracker.getState())
} catch (e) {
accounts = []
}
accounts.forEach((address) => {
- this.ethStore.removeAccount(address)
+ this.accountTracker.removeAccount(address)
})
// clear keyrings from memory
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..4541221d5 100644
--- a/app/scripts/lib/pending-tx-tracker.js
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -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,6 +89,7 @@ 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
@@ -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) {
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 82c0b6131..d7b76fe22 100644
--- a/app/scripts/lib/tx-state-manager.js
+++ b/app/scripts/lib/tx-state-manager.js
@@ -229,6 +229,7 @@ module.exports = class TransactionStateManger extends EventEmitter {
const txMeta = this.getTx(txId)
txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId)
+ this.emit(`${status}`, txId)
if (status === 'submitted' || status === 'rejected') {
this.emit(`${txMeta.id}:finished`, txMeta)
}
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 23049c46d..96b34507f 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -1,12 +1,18 @@
const EventEmitter = require('events')
const extend = require('xtend')
const promiseToCallback = require('promise-to-callback')
-const pipe = require('pump')
+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 streamIntoProvider = require('web3-stream-provider/handler')
+const RpcEngine = require('json-rpc-engine')
+const debounce = require('debounce')
+const createEngineStream = require('json-rpc-middleware-stream/engineStream')
+const createFilterMiddleware = require('eth-json-rpc-filters')
+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 NetworkController = require('./controllers/network')
@@ -20,12 +26,11 @@ 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')
const getBuyEthUrl = require('./lib/buy-eth-url')
-const debounce = require('debounce')
-
const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter {
@@ -77,22 +82,30 @@ module.exports = class MetamaskController extends EventEmitter {
// rpc provider
this.provider = this.initializeProvider()
+ this.blockTracker = this.provider
// 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.provider,
+ blockTracker: this.blockTracker,
})
// 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
@@ -109,12 +122,23 @@ module.exports = class MetamaskController extends EventEmitter {
getNetwork: this.networkController.getNetworkState.bind(this),
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider,
- blockTracker: 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,
@@ -166,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))
@@ -242,16 +267,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(),
@@ -337,36 +364,43 @@ module.exports = class MetamaskController extends EventEmitter {
setupUntrustedCommunication (connectionStream, originDomain) {
// Check if new connection is blacklisted
if (this.blacklistController.checkForPhishing(originDomain)) {
- console.log('MetaMask - sending phishing warning for', originDomain)
+ log.debug('MetaMask - sending phishing warning for', originDomain)
this.sendPhishingWarning(connectionStream, originDomain)
return
}
// setup multiplexing
- const mx = setupMultiplex(connectionStream)
+ const mux = setupMultiplex(connectionStream)
// connect features
- this.setupProviderConnection(mx.createStream('provider'), originDomain)
- this.setupPublicConfig(mx.createStream('publicConfig'))
+ this.setupProviderConnection(mux.createStream('provider'), originDomain)
+ this.setupPublicConfig(mux.createStream('publicConfig'))
}
setupTrustedCommunication (connectionStream, originDomain) {
// setup multiplexing
- const mx = setupMultiplex(connectionStream)
+ const mux = setupMultiplex(connectionStream)
// connect features
- this.setupControllerConnection(mx.createStream('controller'))
- this.setupProviderConnection(mx.createStream('provider'), originDomain)
+ this.setupControllerConnection(mux.createStream('controller'))
+ this.setupProviderConnection(mux.createStream('provider'), originDomain)
}
sendPhishingWarning (connectionStream, hostname) {
- const mx = setupMultiplex(connectionStream)
- const phishingStream = mx.createStream('phishing')
+ const mux = setupMultiplex(connectionStream)
+ const phishingStream = mux.createStream('phishing')
phishingStream.write({ hostname })
}
setupControllerConnection (outStream) {
const api = this.getApi()
const dnode = Dnode(api)
- outStream.pipe(dnode).pipe(outStream)
+ pump(
+ outStream,
+ dnode,
+ outStream,
+ (err) => {
+ if (err) log.error(err)
+ }
+ )
dnode.on('remote', (remote) => {
// push updates to popup
const sendUpdate = remote.sendUpdate.bind(remote)
@@ -374,27 +408,42 @@ module.exports = class MetamaskController extends EventEmitter {
})
}
- setupProviderConnection (outStream, originDomain) {
- streamIntoProvider(outStream, this.provider, onRequest, onResponse)
- // append dapp origin domain to request
- function onRequest (request) {
- request.origin = originDomain
- }
- // log rpc activity
- function onResponse (err, request, response) {
- if (err) return console.error(err)
- if (response.error) {
- console.error('Error in RPC response:\n', response)
+ setupProviderConnection (outStream, origin) {
+ // setup json rpc engine stack
+ const engine = new RpcEngine()
+
+ // create filter polyfill middleware
+ const filterMiddleware = createFilterMiddleware({
+ provider: this.provider,
+ blockTracker: this.blockTracker,
+ })
+
+ engine.push(createOriginMiddleware({ origin }))
+ engine.push(createLoggerMiddleware({ origin }))
+ engine.push(filterMiddleware)
+ engine.push(createProviderMiddleware({ provider: this.provider }))
+
+ // setup connection
+ const providerStream = createEngineStream({ engine })
+ pump(
+ outStream,
+ providerStream,
+ outStream,
+ (err) => {
+ // cleanup filter polyfill middleware
+ filterMiddleware.destroy()
+ if (err) log.error(err)
}
- if (request.isMetamaskInternal) return
- log.info(`RPC (${originDomain}):`, request, '->', response)
- }
+ )
}
setupPublicConfig (outStream) {
- pipe(
+ pump(
this.publicConfigStore,
- outStream
+ outStream,
+ (err) => {
+ if (err) log.error(err)
+ }
)
}
@@ -647,4 +696,4 @@ module.exports = class MetamaskController extends EventEmitter {
return Promise.resolve(rpcTarget)
})
}
-} \ No newline at end of file
+}