aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts
diff options
context:
space:
mode:
authorKevin Serrano <kevgagser@gmail.com>2018-10-15 08:32:29 +0800
committerGitHub <noreply@github.com>2018-10-15 08:32:29 +0800
commit85884b21afe6e5e85b58123b24e72776d1437cc6 (patch)
tree6dbec947089691c0dffd5febbe6e92c1f712e40f /app/scripts
parentdb981b827b07c946e17d3a8aeaefd2143c236ef7 (diff)
parent7f6b488c04e6ea12b2ef24ac936b6a236ad904c2 (diff)
downloadtangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar.gz
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.tar.zst
tangerine-wallet-browser-85884b21afe6e5e85b58123b24e72776d1437cc6.zip
Merge pull request #5512 from MetaMask/v4.14.0
Version 4.14.0
Diffstat (limited to 'app/scripts')
-rw-r--r--app/scripts/background.js33
-rw-r--r--app/scripts/contentscript.js8
-rw-r--r--app/scripts/controllers/balance.js2
-rw-r--r--app/scripts/controllers/blacklist.js22
-rw-r--r--app/scripts/controllers/currency.js2
-rw-r--r--app/scripts/controllers/network/createInfuraClient.js27
-rw-r--r--app/scripts/controllers/network/createJsonRpcClient.js25
-rw-r--r--app/scripts/controllers/network/createLocalhostClient.js21
-rw-r--r--app/scripts/controllers/network/createMetamaskMiddleware.js43
-rw-r--r--app/scripts/controllers/network/network.js122
-rw-r--r--app/scripts/controllers/preferences.js149
-rw-r--r--app/scripts/controllers/recent-blocks.js56
-rw-r--r--app/scripts/controllers/transactions/enums.js12
-rw-r--r--app/scripts/controllers/transactions/index.js122
-rw-r--r--app/scripts/controllers/transactions/nonce-tracker.js28
-rw-r--r--app/scripts/controllers/transactions/pending-tx-tracker.js80
-rw-r--r--app/scripts/controllers/transactions/tx-gas-utils.js2
-rw-r--r--app/scripts/controllers/transactions/tx-state-manager.js1
-rw-r--r--app/scripts/inpage.js38
-rw-r--r--app/scripts/lib/account-tracker.js143
-rw-r--r--app/scripts/lib/auto-reload.js6
-rw-r--r--app/scripts/lib/config-manager.js254
-rw-r--r--app/scripts/lib/createErrorMiddleware.js67
-rw-r--r--app/scripts/lib/events-proxy.js42
-rw-r--r--app/scripts/lib/inpage-provider.js125
-rw-r--r--app/scripts/lib/ipfsContent.js4
-rw-r--r--app/scripts/lib/message-manager.js33
-rw-r--r--app/scripts/lib/personal-message-manager.js37
-rw-r--r--app/scripts/lib/port-stream.js80
-rw-r--r--app/scripts/lib/setupRaven.js4
-rw-r--r--app/scripts/lib/typed-message-manager.js99
-rw-r--r--app/scripts/lib/util.js14
-rw-r--r--app/scripts/metamask-controller.js425
-rw-r--r--app/scripts/migrations/_multi-keyring.js50
-rw-r--r--app/scripts/notice-controller.js2
-rw-r--r--app/scripts/phishing-detect.js59
-rw-r--r--app/scripts/ui.js2
37 files changed, 1156 insertions, 1083 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 3d3afdd4e..0343e134c 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -15,11 +15,11 @@ const asStream = require('obs-store/lib/asStream')
const ExtensionPlatform = require('./platforms/extension')
const Migrator = require('./lib/migrator/')
const migrations = require('./migrations/')
-const PortStream = require('./lib/port-stream.js')
+const PortStream = require('extension-port-stream')
const createStreamSink = require('./lib/createStreamSink')
const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller')
-const firstTimeState = require('./first-time-state')
+const rawFirstTimeState = require('./first-time-state')
const setupRaven = require('./lib/setupRaven')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
@@ -34,8 +34,10 @@ const {
ENVIRONMENT_TYPE_FULLSCREEN,
} = require('./lib/enums')
+// METAMASK_TEST_CONFIG is used in e2e tests to set the default network to localhost
+const firstTimeState = Object.assign({}, rawFirstTimeState, global.METAMASK_TEST_CONFIG)
+
const STORAGE_KEY = 'metamask-config'
-const METAMASK_DEBUG = process.env.METAMASK_DEBUG
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
@@ -253,6 +255,7 @@ function setupController (initState, initLangCode) {
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
+ showWatchAssetUi: showWatchAssetUi,
// initial state
initState,
// initial locale code
@@ -402,6 +405,7 @@ function setupController (initState, initLangCode) {
controller.txController.on('update:badge', updateBadge)
controller.messageManager.on('updateBadge', updateBadge)
controller.personalMessageManager.on('updateBadge', updateBadge)
+ controller.typedMessageManager.on('updateBadge', updateBadge)
/**
* Updates the Web Extension's "badge" number, on the little fox in the toolbar.
@@ -440,9 +444,20 @@ function triggerUi () {
})
}
-// On first install, open a window to MetaMask website to how-it-works.
-extension.runtime.onInstalled.addListener(function (details) {
- if ((details.reason === 'install') && (!METAMASK_DEBUG)) {
- extension.tabs.create({url: 'https://metamask.io/#how-it-works'})
- }
-})
+/**
+ * Opens the browser popup for user confirmation of watchAsset
+ * then it waits until user interact with the UI
+ */
+function showWatchAssetUi () {
+ triggerUi()
+ return new Promise(
+ (resolve) => {
+ var interval = setInterval(() => {
+ if (!notificationIsOpen) {
+ clearInterval(interval)
+ resolve()
+ }
+ }, 1000)
+ }
+ )
+}
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index e0a2b0061..d870741d6 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 querystring = require('querystring')
const LocalMessageDuplexStream = require('post-message-stream')
const PongStream = require('ping-pong-stream/pong')
const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
-const PortStream = require('./lib/port-stream.js')
+const PortStream = require('extension-port-stream')
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString()
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n'
@@ -199,5 +200,8 @@ function blacklistedDomainCheck () {
function redirectToPhishingWarning () {
console.log('MetaMask - routing to Phishing Warning component')
const extensionURL = extension.runtime.getURL('phishing.html')
- window.location.href = extensionURL
+ window.location.href = `${extensionURL}#${querystring.stringify({
+ hostname: window.location.hostname,
+ href: window.location.href,
+ })}`
}
diff --git a/app/scripts/controllers/balance.js b/app/scripts/controllers/balance.js
index 4c97810a3..465751e61 100644
--- a/app/scripts/controllers/balance.js
+++ b/app/scripts/controllers/balance.js
@@ -80,7 +80,7 @@ class BalanceController {
}
})
this.accountTracker.store.subscribe(update)
- this.blockTracker.on('block', update)
+ this.blockTracker.on('latest', update)
}
/**
diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js
index 1d2191433..89c7cc888 100644
--- a/app/scripts/controllers/blacklist.js
+++ b/app/scripts/controllers/blacklist.js
@@ -29,6 +29,7 @@ class BlacklistController {
constructor (opts = {}) {
const initState = extend({
phishing: PHISHING_DETECTION_CONFIG,
+ whitelist: [],
}, opts.initState)
this.store = new ObservableStore(initState)
// phishing detector
@@ -39,6 +40,21 @@ class BlacklistController {
}
/**
+ * Adds the given hostname to the runtime whitelist
+ * @param {string} hostname the hostname to whitelist
+ */
+ whitelistDomain (hostname) {
+ if (!hostname) {
+ return
+ }
+
+ const { whitelist } = this.store.getState()
+ this.store.updateState({
+ whitelist: [...new Set([hostname, ...whitelist])],
+ })
+ }
+
+ /**
* Given a url, returns the result of checking if that url is in the store.phishing blacklist
*
* @param {string} hostname The hostname portion of a url; the one that will be checked against the white and
@@ -48,6 +64,12 @@ class BlacklistController {
*/
checkForPhishing (hostname) {
if (!hostname) return false
+
+ const { whitelist } = this.store.getState()
+ if (whitelist.some((e) => e === hostname)) {
+ return false
+ }
+
const { result } = this._phishingDetector.check(hostname)
return result
}
diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js
index a93aff49b..d5bc5fe2b 100644
--- a/app/scripts/controllers/currency.js
+++ b/app/scripts/controllers/currency.js
@@ -1,4 +1,4 @@
- const ObservableStore = require('obs-store')
+const ObservableStore = require('obs-store')
const extend = require('xtend')
const log = require('loglevel')
diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js
new file mode 100644
index 000000000..326bcb355
--- /dev/null
+++ b/app/scripts/controllers/network/createInfuraClient.js
@@ -0,0 +1,27 @@
+const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
+const createBlockReRefMiddleware = require('eth-json-rpc-middleware/block-ref')
+const createRetryOnEmptyMiddleware = require('eth-json-rpc-middleware/retryOnEmpty')
+const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
+const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache')
+const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
+const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
+const createInfuraMiddleware = require('eth-json-rpc-infura')
+const BlockTracker = require('eth-block-tracker')
+
+module.exports = createInfuraClient
+
+function createInfuraClient ({ network }) {
+ const infuraMiddleware = createInfuraMiddleware({ network })
+ const infuraProvider = providerFromMiddleware(infuraMiddleware)
+ const blockTracker = new BlockTracker({ provider: infuraProvider })
+
+ const networkMiddleware = mergeMiddleware([
+ createBlockCacheMiddleware({ blockTracker }),
+ createInflightMiddleware(),
+ createBlockReRefMiddleware({ blockTracker, provider: infuraProvider }),
+ createRetryOnEmptyMiddleware({ blockTracker, provider: infuraProvider }),
+ createBlockTrackerInspectorMiddleware({ blockTracker }),
+ infuraMiddleware,
+ ])
+ return { networkMiddleware, blockTracker }
+}
diff --git a/app/scripts/controllers/network/createJsonRpcClient.js b/app/scripts/controllers/network/createJsonRpcClient.js
new file mode 100644
index 000000000..a8cbf2aaf
--- /dev/null
+++ b/app/scripts/controllers/network/createJsonRpcClient.js
@@ -0,0 +1,25 @@
+const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
+const createFetchMiddleware = require('eth-json-rpc-middleware/fetch')
+const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite')
+const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
+const createInflightMiddleware = require('eth-json-rpc-middleware/inflight-cache')
+const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
+const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
+const BlockTracker = require('eth-block-tracker')
+
+module.exports = createJsonRpcClient
+
+function createJsonRpcClient ({ rpcUrl }) {
+ const fetchMiddleware = createFetchMiddleware({ rpcUrl })
+ const blockProvider = providerFromMiddleware(fetchMiddleware)
+ const blockTracker = new BlockTracker({ provider: blockProvider })
+
+ const networkMiddleware = mergeMiddleware([
+ createBlockRefRewriteMiddleware({ blockTracker }),
+ createBlockCacheMiddleware({ blockTracker }),
+ createInflightMiddleware(),
+ createBlockTrackerInspectorMiddleware({ blockTracker }),
+ fetchMiddleware,
+ ])
+ return { networkMiddleware, blockTracker }
+}
diff --git a/app/scripts/controllers/network/createLocalhostClient.js b/app/scripts/controllers/network/createLocalhostClient.js
new file mode 100644
index 000000000..09b1d3c1c
--- /dev/null
+++ b/app/scripts/controllers/network/createLocalhostClient.js
@@ -0,0 +1,21 @@
+const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
+const createFetchMiddleware = require('eth-json-rpc-middleware/fetch')
+const createBlockRefRewriteMiddleware = require('eth-json-rpc-middleware/block-ref-rewrite')
+const createBlockTrackerInspectorMiddleware = require('eth-json-rpc-middleware/block-tracker-inspector')
+const providerFromMiddleware = require('eth-json-rpc-middleware/providerFromMiddleware')
+const BlockTracker = require('eth-block-tracker')
+
+module.exports = createLocalhostClient
+
+function createLocalhostClient () {
+ const fetchMiddleware = createFetchMiddleware({ rpcUrl: 'http://localhost:8545/' })
+ const blockProvider = providerFromMiddleware(fetchMiddleware)
+ const blockTracker = new BlockTracker({ provider: blockProvider, pollingInterval: 1000 })
+
+ const networkMiddleware = mergeMiddleware([
+ createBlockRefRewriteMiddleware({ blockTracker }),
+ createBlockTrackerInspectorMiddleware({ blockTracker }),
+ fetchMiddleware,
+ ])
+ return { networkMiddleware, blockTracker }
+}
diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js
new file mode 100644
index 000000000..9e6a45888
--- /dev/null
+++ b/app/scripts/controllers/network/createMetamaskMiddleware.js
@@ -0,0 +1,43 @@
+const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
+const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware')
+const createAsyncMiddleware = require('json-rpc-engine/src/createAsyncMiddleware')
+const createWalletSubprovider = require('eth-json-rpc-middleware/wallet')
+
+module.exports = createMetamaskMiddleware
+
+function createMetamaskMiddleware ({
+ version,
+ getAccounts,
+ processTransaction,
+ processEthSignMessage,
+ processTypedMessage,
+ processPersonalMessage,
+ getPendingNonce,
+}) {
+ const metamaskMiddleware = mergeMiddleware([
+ createScaffoldMiddleware({
+ // staticSubprovider
+ eth_syncing: false,
+ web3_clientVersion: `MetaMask/v${version}`,
+ }),
+ createWalletSubprovider({
+ getAccounts,
+ processTransaction,
+ processEthSignMessage,
+ processTypedMessage,
+ processPersonalMessage,
+ }),
+ createPendingNonceMiddleware({ getPendingNonce }),
+ ])
+ return metamaskMiddleware
+}
+
+function createPendingNonceMiddleware ({ getPendingNonce }) {
+ return createAsyncMiddleware(async (req, res, next) => {
+ if (req.method !== 'eth_getTransactionCount') return next()
+ const address = req.params[0]
+ const blockRef = req.params[1]
+ if (blockRef !== 'pending') return next()
+ res.result = await getPendingNonce(address)
+ })
+}
diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js
index b6f7705b5..c1667d9a6 100644
--- a/app/scripts/controllers/network/network.js
+++ b/app/scripts/controllers/network/network.js
@@ -1,15 +1,17 @@
const assert = require('assert')
const EventEmitter = require('events')
-const createMetamaskProvider = require('web3-provider-engine/zero.js')
-const SubproviderFromProvider = require('web3-provider-engine/subproviders/provider.js')
-const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
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 JsonRpcEngine = require('json-rpc-engine')
+const providerFromEngine = require('eth-json-rpc-middleware/providerFromEngine')
const log = require('loglevel')
-const urlUtil = require('url')
+const createMetamaskMiddleware = require('./createMetamaskMiddleware')
+const createInfuraClient = require('./createInfuraClient')
+const createJsonRpcClient = require('./createJsonRpcClient')
+const createLocalhostClient = require('./createLocalhostClient')
+const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy')
+
const {
ROPSTEN,
RINKEBY,
@@ -17,7 +19,6 @@ const {
MAINNET,
LOCALHOST,
} = require('./enums')
-const LOCALHOST_RPC_URL = 'http://localhost:8545'
const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET]
const env = process.env.METAMASK_ENV
@@ -39,21 +40,27 @@ module.exports = class NetworkController extends EventEmitter {
this.providerStore = new ObservableStore(providerConfig)
this.networkStore = new ObservableStore('loading')
this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
- // create event emitter proxy
- this._proxy = createEventEmitterProxy()
-
this.on('networkDidChange', this.lookupNetwork)
+ // provider and block tracker
+ this._provider = null
+ this._blockTracker = null
+ // provider and block tracker proxies - because the network changes
+ this._providerProxy = null
+ this._blockTrackerProxy = null
}
- initializeProvider (_providerParams) {
- this._baseProviderParams = _providerParams
+ initializeProvider (providerParams) {
+ this._baseProviderParams = providerParams
const { type, rpcTarget } = this.providerStore.getState()
this._configureProvider({ type, rpcTarget })
- 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._proxy
+ }
+
+ // return the proxies so the references will always be good
+ getProviderAndBlockTracker () {
+ const provider = this._providerProxy
+ const blockTracker = this._blockTrackerProxy
+ return { provider, blockTracker }
}
verifyNetwork () {
@@ -75,10 +82,11 @@ module.exports = class NetworkController extends EventEmitter {
lookupNetwork () {
// Prevent firing when provider is not defined.
- if (!this.ethQuery || !this.ethQuery.sendAsync) {
- return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery')
+ if (!this._provider) {
+ return log.warn('NetworkController - lookupNetwork aborted due to missing provider')
}
- this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
+ const ethQuery = new EthQuery(this._provider)
+ ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading')
log.info('web3.getNetwork returned ' + network)
this.setNetworkState(network)
@@ -131,7 +139,7 @@ module.exports = class NetworkController extends EventEmitter {
this._configureInfuraProvider(opts)
// other type-based rpc endpoints
} else if (type === LOCALHOST) {
- this._configureStandardProvider({ rpcUrl: LOCALHOST_RPC_URL })
+ this._configureLocalhostProvider()
// url-based rpc endpoints
} else if (type === 'rpc') {
this._configureStandardProvider({ rpcUrl: rpcTarget })
@@ -141,49 +149,47 @@ module.exports = class NetworkController extends EventEmitter {
}
_configureInfuraProvider ({ type }) {
- log.info('_configureInfuraProvider', type)
- const infuraProvider = createInfuraProvider({ network: type })
- const infuraSubprovider = new SubproviderFromProvider(infuraProvider)
- const providerParams = extend(this._baseProviderParams, {
- engineParams: {
- pollingInterval: 8000,
- blockTrackerProvider: infuraProvider,
- },
- dataSubprovider: infuraSubprovider,
- })
- const provider = createMetamaskProvider(providerParams)
- this._setProvider(provider)
+ log.info('NetworkController - configureInfuraProvider', type)
+ const networkClient = createInfuraClient({ network: type })
+ this._setNetworkClient(networkClient)
+ }
+
+ _configureLocalhostProvider () {
+ log.info('NetworkController - configureLocalhostProvider')
+ const networkClient = createLocalhostClient()
+ this._setNetworkClient(networkClient)
}
_configureStandardProvider ({ rpcUrl }) {
- // urlUtil handles malformed urls
- rpcUrl = urlUtil.parse(rpcUrl).format()
- const providerParams = extend(this._baseProviderParams, {
- rpcUrl,
- engineParams: {
- pollingInterval: 8000,
- },
- })
- const provider = createMetamaskProvider(providerParams)
- this._setProvider(provider)
- }
-
- _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()
+ log.info('NetworkController - configureStandardProvider', rpcUrl)
+ const networkClient = createJsonRpcClient({ rpcUrl })
+ this._setNetworkClient(networkClient)
+ }
+
+ _setNetworkClient ({ networkMiddleware, blockTracker }) {
+ const metamaskMiddleware = createMetamaskMiddleware(this._baseProviderParams)
+ const engine = new JsonRpcEngine()
+ engine.push(metamaskMiddleware)
+ engine.push(networkMiddleware)
+ const provider = providerFromEngine(engine)
+ this._setProviderAndBlockTracker({ provider, blockTracker })
+ }
+
+ _setProviderAndBlockTracker ({ provider, blockTracker }) {
+ // update or intialize proxies
+ if (this._providerProxy) {
+ this._providerProxy.setTarget(provider)
+ } else {
+ this._providerProxy = createSwappableProxy(provider)
+ }
+ if (this._blockTrackerProxy) {
+ this._blockTrackerProxy.setTarget(blockTracker)
+ } else {
+ this._blockTrackerProxy = createEventEmitterProxy(blockTracker, { eventFilter: 'skipInternal' })
}
- // override block tracler
- provider._blockTracker = createEventEmitterProxy(provider._blockTracker, blockTrackerHandlers)
- // set as new provider
+ // set new provider and blockTracker
this._provider = provider
- this._proxy.setTarget(provider)
+ this._blockTracker = blockTracker
}
_logBlock (block) {
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 707fd7de9..fd6a4866d 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -1,5 +1,6 @@
const ObservableStore = require('obs-store')
const normalizeAddress = require('eth-sig-util').normalize
+const { isValidAddress } = require('ethereumjs-util')
const extend = require('xtend')
@@ -14,6 +15,7 @@ class PreferencesController {
* @property {string} store.currentAccountTab Indicates the selected tab in the ui
* @property {array} store.tokens The tokens the user wants display in their token lists
* @property {object} store.accountTokens The tokens stored per account and then per network type
+ * @property {object} store.assetImages Contains assets objects related to assets added
* @property {boolean} store.useBlockie The users preference for blockie identicons within the UI
* @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature
@@ -26,22 +28,43 @@ class PreferencesController {
frequentRpcList: [],
currentAccountTab: 'history',
accountTokens: {},
+ assetImages: {},
tokens: [],
+ suggestedTokens: {},
useBlockie: false,
featureFlags: {},
currentLocale: opts.initLangCode,
identities: {},
lostIdentities: {},
+ seedWords: null,
+ forgottenPassword: false,
}, opts.initState)
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
+ this.showWatchAssetUi = opts.showWatchAssetUi
this._subscribeProviderType()
}
// PUBLIC METHODS
/**
+ * Sets the {@code forgottenPassword} state property
+ * @param {boolean} forgottenPassword whether or not the user has forgotten their password
+ */
+ setPasswordForgotten (forgottenPassword) {
+ this.store.updateState({ forgottenPassword })
+ }
+
+ /**
+ * Sets the {@code seedWords} seed words
+ * @param {string|null} seedWords the seed words
+ */
+ setSeedWords (seedWords) {
+ this.store.updateState({ seedWords })
+ }
+
+ /**
* Setter for the `useBlockie` property
*
* @param {boolean} val Whether or not the user prefers blockie indicators
@@ -51,6 +74,53 @@ class PreferencesController {
this.store.updateState({ useBlockie: val })
}
+ getSuggestedTokens () {
+ return this.store.getState().suggestedTokens
+ }
+
+ getAssetImages () {
+ return this.store.getState().assetImages
+ }
+
+ addSuggestedERC20Asset (tokenOpts) {
+ this._validateERC20AssetParams(tokenOpts)
+ const suggested = this.getSuggestedTokens()
+ const { rawAddress, symbol, decimals, image } = tokenOpts
+ const address = normalizeAddress(rawAddress)
+ const newEntry = { address, symbol, decimals, image }
+ suggested[address] = newEntry
+ this.store.updateState({ suggestedTokens: suggested })
+ }
+
+ /**
+ * RPC engine middleware for requesting new asset added
+ *
+ * @param req
+ * @param res
+ * @param {Function} - next
+ * @param {Function} - end
+ */
+ async requestWatchAsset (req, res, next, end) {
+ if (req.method === 'metamask_watchAsset') {
+ const { type, options } = req.params
+ switch (type) {
+ case 'ERC20':
+ const result = await this._handleWatchAssetERC20(options)
+ if (result instanceof Error) {
+ end(result)
+ } else {
+ res.result = result
+ end()
+ }
+ break
+ default:
+ end(new Error(`Asset of type ${type} not supported`))
+ }
+ } else {
+ next()
+ }
+ }
+
/**
* Getter for the `useBlockie` property
*
@@ -186,6 +256,13 @@ class PreferencesController {
return selected
}
+ removeSuggestedTokens () {
+ return new Promise((resolve, reject) => {
+ this.store.updateState({ suggestedTokens: {} })
+ resolve({})
+ })
+ }
+
/**
* Setter for the `selectedAddress` property
*
@@ -232,11 +309,11 @@ class PreferencesController {
* @returns {Promise<array>} Promises the new array of AddedToken objects.
*
*/
- async addToken (rawAddress, symbol, decimals) {
+ async addToken (rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
-
const tokens = this.store.getState().tokens
+ const assetImages = this.getAssetImages()
const previousEntry = tokens.find((token, index) => {
return token.address === address
})
@@ -247,7 +324,8 @@ class PreferencesController {
} else {
tokens.push(newEntry)
}
- this._updateAccountTokens(tokens)
+ assetImages[address] = image
+ this._updateAccountTokens(tokens, assetImages)
return Promise.resolve(tokens)
}
@@ -260,8 +338,10 @@ class PreferencesController {
*/
removeToken (rawAddress) {
const tokens = this.store.getState().tokens
+ const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
- this._updateAccountTokens(updatedTokens)
+ delete assetImages[rawAddress]
+ this._updateAccountTokens(updatedTokens, assetImages)
return Promise.resolve(updatedTokens)
}
@@ -295,11 +375,12 @@ class PreferencesController {
* Gets an updated rpc list from this.addToFrequentRpcList() and sets the `frequentRpcList` to this update list.
*
* @param {string} _url The the new rpc url to add to the updated list
+ * @param {bool} remove Remove selected url
* @returns {Promise<void>} Promise resolves with undefined
*
*/
- updateFrequentRpcList (_url) {
- return this.addToFrequentRpcList(_url)
+ updateFrequentRpcList (_url, remove = false) {
+ return this.addToFrequentRpcList(_url, remove)
.then((rpcList) => {
this.store.updateState({ frequentRpcList: rpcList })
return Promise.resolve()
@@ -322,25 +403,23 @@ class PreferencesController {
/**
* Returns an updated rpcList based on the passed url and the current list.
- * The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the
+ * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the
* end of the list. The current list is modified and returned as a promise.
*
* @param {string} _url The rpc url to add to the frequentRpcList.
+ * @param {bool} remove Remove selected url
* @returns {Promise<array>} The updated frequentRpcList.
*
*/
- addToFrequentRpcList (_url) {
+ addToFrequentRpcList (_url, remove = false) {
const rpcList = this.getFrequentRpcList()
const index = rpcList.findIndex((element) => { return element === _url })
if (index !== -1) {
rpcList.splice(index, 1)
}
- if (_url !== 'http://localhost:8545') {
+ if (!remove && _url !== 'http://localhost:8545') {
rpcList.push(_url)
}
- if (rpcList.length > 2) {
- rpcList.shift()
- }
return Promise.resolve(rpcList)
}
@@ -387,6 +466,7 @@ class PreferencesController {
//
// PRIVATE METHODS
//
+
/**
* Subscription to network provider type.
*
@@ -405,10 +485,10 @@ class PreferencesController {
* @param {array} tokens Array of tokens to be updated.
*
*/
- _updateAccountTokens (tokens) {
+ _updateAccountTokens (tokens, assetImages) {
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
- this.store.updateState({ accountTokens, tokens })
+ this.store.updateState({ accountTokens, tokens, assetImages })
}
/**
@@ -438,6 +518,47 @@ class PreferencesController {
const tokens = accountTokens[selectedAddress][providerType]
return { tokens, accountTokens, providerType, selectedAddress }
}
+
+ /**
+ * Handle the suggestion of an ERC20 asset through `watchAsset`
+ * *
+ * @param {Promise} promise Promise according to addition of ERC20 token
+ *
+ */
+ async _handleWatchAssetERC20 (options) {
+ const { address, symbol, decimals, image } = options
+ const rawAddress = address
+ try {
+ this._validateERC20AssetParams({ rawAddress, symbol, decimals })
+ } catch (err) {
+ return err
+ }
+ const tokenOpts = { rawAddress, decimals, symbol, image }
+ this.addSuggestedERC20Asset(tokenOpts)
+ return this.showWatchAssetUi().then(() => {
+ const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
+ return tokenAddresses.length > 0
+ })
+ }
+
+ /**
+ * Validates that the passed options for suggested token have all required properties.
+ *
+ * @param {Object} opts The options object to validate
+ * @throws {string} Throw a custom error indicating that address, symbol and/or decimals
+ * doesn't fulfill requirements
+ *
+ */
+ _validateERC20AssetParams (opts) {
+ const { rawAddress, symbol, decimals } = opts
+ if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
+ if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
+ const numDecimals = parseInt(decimals, 10)
+ if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
+ throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
+ }
+ if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`)
+ }
}
module.exports = PreferencesController
diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js
index 926268691..d270f6f44 100644
--- a/app/scripts/controllers/recent-blocks.js
+++ b/app/scripts/controllers/recent-blocks.js
@@ -1,14 +1,14 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
-const BN = require('ethereumjs-util').BN
const EthQuery = require('eth-query')
const log = require('loglevel')
+const pify = require('pify')
class RecentBlocksController {
/**
* Controller responsible for storing, updating and managing the recent history of blocks. Blocks are back filled
- * upon the controller's construction and then the list is updated when the given block tracker gets a 'block' event
+ * upon the controller's construction and then the list is updated when the given block tracker gets a 'latest' event
* (indicating that there is a new block to process).
*
* @typedef {Object} RecentBlocksController
@@ -16,7 +16,7 @@ class RecentBlocksController {
* @param {BlockTracker} opts.blockTracker Contains objects necessary for tracking blocks and querying the blockchain
* @param {BlockTracker} opts.provider The provider used to create a new EthQuery instance.
* @property {BlockTracker} blockTracker Points to the passed BlockTracker. On RecentBlocksController construction,
- * listens for 'block' events so that new blocks can be processed and added to storage.
+ * listens for 'latest' events so that new blocks can be processed and added to storage.
* @property {EthQuery} ethQuery Points to the EthQuery instance created with the passed provider
* @property {number} historyLength The maximum length of blocks to track
* @property {object} store Stores the recentBlocks
@@ -34,7 +34,13 @@ class RecentBlocksController {
}, opts.initState)
this.store = new ObservableStore(initState)
- this.blockTracker.on('block', this.processBlock.bind(this))
+ this.blockTracker.on('latest', async (newBlockNumberHex) => {
+ try {
+ await this.processBlock(newBlockNumberHex)
+ } catch (err) {
+ log.error(err)
+ }
+ })
this.backfill()
}
@@ -55,7 +61,11 @@ class RecentBlocksController {
* @param {object} newBlock The new block to modify and add to the recentBlocks array
*
*/
- processBlock (newBlock) {
+ async processBlock (newBlockNumberHex) {
+ const newBlockNumber = Number.parseInt(newBlockNumberHex, 16)
+ const newBlock = await this.getBlockByNumber(newBlockNumber, true)
+ if (!newBlock) return
+
const block = this.mapTransactionsToPrices(newBlock)
const state = this.store.getState()
@@ -108,9 +118,9 @@ class RecentBlocksController {
}
/**
- * On this.blockTracker's first 'block' event after this RecentBlocksController's instantiation, the store.recentBlocks
+ * On this.blockTracker's first 'latest' event after this RecentBlocksController's instantiation, the store.recentBlocks
* array is populated with this.historyLength number of blocks. The block number of the this.blockTracker's first
- * 'block' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying
+ * 'latest' event is used to iteratively generate all the numbers of the previous blocks, which are obtained by querying
* the blockchain. These blocks are backfilled so that the recentBlocks array is ordered from oldest to newest.
*
* Each iteration over the block numbers is delayed by 100 milliseconds.
@@ -118,18 +128,17 @@ class RecentBlocksController {
* @returns {Promise<void>} Promises undefined
*/
async backfill () {
- this.blockTracker.once('block', async (block) => {
- const currentBlockNumber = Number.parseInt(block.number, 16)
+ this.blockTracker.once('latest', async (blockNumberHex) => {
+ const currentBlockNumber = Number.parseInt(blockNumberHex, 16)
const blocksToFetch = Math.min(currentBlockNumber, this.historyLength)
const prevBlockNumber = currentBlockNumber - 1
const targetBlockNumbers = Array(blocksToFetch).fill().map((_, index) => prevBlockNumber - index)
await Promise.all(targetBlockNumbers.map(async (targetBlockNumber) => {
try {
- const newBlock = await this.getBlockByNumber(targetBlockNumber)
+ const newBlock = await this.getBlockByNumber(targetBlockNumber, true)
+ if (!newBlock) return
- if (newBlock) {
- this.backfillBlock(newBlock)
- }
+ this.backfillBlock(newBlock)
} catch (e) {
log.error(e)
}
@@ -138,18 +147,6 @@ class RecentBlocksController {
}
/**
- * A helper for this.backfill. Provides an easy way to ensure a 100 millisecond delay using await
- *
- * @returns {Promise<void>} Promises undefined
- *
- */
- async wait () {
- return new Promise((resolve) => {
- setTimeout(resolve, 100)
- })
- }
-
- /**
* Uses EthQuery to get a block that has a given block number.
*
* @param {number} number The number of the block to get
@@ -157,13 +154,8 @@ class RecentBlocksController {
*
*/
async getBlockByNumber (number) {
- const bn = new BN(number)
- return new Promise((resolve, reject) => {
- this.ethQuery.getBlockByNumber('0x' + bn.toString(16), true, (err, block) => {
- if (err) reject(err)
- resolve(block)
- })
- })
+ const blockNumberHex = '0x' + number.toString(16)
+ return await pify(this.ethQuery.getBlockByNumber).call(this.ethQuery, blockNumberHex, true)
}
}
diff --git a/app/scripts/controllers/transactions/enums.js b/app/scripts/controllers/transactions/enums.js
new file mode 100644
index 000000000..be6f16e0d
--- /dev/null
+++ b/app/scripts/controllers/transactions/enums.js
@@ -0,0 +1,12 @@
+const TRANSACTION_TYPE_CANCEL = 'cancel'
+const TRANSACTION_TYPE_RETRY = 'retry'
+const TRANSACTION_TYPE_STANDARD = 'standard'
+
+const TRANSACTION_STATUS_APPROVED = 'approved'
+
+module.exports = {
+ TRANSACTION_TYPE_CANCEL,
+ TRANSACTION_TYPE_RETRY,
+ TRANSACTION_TYPE_STANDARD,
+ TRANSACTION_STATUS_APPROVED,
+}
diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js
index 8e2288aed..a57c85f50 100644
--- a/app/scripts/controllers/transactions/index.js
+++ b/app/scripts/controllers/transactions/index.js
@@ -11,6 +11,14 @@ const txUtils = require('./lib/util')
const cleanErrorStack = require('../../lib/cleanErrorStack')
const log = require('loglevel')
const recipientBlacklistChecker = require('./lib/recipient-blacklist-checker')
+const {
+ TRANSACTION_TYPE_CANCEL,
+ TRANSACTION_TYPE_RETRY,
+ TRANSACTION_TYPE_STANDARD,
+ TRANSACTION_STATUS_APPROVED,
+} = require('./enums')
+
+const { hexToBn, bnToHex, BnMultiplyByFraction } = require('../../lib/util')
/**
Transaction Controller is an aggregate of sub-controllers and trackers
@@ -65,6 +73,7 @@ class TransactionController extends EventEmitter {
this.store = this.txStateManager.store
this.nonceTracker = new NonceTracker({
provider: this.provider,
+ blockTracker: this.blockTracker,
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getConfirmedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
})
@@ -78,13 +87,17 @@ class TransactionController extends EventEmitter {
})
this.txStateManager.store.subscribe(() => this.emit('update:badge'))
- this._setupListners()
+ this._setupListeners()
// memstore is computed from a few different stores
this._updateMemstore()
this.txStateManager.store.subscribe(() => this._updateMemstore())
this.networkStore.subscribe(() => this._updateMemstore())
this.preferencesStore.subscribe(() => this._updateMemstore())
+
+ // request state update to finalize initialization
+ this._updatePendingTxsAfterFirstBlock()
}
+
/** @returns {number} the chainId*/
getChainId () {
const networkState = this.networkStore.getState()
@@ -153,9 +166,16 @@ class TransactionController extends EventEmitter {
async addUnapprovedTransaction (txParams) {
// validate
const normalizedTxParams = txUtils.normalizeTxParams(txParams)
+ // Assert the from address is the selected address
+ if (normalizedTxParams.from !== this.getSelectedAddress()) {
+ throw new Error(`Transaction from address isn't valid for this account`)
+ }
txUtils.validateTxParams(normalizedTxParams)
// construct txMeta
- let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams })
+ let txMeta = this.txStateManager.generateTxMeta({
+ txParams: normalizedTxParams,
+ type: TRANSACTION_TYPE_STANDARD,
+ })
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
@@ -209,6 +229,7 @@ class TransactionController extends EventEmitter {
txParams: originalTxMeta.txParams,
lastGasPrice,
loadingDefaults: false,
+ type: TRANSACTION_TYPE_RETRY,
})
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
@@ -216,6 +237,40 @@ class TransactionController extends EventEmitter {
}
/**
+ * Creates a new approved transaction to attempt to cancel a previously submitted transaction. The
+ * new transaction contains the same nonce as the previous, is a basic ETH transfer of 0x value to
+ * the sender's address, and has a higher gasPrice than that of the previous transaction.
+ * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel
+ * @param {string=} customGasPrice - the hex value to use for the cancel transaction
+ * @returns {txMeta}
+ */
+ async createCancelTransaction (originalTxId, customGasPrice) {
+ const originalTxMeta = this.txStateManager.getTx(originalTxId)
+ const { txParams } = originalTxMeta
+ const { gasPrice: lastGasPrice, from, nonce } = txParams
+
+ const newGasPrice = customGasPrice || bnToHex(BnMultiplyByFraction(hexToBn(lastGasPrice), 11, 10))
+ const newTxMeta = this.txStateManager.generateTxMeta({
+ txParams: {
+ from,
+ to: from,
+ nonce,
+ gas: '0x5208',
+ value: '0x0',
+ gasPrice: newGasPrice,
+ },
+ lastGasPrice,
+ loadingDefaults: false,
+ status: TRANSACTION_STATUS_APPROVED,
+ type: TRANSACTION_TYPE_CANCEL,
+ })
+
+ this.addTx(newTxMeta)
+ await this.approveTransaction(newTxMeta.id)
+ return newTxMeta
+ }
+
+ /**
updates the txMeta in the txStateManager
@param txMeta {Object} - the updated txMeta
*/
@@ -311,6 +366,11 @@ class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusSubmitted(txId)
}
+ confirmTransaction (txId) {
+ this.txStateManager.setTxStatusConfirmed(txId)
+ this._markNonceDuplicatesDropped(txId)
+ }
+
/**
Convenience method for the ui thats sets the transaction to rejected
@param txId {number} - the tx's Id
@@ -354,6 +414,14 @@ class TransactionController extends EventEmitter {
this.getFilteredTxList = (opts) => this.txStateManager.getFilteredTxList(opts)
}
+ // called once on startup
+ async _updatePendingTxsAfterFirstBlock () {
+ // wait for first block so we know we're ready
+ await this.blockTracker.getLatestBlock()
+ // get status update for all pending transactions (for the current network)
+ await this.pendingTxTracker.updatePendingTxs()
+ }
+
/**
If transaction controller was rebooted with transactions that are uncompleted
in steps of the transaction signing or user confirmation process it will either
@@ -375,7 +443,7 @@ class TransactionController extends EventEmitter {
})
this.txStateManager.getFilteredTxList({
- status: 'approved',
+ status: TRANSACTION_STATUS_APPROVED,
}).forEach((txMeta) => {
const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
@@ -386,14 +454,14 @@ class TransactionController extends EventEmitter {
is called in constructor applies the listeners for pendingTxTracker txStateManager
and blockTracker
*/
- _setupListners () {
+ _setupListeners () {
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
+ this._setupBlockTrackerListener()
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
- this.pendingTxTracker.on('tx:confirmed', (txId) => this.txStateManager.setTxStatusConfirmed(txId))
- this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
+ this.pendingTxTracker.on('tx:confirmed', (txId) => this.confirmTransaction(txId))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
@@ -405,13 +473,6 @@ class TransactionController extends EventEmitter {
txMeta.retryCount++
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:retry')
})
-
- 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
- this.blockTracker.on('latest', this.pendingTxTracker.resubmitPendingTxs.bind(this.pendingTxTracker))
- this.blockTracker.on('sync', this.pendingTxTracker.queryPendingTxs.bind(this.pendingTxTracker))
-
}
/**
@@ -435,10 +496,45 @@ class TransactionController extends EventEmitter {
})
}
+ _setupBlockTrackerListener () {
+ let listenersAreActive = false
+ const latestBlockHandler = this._onLatestBlock.bind(this)
+ const blockTracker = this.blockTracker
+ const txStateManager = this.txStateManager
+
+ txStateManager.on('tx:status-update', updateSubscription)
+ updateSubscription()
+
+ function updateSubscription () {
+ const pendingTxs = txStateManager.getPendingTransactions()
+ if (!listenersAreActive && pendingTxs.length > 0) {
+ blockTracker.on('latest', latestBlockHandler)
+ listenersAreActive = true
+ } else if (listenersAreActive && !pendingTxs.length) {
+ blockTracker.removeListener('latest', latestBlockHandler)
+ listenersAreActive = false
+ }
+ }
+ }
+
+ async _onLatestBlock (blockNumber) {
+ try {
+ await this.pendingTxTracker.updatePendingTxs()
+ } catch (err) {
+ log.error(err)
+ }
+ try {
+ await this.pendingTxTracker.resubmitPendingTxs(blockNumber)
+ } catch (err) {
+ log.error(err)
+ }
+ }
+
/**
Updates the memStore in transaction controller
*/
_updateMemstore () {
+ this.pendingTxTracker.updatePendingTxs()
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.txStateManager.getFilteredTxList({
from: this.getSelectedAddress(),
diff --git a/app/scripts/controllers/transactions/nonce-tracker.js b/app/scripts/controllers/transactions/nonce-tracker.js
index 06f336eaa..421036368 100644
--- a/app/scripts/controllers/transactions/nonce-tracker.js
+++ b/app/scripts/controllers/transactions/nonce-tracker.js
@@ -12,8 +12,9 @@ const Mutex = require('await-semaphore').Mutex
*/
class NonceTracker {
- constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
+ constructor ({ provider, blockTracker, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider
+ this.blockTracker = blockTracker
this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions
this.getConfirmedTransactions = getConfirmedTransactions
@@ -34,7 +35,7 @@ class NonceTracker {
* @typedef NonceDetails
* @property {number} highestLocallyConfirmed - A hex string of the highest nonce on a confirmed transaction.
* @property {number} nextNetworkNonce - The next nonce suggested by the eth_getTransactionCount method.
- * @property {number} highetSuggested - The maximum between the other two, the number returned.
+ * @property {number} highestSuggested - The maximum between the other two, the number returned.
*/
/**
@@ -80,15 +81,6 @@ class NonceTracker {
}
}
- async _getCurrentBlock () {
- const blockTracker = this._getBlockTracker()
- const currentBlock = blockTracker.getCurrentBlock()
- if (currentBlock) return currentBlock
- return await new Promise((reject, resolve) => {
- blockTracker.once('latest', resolve)
- })
- }
-
async _globalMutexFree () {
const globalMutex = this._lookupMutex('global')
const releaseLock = await globalMutex.acquire()
@@ -114,9 +106,8 @@ class NonceTracker {
// calculate next nonce
// we need to make sure our base count
// and pending count are from the same block
- const currentBlock = await this._getCurrentBlock()
- const blockNumber = currentBlock.blockNumber
- const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber || 'latest')
+ const blockNumber = await this.blockTracker.getLatestBlock()
+ const baseCountBN = await this.ethQuery.getTransactionCount(address, blockNumber)
const baseCount = baseCountBN.toNumber()
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
const nonceDetails = { blockNumber, baseCount }
@@ -165,15 +156,6 @@ class NonceTracker {
return { name: 'local', nonce: highest, details: { startPoint, highest } }
}
- // this is a hotfix for the fact that the blockTracker will
- // change when the network changes
-
- /**
- @returns {Object} the current blockTracker
- */
- _getBlockTracker () {
- return this.provider._blockTracker
- }
}
module.exports = NonceTracker
diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js
index 4e41cdaf8..70cac096b 100644
--- a/app/scripts/controllers/transactions/pending-tx-tracker.js
+++ b/app/scripts/controllers/transactions/pending-tx-tracker.js
@@ -1,6 +1,7 @@
const EventEmitter = require('events')
const log = require('loglevel')
const EthQuery = require('ethjs-query')
+
/**
Event emitter utility class for tracking the transactions as they<br>
@@ -23,55 +24,26 @@ class PendingTransactionTracker extends EventEmitter {
super()
this.query = new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker
- // default is one day
this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions
this.publishTransaction = config.publishTransaction
- this._checkPendingTxs()
+ this.confirmTransaction = config.confirmTransaction
}
/**
- checks if a signed tx is in a block and
- if it is included emits tx status as 'confirmed'
- @param block {object}, a full block
- @emits tx:confirmed
- @emits tx:failed
- */
- checkForTxInBlock (block) {
- const signedTxList = this.getPendingTransactions()
- if (!signedTxList.length) return
- signedTxList.forEach((txMeta) => {
- const txHash = txMeta.hash
- const txId = txMeta.id
-
- if (!txHash) {
- const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
- noTxHashErr.name = 'NoTxHashError'
- this.emit('tx:failed', txId, noTxHashErr)
- return
- }
-
-
- block.transactions.forEach((tx) => {
- if (tx.hash === txHash) this.emit('tx:confirmed', txId)
- })
- })
- }
-
- /**
- asks the network for the transaction to see if a block number is included on it
- if we have skipped/missed blocks
- @param object - oldBlock newBlock
+ checks the network for signed txs and releases the nonce global lock if it is
*/
- queryPendingTxs ({ oldBlock, newBlock }) {
- // check pending transactions on start
- if (!oldBlock) {
- this._checkPendingTxs()
- return
+ async updatePendingTxs () {
+ // in order to keep the nonceTracker accurate we block it while updating pending transactions
+ const nonceGlobalLock = await this.nonceTracker.getGlobalLock()
+ try {
+ const pendingTxs = this.getPendingTransactions()
+ await Promise.all(pendingTxs.map((txMeta) => this._checkPendingTx(txMeta)))
+ } catch (err) {
+ log.error('PendingTransactionTracker - Error updating pending transactions')
+ log.error(err)
}
- // if we synced by more than one block, check for missed pending transactions
- const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16)
- if (diff > 1) this._checkPendingTxs()
+ nonceGlobalLock.releaseLock()
}
/**
@@ -79,11 +51,11 @@ class PendingTransactionTracker extends EventEmitter {
@param block {object} - a block object
@emits tx:warning
*/
- resubmitPendingTxs (block) {
+ resubmitPendingTxs (blockNumber) {
const pending = this.getPendingTransactions()
// only try resubmitting if their are transactions to resubmit
if (!pending.length) return
- pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => {
+ pending.forEach((txMeta) => this._resubmitTx(txMeta, blockNumber).catch((err) => {
/*
Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce
@@ -145,6 +117,7 @@ class PendingTransactionTracker extends EventEmitter {
this.emit('tx:retry', txMeta)
return txHash
}
+
/**
Ask the network for the transaction to see if it has been include in a block
@param txMeta {Object} - the txMeta object
@@ -174,9 +147,8 @@ class PendingTransactionTracker extends EventEmitter {
}
// get latest transaction status
- let txParams
try {
- txParams = await this.query.getTransactionByHash(txHash)
+ const txParams = await this.query.getTransactionByHash(txHash)
if (!txParams) return
if (txParams.blockNumber) {
this.emit('tx:confirmed', txId)
@@ -191,26 +163,12 @@ class PendingTransactionTracker extends EventEmitter {
}
/**
- checks the network for signed txs and releases the nonce global lock if it is
- */
- async _checkPendingTxs () {
- const signedTxList = this.getPendingTransactions()
- // in order to keep the nonceTracker accurate we block it while updating pending transactions
- const { releaseLock } = await this.nonceTracker.getGlobalLock()
- try {
- await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta)))
- } catch (err) {
- log.error('PendingTransactionWatcher - Error updating pending transactions')
- log.error(err)
- }
- releaseLock()
- }
-
- /**
checks to see if a confirmed txMeta has the same nonce
@param txMeta {Object} - txMeta object
@returns {boolean}
*/
+
+
async _checkIfNonceIsTaken (txMeta) {
const address = txMeta.txParams.from
const completed = this.getCompletedTransactions(address)
diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js
index 5cd0f5407..3dd45507f 100644
--- a/app/scripts/controllers/transactions/tx-gas-utils.js
+++ b/app/scripts/controllers/transactions/tx-gas-utils.js
@@ -25,7 +25,7 @@ class TxGasUtil {
@returns {object} the txMeta object with the gas written to the txParams
*/
async analyzeGasUsage (txMeta) {
- const block = await this.query.getBlockByNumber('latest', true)
+ const block = await this.query.getBlockByNumber('latest', false)
let estimatedGasHex
try {
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js
index 28a18ca2e..daa6cc388 100644
--- a/app/scripts/controllers/transactions/tx-state-manager.js
+++ b/app/scripts/controllers/transactions/tx-state-manager.js
@@ -353,6 +353,7 @@ class TransactionStateManager extends EventEmitter {
const txMeta = this.getTx(txId)
txMeta.err = {
message: err.toString(),
+ rpc: err.value,
stack: err.stack,
}
this.updateTx(txMeta)
diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js
index 7dd7fda02..431702d63 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -4,11 +4,17 @@ require('web3/dist/web3.min.js')
const log = require('loglevel')
const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
-const MetamaskInpageProvider = require('./lib/inpage-provider.js')
+const MetamaskInpageProvider = require('metamask-inpage-provider')
+
restoreContextAfterImports()
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
+console.warn('ATTENTION: In an effort to improve user privacy, MetaMask will ' +
+'stop exposing user accounts to dapps by default beginning November 2nd, 2018. ' +
+'Dapps should call provider.enable() in order to view and use accounts. Please see ' +
+'https://bit.ly/2QQHXvF for complete information and up-to-date example code.')
+
//
// setup plugin communication
//
@@ -22,6 +28,33 @@ var metamaskStream = new LocalMessageDuplexStream({
// compose the inpage provider
var inpageProvider = new MetamaskInpageProvider(metamaskStream)
+// Augment the provider with its enable method
+inpageProvider.enable = function (options = {}) {
+ return new Promise((resolve, reject) => {
+ if (options.mockRejection) {
+ reject('User rejected account access')
+ } else {
+ inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
+ if (error) {
+ reject(error)
+ } else {
+ resolve(response.result)
+ }
+ })
+ }
+ })
+}
+
+// Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound
+// `sendAsync` method on the prototype, causing `this` reference issues with drizzle
+const proxiedInpageProvider = new Proxy(inpageProvider, {
+ // straight up lie that we deleted the property so that it doesnt
+ // throw an error in strict mode
+ deleteProperty: () => true,
+})
+
+window.ethereum = proxiedInpageProvider
+
//
// setup web3
//
@@ -33,7 +66,8 @@ if (typeof window.web3 !== 'undefined') {
or MetaMask and another web3 extension. Please remove one
and try again.`)
}
-var web3 = new Web3(inpageProvider)
+
+var web3 = new Web3(proxiedInpageProvider)
web3.setProvider = function () {
log.debug('MetaMask - overrode web3.setProvider')
}
diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js
index 0f7b3d865..2e9340018 100644
--- a/app/scripts/lib/account-tracker.js
+++ b/app/scripts/lib/account-tracker.js
@@ -7,14 +7,13 @@
* 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 () {}
+const log = require('loglevel')
+const pify = require('pify')
-class AccountTracker extends EventEmitter {
+class AccountTracker {
/**
* This module is responsible for tracking any number of accounts and caching their current balances & transaction
@@ -35,8 +34,6 @@ class AccountTracker extends EventEmitter {
*
*/
constructor (opts = {}) {
- super()
-
const initState = {
accounts: {},
currentBlockGasLimit: '',
@@ -44,12 +41,29 @@ class AccountTracker extends EventEmitter {
this.store = new ObservableStore(initState)
this._provider = opts.provider
- this._query = new EthQuery(this._provider)
+ this._query = pify(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
+ this._currentBlockNumber = this._blockTracker.getCurrentBlock()
+ this._blockTracker.once('latest', blockNumber => {
+ this._currentBlockNumber = blockNumber
+ })
+ // bind function for easier listener syntax
+ this._updateForBlock = this._updateForBlock.bind(this)
+ }
+
+ start () {
+ // remove first to avoid double add
+ this._blockTracker.removeListener('latest', this._updateForBlock)
+ // add listener
+ this._blockTracker.addListener('latest', this._updateForBlock)
+ // fetch account balances
+ this._updateAccounts()
+ }
+
+ stop () {
+ // remove listener
+ this._blockTracker.removeListener('latest', this._updateForBlock)
}
/**
@@ -67,49 +81,57 @@ class AccountTracker extends EventEmitter {
const accounts = this.store.getState().accounts
const locals = Object.keys(accounts)
- const toAdd = []
+ const accountsToAdd = []
addresses.forEach((upstream) => {
if (!locals.includes(upstream)) {
- toAdd.push(upstream)
+ accountsToAdd.push(upstream)
}
})
- const toRemove = []
+ const accountsToRemove = []
locals.forEach((local) => {
if (!addresses.includes(local)) {
- toRemove.push(local)
+ accountsToRemove.push(local)
}
})
- toAdd.forEach(upstream => this.addAccount(upstream))
- toRemove.forEach(local => this.removeAccount(local))
- this._updateAccounts()
+ this.addAccounts(accountsToAdd)
+ this.removeAccount(accountsToRemove)
}
/**
- * Adds a new address to this AccountTracker's accounts object, which points to an empty object. This object will be
+ * Adds new addresses to track the balances of
* given a balance as long this._currentBlockNumber is defined.
*
- * @param {string} address A hex address of a new account to store in this AccountTracker's accounts object
+ * @param {array} addresses An array of hex addresses of new accounts to track
*
*/
- addAccount (address) {
+ addAccounts (addresses) {
const accounts = this.store.getState().accounts
- accounts[address] = {}
+ // add initial state for addresses
+ addresses.forEach(address => {
+ accounts[address] = {}
+ })
+ // save accounts state
this.store.updateState({ accounts })
+ // fetch balances for the accounts if there is block number ready
if (!this._currentBlockNumber) return
- this._updateAccount(address)
+ addresses.forEach(address => this._updateAccount(address))
}
/**
- * Removes an account from this AccountTracker's accounts object
+ * Removes accounts from being tracked
*
- * @param {string} address A hex address of a the account to remove
+ * @param {array} an array of hex addresses to stop tracking
*
*/
- removeAccount (address) {
+ removeAccount (addresses) {
const accounts = this.store.getState().accounts
- delete accounts[address]
+ // remove each state object
+ addresses.forEach(address => {
+ delete accounts[address]
+ })
+ // save accounts state
this.store.updateState({ accounts })
}
@@ -118,71 +140,56 @@ class AccountTracker extends EventEmitter {
* via EthQuery
*
* @private
- * @param {object} block Data about the block that contains the data to update to.
+ * @param {number} blockNumber the block number to update to.
* @fires 'block' The updated state, if all account updates are successful
*
*/
- _updateForBlock (block) {
- this._currentBlockNumber = block.number
- const currentBlockGasLimit = block.gasLimit
+ async _updateForBlock (blockNumber) {
+ this._currentBlockNumber = blockNumber
+ // block gasLimit polling shouldn't be in account-tracker shouldn't be here...
+ const currentBlock = await this._query.getBlockByNumber(blockNumber, false)
+ if (!currentBlock) return
+ const currentBlockGasLimit = currentBlock.gasLimit
this.store.updateState({ currentBlockGasLimit })
- async.parallel([
- this._updateAccounts.bind(this),
- ], (err) => {
- if (err) return console.error(err)
- this.emit('block', this.store.getState())
- })
+ try {
+ await this._updateAccounts()
+ } catch (err) {
+ log.error(err)
+ }
}
/**
* Calls this._updateAccount for each account in this.store
*
- * @param {Function} cb A callback to pass to this._updateAccount, called after each account is successfully updated
+ * @returns {Promise} after all account balances updated
*
*/
- _updateAccounts (cb = noop) {
+ async _updateAccounts () {
const accounts = this.store.getState().accounts
const addresses = Object.keys(accounts)
- async.each(addresses, this._updateAccount.bind(this), cb)
+ await Promise.all(addresses.map(this._updateAccount.bind(this)))
}
/**
- * Updates the current balance of an account. Gets an updated balance via this._getAccount.
+ * Updates the current balance of an account.
*
* @private
* @param {string} address A hex address of a the account to be updated
- * @param {Function} cb A callback to call once the account at address is successfully update
+ * @returns {Promise} after the account balance is updated
*
*/
- _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)
- })
- }
-
- /**
- * Gets the current balance of an account via EthQuery.
- *
- * @private
- * @param {string} address A hex address of a the account to query
- * @param {Function} cb A callback to call once the account at address is successfully update
- *
- */
- _getAccount (address, cb = noop) {
- const query = this._query
- async.parallel({
- balance: query.getBalance.bind(query, address),
- }, cb)
+ async _updateAccount (address) {
+ // query balance
+ const balance = await this._query.getBalance(address)
+ const result = { address, balance }
+ // update accounts state
+ const { accounts } = this.store.getState()
+ // only populate if the entry is still present
+ if (!accounts[address]) return
+ accounts[address] = result
+ this.store.updateState({ accounts })
}
}
diff --git a/app/scripts/lib/auto-reload.js b/app/scripts/lib/auto-reload.js
index cce31c3d2..558391a06 100644
--- a/app/scripts/lib/auto-reload.js
+++ b/app/scripts/lib/auto-reload.js
@@ -2,18 +2,12 @@ module.exports = setupDappAutoReload
function setupDappAutoReload (web3, observable) {
// export web3 as a global, checking for usage
- let hasBeenWarned = false
let reloadInProgress = false
let lastTimeUsed
let lastSeenNetwork
global.web3 = new Proxy(web3, {
get: (_web3, key) => {
- // show warning once on web3 access
- if (!hasBeenWarned && key !== 'currentProvider') {
- console.warn('MetaMask: web3 will be deprecated in the near future in favor of the ethereumProvider \nhttps://github.com/MetaMask/faq/blob/master/detecting_metamask.md#web3-deprecation')
- hasBeenWarned = true
- }
// get the time of use
lastTimeUsed = Date.now()
// return value normally
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
deleted file mode 100644
index 221746467..000000000
--- a/app/scripts/lib/config-manager.js
+++ /dev/null
@@ -1,254 +0,0 @@
-const ethUtil = require('ethereumjs-util')
-const normalize = require('eth-sig-util').normalize
-const {
- MAINNET_RPC_URL,
- ROPSTEN_RPC_URL,
- KOVAN_RPC_URL,
- RINKEBY_RPC_URL,
-} = require('../controllers/network/enums')
-
-/* The config-manager is a convenience object
- * wrapping a pojo-migrator.
- *
- * It exists mostly to allow the creation of
- * convenience methods to access and persist
- * particular portions of the state.
- */
-module.exports = ConfigManager
-function ConfigManager (opts) {
- // ConfigManager is observable and will emit updates
- this._subs = []
- this.store = opts.store
-}
-
-ConfigManager.prototype.setConfig = function (config) {
- var data = this.getData()
- data.config = config
- this.setData(data)
- this._emitUpdates(config)
-}
-
-ConfigManager.prototype.getConfig = function () {
- var data = this.getData()
- return data.config
-}
-
-ConfigManager.prototype.setData = function (data) {
- this.store.putState(data)
-}
-
-ConfigManager.prototype.getData = function () {
- return this.store.getState()
-}
-
-ConfigManager.prototype.setPasswordForgotten = function (passwordForgottenState) {
- const data = this.getData()
- data.forgottenPassword = passwordForgottenState
- this.setData(data)
-}
-
-ConfigManager.prototype.getPasswordForgotten = function (passwordForgottenState) {
- const data = this.getData()
- return data.forgottenPassword
-}
-
-ConfigManager.prototype.setWallet = function (wallet) {
- var data = this.getData()
- data.wallet = wallet
- this.setData(data)
-}
-
-ConfigManager.prototype.setVault = function (encryptedString) {
- var data = this.getData()
- data.vault = encryptedString
- this.setData(data)
-}
-
-ConfigManager.prototype.getVault = function () {
- var data = this.getData()
- return data.vault
-}
-
-ConfigManager.prototype.getKeychains = function () {
- return this.getData().keychains || []
-}
-
-ConfigManager.prototype.setKeychains = function (keychains) {
- var data = this.getData()
- data.keychains = keychains
- this.setData(data)
-}
-
-ConfigManager.prototype.getSelectedAccount = function () {
- var config = this.getConfig()
- return config.selectedAccount
-}
-
-ConfigManager.prototype.setSelectedAccount = function (address) {
- var config = this.getConfig()
- config.selectedAccount = ethUtil.addHexPrefix(address)
- this.setConfig(config)
-}
-
-ConfigManager.prototype.getWallet = function () {
- return this.getData().wallet
-}
-
-// Takes a boolean
-ConfigManager.prototype.setShowSeedWords = function (should) {
- var data = this.getData()
- data.showSeedWords = should
- this.setData(data)
-}
-
-
-ConfigManager.prototype.getShouldShowSeedWords = function () {
- var data = this.getData()
- return data.showSeedWords
-}
-
-ConfigManager.prototype.setSeedWords = function (words) {
- var data = this.getData()
- data.seedWords = words
- this.setData(data)
-}
-
-ConfigManager.prototype.getSeedWords = function () {
- var data = this.getData()
- return data.seedWords
-}
-ConfigManager.prototype.setRpcTarget = function (rpcUrl) {
- var config = this.getConfig()
- config.provider = {
- type: 'rpc',
- rpcTarget: rpcUrl,
- }
- this.setConfig(config)
-}
-
-ConfigManager.prototype.setProviderType = function (type) {
- var config = this.getConfig()
- config.provider = {
- type: type,
- }
- this.setConfig(config)
-}
-
-ConfigManager.prototype.useEtherscanProvider = function () {
- var config = this.getConfig()
- config.provider = {
- type: 'etherscan',
- }
- this.setConfig(config)
-}
-
-ConfigManager.prototype.getProvider = function () {
- var config = this.getConfig()
- return config.provider
-}
-
-ConfigManager.prototype.getCurrentRpcAddress = function () {
- var provider = this.getProvider()
- if (!provider) return null
- switch (provider.type) {
-
- case 'mainnet':
- return MAINNET_RPC_URL
-
- case 'ropsten':
- return ROPSTEN_RPC_URL
-
- case 'kovan':
- return KOVAN_RPC_URL
-
- case 'rinkeby':
- return RINKEBY_RPC_URL
-
- default:
- return provider && provider.rpcTarget ? provider.rpcTarget : RINKEBY_RPC_URL
- }
-}
-
-//
-// Tx
-//
-
-ConfigManager.prototype.getTxList = function () {
- var data = this.getData()
- if (data.transactions !== undefined) {
- return data.transactions
- } else {
- return []
- }
-}
-
-ConfigManager.prototype.setTxList = function (txList) {
- var data = this.getData()
- data.transactions = txList
- this.setData(data)
-}
-
-
-// wallet nickname methods
-
-ConfigManager.prototype.getWalletNicknames = function () {
- var data = this.getData()
- const nicknames = ('walletNicknames' in data) ? data.walletNicknames : {}
- return nicknames
-}
-
-ConfigManager.prototype.nicknameForWallet = function (account) {
- const address = normalize(account)
- const nicknames = this.getWalletNicknames()
- return nicknames[address]
-}
-
-ConfigManager.prototype.setNicknameForWallet = function (account, nickname) {
- const address = normalize(account)
- const nicknames = this.getWalletNicknames()
- nicknames[address] = nickname
- var data = this.getData()
- data.walletNicknames = nicknames
- this.setData(data)
-}
-
-// observable
-
-ConfigManager.prototype.getSalt = function () {
- var data = this.getData()
- return data.salt
-}
-
-ConfigManager.prototype.setSalt = function (salt) {
- var data = this.getData()
- data.salt = salt
- this.setData(data)
-}
-
-ConfigManager.prototype.subscribe = function (fn) {
- this._subs.push(fn)
- var unsubscribe = this.unsubscribe.bind(this, fn)
- return unsubscribe
-}
-
-ConfigManager.prototype.unsubscribe = function (fn) {
- var index = this._subs.indexOf(fn)
- if (index !== -1) this._subs.splice(index, 1)
-}
-
-ConfigManager.prototype._emitUpdates = function (state) {
- this._subs.forEach(function (handler) {
- handler(state)
- })
-}
-
-ConfigManager.prototype.setLostAccounts = function (lostAccounts) {
- var data = this.getData()
- data.lostAccounts = lostAccounts
- this.setData(data)
-}
-
-ConfigManager.prototype.getLostAccounts = function () {
- var data = this.getData()
- return data.lostAccounts || []
-}
diff --git a/app/scripts/lib/createErrorMiddleware.js b/app/scripts/lib/createErrorMiddleware.js
deleted file mode 100644
index 7f6a4bd73..000000000
--- a/app/scripts/lib/createErrorMiddleware.js
+++ /dev/null
@@ -1,67 +0,0 @@
-const log = require('loglevel')
-
-/**
- * JSON-RPC error object
- *
- * @typedef {Object} RpcError
- * @property {number} code - Indicates the error type that occurred
- * @property {Object} [data] - Contains additional information about the error
- * @property {string} [message] - Short description of the error
- */
-
-/**
- * Middleware configuration object
- *
- * @typedef {Object} MiddlewareConfig
- * @property {boolean} [override] - Use RPC_ERRORS message in place of provider message
- */
-
-/**
- * Map of standard and non-standard RPC error codes to messages
- */
-const RPC_ERRORS = {
- 1: 'An unauthorized action was attempted.',
- 2: 'A disallowed action was attempted.',
- 3: 'An execution error occurred.',
- [-32600]: 'The JSON sent is not a valid Request object.',
- [-32601]: 'The method does not exist / is not available.',
- [-32602]: 'Invalid method parameter(s).',
- [-32603]: 'Internal JSON-RPC error.',
- [-32700]: 'Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.',
- internal: 'Internal server error.',
- unknown: 'Unknown JSON-RPC error.',
-}
-
-/**
- * Modifies a JSON-RPC error object in-place to add a human-readable message,
- * optionally overriding any provider-supplied message
- *
- * @param {RpcError} error - JSON-RPC error object
- * @param {boolean} override - Use RPC_ERRORS message in place of provider message
- */
-function sanitizeRPCError (error, override) {
- if (error.message && !override) { return error }
- const message = error.code > -31099 && error.code < -32100 ? RPC_ERRORS.internal : RPC_ERRORS[error.code]
- error.message = message || RPC_ERRORS.unknown
-}
-
-/**
- * json-rpc-engine middleware that both logs standard and non-standard error
- * messages and ends middleware stack traversal if an error is encountered
- *
- * @param {MiddlewareConfig} [config={override:true}] - Middleware configuration
- * @returns {Function} json-rpc-engine middleware function
- */
-function createErrorMiddleware ({ override = true } = {}) {
- return (req, res, next) => {
- next(done => {
- const { error } = res
- if (!error) { return done() }
- sanitizeRPCError(error)
- log.error(`MetaMask - RPC Error: ${error.message}`, error)
- done()
- })
- }
-}
-
-module.exports = createErrorMiddleware
diff --git a/app/scripts/lib/events-proxy.js b/app/scripts/lib/events-proxy.js
deleted file mode 100644
index f83773ccc..000000000
--- a/app/scripts/lib/events-proxy.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Returns an EventEmitter that proxies events from the given event emitter
- * @param {any} eventEmitter
- * @param {object} listeners - The listeners to proxy to
- * @returns {any}
- */
-module.exports = function createEventEmitterProxy (eventEmitter, listeners) {
- let target = eventEmitter
- const eventHandlers = listeners || {}
- const proxy = /** @type {any} */ (new Proxy({}, {
- get: (_, name) => {
- // intercept listeners
- if (name === 'on') return addListener
- if (name === 'setTarget') return setTarget
- if (name === 'proxyEventHandlers') return eventHandlers
- return (/** @type {any} */ (target))[name]
- },
- set: (_, name, value) => {
- target[name] = value
- return true
- },
- }))
- function setTarget (/** @type {EventEmitter} */ eventEmitter) {
- target = eventEmitter
- // migrate listeners
- Object.keys(eventHandlers).forEach((name) => {
- /** @type {Array<Function>} */ (eventHandlers[name]).forEach((handler) => target.on(name, handler))
- })
- }
- /**
- * Attaches a function to be called whenever the specified event is emitted
- * @param {string} name
- * @param {Function} 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
-}
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
deleted file mode 100644
index 6ef511453..000000000
--- a/app/scripts/lib/inpage-provider.js
+++ /dev/null
@@ -1,125 +0,0 @@
-const pump = require('pump')
-const RpcEngine = require('json-rpc-engine')
-const createErrorMiddleware = require('./createErrorMiddleware')
-const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
-const createStreamMiddleware = require('json-rpc-middleware-stream')
-const LocalStorageStore = require('obs-store')
-const asStream = require('obs-store/lib/asStream')
-const ObjectMultiplex = require('obj-multiplex')
-
-module.exports = MetamaskInpageProvider
-
-function MetamaskInpageProvider (connectionStream) {
- const self = this
-
- // setup connectionStream multiplexing
- const mux = self.mux = new ObjectMultiplex()
- pump(
- connectionStream,
- mux,
- connectionStream,
- (err) => logStreamDisconnectWarning('MetaMask', err)
- )
-
- // subscribe to metamask public config (one-way)
- self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
-
- pump(
- mux.createStream('publicConfig'),
- asStream(self.publicConfigStore),
- (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
- )
-
- // ignore phishing warning message (handled elsewhere)
- mux.ignoreStream('phishing')
-
- // connect to async provider
- const streamMiddleware = createStreamMiddleware()
- pump(
- streamMiddleware.stream,
- mux.createStream('provider'),
- streamMiddleware.stream,
- (err) => logStreamDisconnectWarning('MetaMask RpcProvider', err)
- )
-
- // handle sendAsync requests via dapp-side rpc engine
- const rpcEngine = new RpcEngine()
- rpcEngine.push(createIdRemapMiddleware())
- rpcEngine.push(createErrorMiddleware())
- 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
-
- if (payload.method === 'eth_signTypedData') {
- console.warn('MetaMask: This experimental version of eth_signTypedData will be deprecated in the next release in favor of the standard as defined in EIP-712. See https://git.io/fNzPl for more information on the new standard.')
- }
-
- self.rpcEngine.handle(payload, cb)
-}
-
-
-MetamaskInpageProvider.prototype.send = function (payload) {
- const self = this
-
- let selectedAddress
- let result = null
- switch (payload.method) {
-
- case 'eth_accounts':
- // read from localStorage
- selectedAddress = self.publicConfigStore.getState().selectedAddress
- result = selectedAddress ? [selectedAddress] : []
- break
-
- case 'eth_coinbase':
- // read from localStorage
- selectedAddress = self.publicConfigStore.getState().selectedAddress
- result = selectedAddress || null
- break
-
- case 'eth_uninstallFilter':
- self.sendAsync(payload, noop)
- result = true
- break
-
- case 'net_version':
- const networkVersion = self.publicConfigStore.getState().networkVersion
- result = networkVersion || null
- break
-
- // throw not-supported Error
- default:
- var link = 'https://github.com/MetaMask/faq/blob/master/DEVELOPERS.md#dizzy-all-async---think-of-metamask-as-a-light-client'
- var message = `The MetaMask Web3 object does not support synchronous methods like ${payload.method} without a callback parameter. See ${link} for details.`
- throw new Error(message)
-
- }
-
- // return the result
- return {
- id: payload.id,
- jsonrpc: payload.jsonrpc,
- result: result,
- }
-}
-
-MetamaskInpageProvider.prototype.isConnected = function () {
- return true
-}
-
-MetamaskInpageProvider.prototype.isMetaMask = true
-
-// util
-
-function logStreamDisconnectWarning (remoteLabel, err) {
- let warningMsg = `MetamaskInpageProvider - lost connection to ${remoteLabel}`
- if (err) warningMsg += '\n' + err.stack
- console.warn(warningMsg)
-}
-
-function noop () {}
diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js
index 5db63f47d..8b08453c4 100644
--- a/app/scripts/lib/ipfsContent.js
+++ b/app/scripts/lib/ipfsContent.js
@@ -5,6 +5,8 @@ module.exports = function (provider) {
function ipfsContent (details) {
const name = details.url.substring(7, details.url.length - 1)
let clearTime = null
+ if (/^.+\.eth$/.test(name) === false) return
+
extension.tabs.query({active: true}, tab => {
extension.tabs.update(tab.id, { url: 'loading.html' })
@@ -34,7 +36,7 @@ module.exports = function (provider) {
return { cancel: true }
}
- extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']})
+ extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/'], types: ['main_frame']})
return {
remove () {
diff --git a/app/scripts/lib/message-manager.js b/app/scripts/lib/message-manager.js
index 901367f04..e86629590 100644
--- a/app/scripts/lib/message-manager.js
+++ b/app/scripts/lib/message-manager.js
@@ -69,10 +69,39 @@ module.exports = class MessageManager extends EventEmitter {
* new Message to this.messages, and to save the unapproved Messages from that list to this.memStore.
*
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
+ * @param {Object} req (optional) The original request object possibly containing the origin
+ * @returns {promise} after signature has been
+ *
+ */
+ addUnapprovedMessageAsync (msgParams, req) {
+ return new Promise((resolve, reject) => {
+ const msgId = this.addUnapprovedMessage(msgParams, req)
+ // await finished
+ this.once(`${msgId}:finished`, (data) => {
+ switch (data.status) {
+ case 'signed':
+ return resolve(data.rawSig)
+ case 'rejected':
+ return reject(new Error('MetaMask Message Signature: User denied message signature.'))
+ default:
+ return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
+ }
+ })
+ })
+ }
+
+ /**
+ * Creates a new Message with an 'unapproved' status using the passed msgParams. this.addMsg is called to add the
+ * new Message to this.messages, and to save the unapproved Messages from that list to this.memStore.
+ *
+ * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
+ * @param {Object} req (optional) The original request object where the origin may be specificied
* @returns {number} The id of the newly created message.
*
*/
- addUnapprovedMessage (msgParams) {
+ addUnapprovedMessage (msgParams, req) {
+ // add origin from request
+ if (req) msgParams.origin = req.origin
msgParams.data = normalizeMsgData(msgParams.data)
// create txData obj with parameters and meta data
var time = (new Date()).getTime()
@@ -243,6 +272,6 @@ function normalizeMsgData (data) {
return data
} else {
// data is unicode, convert to hex
- return ethUtil.bufferToHex(new Buffer(data, 'utf8'))
+ return ethUtil.bufferToHex(Buffer.from(data, 'utf8'))
}
}
diff --git a/app/scripts/lib/personal-message-manager.js b/app/scripts/lib/personal-message-manager.js
index e96ced1f2..fdb94f5ec 100644
--- a/app/scripts/lib/personal-message-manager.js
+++ b/app/scripts/lib/personal-message-manager.js
@@ -73,11 +73,43 @@ module.exports = class PersonalMessageManager extends EventEmitter {
* this.memStore.
*
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
+ * @param {Object} req (optional) The original request object possibly containing the origin
+ * @returns {promise} When the message has been signed or rejected
+ *
+ */
+ addUnapprovedMessageAsync (msgParams, req) {
+ return new Promise((resolve, reject) => {
+ if (!msgParams.from) {
+ reject(new Error('MetaMask Message Signature: from field is required.'))
+ }
+ const msgId = this.addUnapprovedMessage(msgParams, req)
+ this.once(`${msgId}:finished`, (data) => {
+ switch (data.status) {
+ case 'signed':
+ return resolve(data.rawSig)
+ case 'rejected':
+ return reject(new Error('MetaMask Message Signature: User denied message signature.'))
+ default:
+ return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
+ }
+ })
+ })
+ }
+
+ /**
+ * Creates a new PersonalMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
+ * the new PersonalMessage to this.messages, and to save the unapproved PersonalMessages from that list to
+ * this.memStore.
+ *
+ * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
+ * @param {Object} req (optional) The original request object possibly containing the origin
* @returns {number} The id of the newly created PersonalMessage.
*
*/
- addUnapprovedMessage (msgParams) {
+ addUnapprovedMessage (msgParams, req) {
log.debug(`PersonalMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
+ // add origin from request
+ if (req) msgParams.origin = req.origin
msgParams.data = this.normalizeMsgData(msgParams.data)
// create txData obj with parameters and meta data
var time = (new Date()).getTime()
@@ -253,8 +285,7 @@ module.exports = class PersonalMessageManager extends EventEmitter {
log.debug(`Message was not hex encoded, interpreting as utf8.`)
}
- return ethUtil.bufferToHex(new Buffer(data, 'utf8'))
+ return ethUtil.bufferToHex(Buffer.from(data, 'utf8'))
}
}
-
diff --git a/app/scripts/lib/port-stream.js b/app/scripts/lib/port-stream.js
deleted file mode 100644
index fd65d94f3..000000000
--- a/app/scripts/lib/port-stream.js
+++ /dev/null
@@ -1,80 +0,0 @@
-const Duplex = require('readable-stream').Duplex
-const inherits = require('util').inherits
-const noop = function () {}
-
-module.exports = PortDuplexStream
-
-inherits(PortDuplexStream, Duplex)
-
-/**
- * Creates a stream that's both readable and writable.
- * The stream supports arbitrary objects.
- *
- * @class
- * @param {Object} port Remote Port object
- */
-function PortDuplexStream (port) {
- Duplex.call(this, {
- objectMode: true,
- })
- this._port = port
- port.onMessage.addListener(this._onMessage.bind(this))
- port.onDisconnect.addListener(this._onDisconnect.bind(this))
-}
-
-/**
- * Callback triggered when a message is received from
- * the remote Port associated with this Stream.
- *
- * @private
- * @param {Object} msg - Payload from the onMessage listener of Port
- */
-PortDuplexStream.prototype._onMessage = function (msg) {
- if (Buffer.isBuffer(msg)) {
- delete msg._isBuffer
- var data = new Buffer(msg)
- this.push(data)
- } else {
- this.push(msg)
- }
-}
-
-/**
- * Callback triggered when the remote Port
- * associated with this Stream disconnects.
- *
- * @private
- */
-PortDuplexStream.prototype._onDisconnect = function () {
- this.destroy()
-}
-
-/**
- * Explicitly sets read operations to a no-op
- */
-PortDuplexStream.prototype._read = noop
-
-
-/**
- * Called internally when data should be written to
- * this writable stream.
- *
- * @private
- * @param {*} msg Arbitrary object to write
- * @param {string} encoding Encoding to use when writing payload
- * @param {Function} cb Called when writing is complete or an error occurs
- */
-PortDuplexStream.prototype._write = function (msg, encoding, cb) {
- try {
- if (Buffer.isBuffer(msg)) {
- var data = msg.toJSON()
- data._isBuffer = true
- this._port.postMessage(data)
- } else {
- this._port.postMessage(msg)
- }
- } catch (err) {
- return cb(new Error('PortDuplexStream - disconnected'))
- }
- cb()
-}
diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js
index 3651524f1..e6e511640 100644
--- a/app/scripts/lib/setupRaven.js
+++ b/app/scripts/lib/setupRaven.js
@@ -70,11 +70,11 @@ function simplifyErrorMessages (report) {
function rewriteErrorMessages (report, rewriteFn) {
// rewrite top level message
- if (report.message) report.message = rewriteFn(report.message)
+ if (typeof report.message === 'string') report.message = rewriteFn(report.message)
// rewrite each exception message
if (report.exception && report.exception.values) {
report.exception.values.forEach(item => {
- item.value = rewriteFn(item.value)
+ if (typeof item.value === 'string') item.value = rewriteFn(item.value)
})
}
}
diff --git a/app/scripts/lib/typed-message-manager.js b/app/scripts/lib/typed-message-manager.js
index c58921610..b10145f3b 100644
--- a/app/scripts/lib/typed-message-manager.js
+++ b/app/scripts/lib/typed-message-manager.js
@@ -4,6 +4,7 @@ const createId = require('./random-id')
const assert = require('assert')
const sigUtil = require('eth-sig-util')
const log = require('loglevel')
+const jsonschema = require('jsonschema')
/**
* Represents, and contains data about, an 'eth_signTypedData' type signature request. These are created when a
@@ -17,7 +18,7 @@ const log = require('loglevel')
* @property {Object} msgParams.from The address that is making the signature request.
* @property {string} msgParams.data A hex string conversion of the raw buffer data of the signature request
* @property {number} time The epoch time at which the this message was created
- * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed' or 'rejected'
+ * @property {string} status Indicates whether the signature request is 'unapproved', 'approved', 'signed', 'rejected', or 'errored'
* @property {string} type The json-prc signing method for which a signature request has been made. A 'Message' will
* always have a 'eth_signTypedData' type.
*
@@ -26,17 +27,10 @@ const log = require('loglevel')
module.exports = class TypedMessageManager extends EventEmitter {
/**
* Controller in charge of managing - storing, adding, removing, updating - TypedMessage.
- *
- * @typedef {Object} TypedMessage
- * @param {Object} opts @deprecated
- * @property {Object} memStore The observable store where TypedMessage are saved.
- * @property {Object} memStore.unapprovedTypedMessages A collection of all TypedMessages in the 'unapproved' state
- * @property {number} memStore.unapprovedTypedMessagesCount The count of all TypedMessages in this.memStore.unapprobedMsgs
- * @property {array} messages Holds all messages that have been created by this TypedMessage
- *
*/
- constructor (opts) {
+ constructor ({ networkController }) {
super()
+ this.networkController = networkController
this.memStore = new ObservableStore({
unapprovedTypedMessages: {},
unapprovedTypedMessagesCount: 0,
@@ -72,11 +66,43 @@ module.exports = class TypedMessageManager extends EventEmitter {
* this.memStore. Before any of this is done, msgParams are validated
*
* @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
+ * @param {Object} req (optional) The original request object possibly containing the origin
+ * @returns {promise} When the message has been signed or rejected
+ *
+ */
+ addUnapprovedMessageAsync (msgParams, req, version) {
+ return new Promise((resolve, reject) => {
+ const msgId = this.addUnapprovedMessage(msgParams, req, version)
+ this.once(`${msgId}:finished`, (data) => {
+ switch (data.status) {
+ case 'signed':
+ return resolve(data.rawSig)
+ case 'rejected':
+ return reject(new Error('MetaMask Message Signature: User denied message signature.'))
+ case 'errored':
+ return reject(new Error(`MetaMask Message Signature: ${data.error}`))
+ default:
+ return reject(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`))
+ }
+ })
+ })
+ }
+
+ /**
+ * Creates a new TypedMessage with an 'unapproved' status using the passed msgParams. this.addMsg is called to add
+ * the new TypedMessage to this.messages, and to save the unapproved TypedMessages from that list to
+ * this.memStore. Before any of this is done, msgParams are validated
+ *
+ * @param {Object} msgParams The params for the eth_sign call to be made after the message is approved.
+ * @param {Object} req (optional) The original request object possibly containing the origin
* @returns {number} The id of the newly created TypedMessage.
*
*/
- addUnapprovedMessage (msgParams) {
+ addUnapprovedMessage (msgParams, req, version) {
+ msgParams.version = version
this.validateParams(msgParams)
+ // add origin from request
+ if (req) msgParams.origin = req.origin
log.debug(`TypedMessageManager addUnapprovedMessage: ${JSON.stringify(msgParams)}`)
// create txData obj with parameters and meta data
@@ -103,14 +129,33 @@ module.exports = class TypedMessageManager extends EventEmitter {
*
*/
validateParams (params) {
- assert.equal(typeof params, 'object', 'Params should ben an object.')
- assert.ok('data' in params, 'Params must include a data field.')
- assert.ok('from' in params, 'Params must include a from field.')
- assert.ok(Array.isArray(params.data), 'Data should be an array.')
- assert.equal(typeof params.from, 'string', 'From field must be a string.')
- assert.doesNotThrow(() => {
- sigUtil.typedSignatureHash(params.data)
- }, 'Expected EIP712 typed data')
+ switch (params.version) {
+ case 'V1':
+ assert.equal(typeof params, 'object', 'Params should ben an object.')
+ assert.ok('data' in params, 'Params must include a data field.')
+ assert.ok('from' in params, 'Params must include a from field.')
+ assert.ok(Array.isArray(params.data), 'Data should be an array.')
+ assert.equal(typeof params.from, 'string', 'From field must be a string.')
+ assert.doesNotThrow(() => {
+ sigUtil.typedSignatureHash(params.data)
+ }, 'Expected EIP712 typed data')
+ break
+ case 'V3':
+ let data
+ assert.equal(typeof params, 'object', 'Params should be an object.')
+ assert.ok('data' in params, 'Params must include a data field.')
+ assert.ok('from' in params, 'Params must include a from field.')
+ assert.equal(typeof params.from, 'string', 'From field must be a string.')
+ assert.equal(typeof params.data, 'string', 'Data must be passed as a valid JSON string.')
+ assert.doesNotThrow(() => { data = JSON.parse(params.data) }, 'Data must be passed as a valid JSON string.')
+ const validation = jsonschema.validate(data, sigUtil.TYPED_MESSAGE_SCHEMA)
+ assert.ok(data.primaryType in data.types, `Primary type of "${data.primaryType}" has no type definition.`)
+ assert.equal(validation.errors.length, 0, 'Data must conform to EIP-712 schema. See https://git.io/fNtcx.')
+ const chainId = data.domain.chainId
+ const activeChainId = parseInt(this.networkController.getNetworkState())
+ chainId && assert.equal(chainId, activeChainId, `Provided chainId (${chainId}) must match the active chainId (${activeChainId})`)
+ break
+ }
}
/**
@@ -185,6 +230,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
*/
prepMsgForSigning (msgParams) {
delete msgParams.metamaskId
+ delete msgParams.version
return Promise.resolve(msgParams)
}
@@ -198,6 +244,19 @@ module.exports = class TypedMessageManager extends EventEmitter {
this._setMsgStatus(msgId, 'rejected')
}
+ /**
+ * Sets a TypedMessage status to 'errored' via a call to this._setMsgStatus.
+ *
+ * @param {number} msgId The id of the TypedMessage to error
+ *
+ */
+ errorMessage (msgId, error) {
+ const msg = this.getMsg(msgId)
+ msg.error = error
+ this._updateMsg(msg)
+ this._setMsgStatus(msgId, 'errored')
+ }
+
//
// PRIVATE METHODS
//
@@ -221,7 +280,7 @@ module.exports = class TypedMessageManager extends EventEmitter {
msg.status = status
this._updateMsg(msg)
this.emit(`${msgId}:${status}`, msg)
- if (status === 'rejected' || status === 'signed') {
+ if (status === 'rejected' || status === 'signed' || status === 'errored') {
this.emit(`${msgId}:finished`, msg)
}
}
diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js
index d7423f2ad..ea13b26be 100644
--- a/app/scripts/lib/util.js
+++ b/app/scripts/lib/util.js
@@ -127,7 +127,21 @@ function BnMultiplyByFraction (targetBN, numerator, denominator) {
return targetBN.mul(numBN).div(denomBN)
}
+function applyListeners (listeners, emitter) {
+ Object.keys(listeners).forEach((key) => {
+ emitter.on(key, listeners[key])
+ })
+}
+
+function removeListeners (listeners, emitter) {
+ Object.keys(listeners).forEach((key) => {
+ emitter.removeListener(key, listeners[key])
+ })
+}
+
module.exports = {
+ removeListeners,
+ applyListeners,
getPlatform,
getStack,
getEnvironmentType,
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 1e1aa035f..493877345 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -15,6 +15,7 @@ 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 createSubscriptionManager = require('eth-json-rpc-filters/subscriptionManager')
const createOriginMiddleware = require('./lib/createOriginMiddleware')
const createLoggerMiddleware = require('./lib/createLoggerMiddleware')
const createProviderMiddleware = require('./lib/createProviderMiddleware')
@@ -36,7 +37,6 @@ const TransactionController = require('./controllers/transactions')
const BalancesController = require('./controllers/computed-balances')
const TokenRatesController = require('./controllers/token-rates')
const DetectTokensController = require('./controllers/detect-tokens')
-const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
@@ -46,10 +46,12 @@ const BN = require('ethereumjs-util').BN
const GWEI_BN = new BN('1000000000')
const percentile = require('percentile')
const seedPhraseVerifier = require('./lib/seed-phrase-verifier')
-const cleanErrorStack = require('./lib/cleanErrorStack')
const log = require('loglevel')
const TrezorKeyring = require('eth-trezor-keyring')
const LedgerBridgeKeyring = require('eth-ledger-bridge-keyring')
+const EthQuery = require('eth-query')
+const ethUtil = require('ethereumjs-util')
+const sigUtil = require('eth-sig-util')
module.exports = class MetamaskController extends EventEmitter {
@@ -67,6 +69,10 @@ module.exports = class MetamaskController extends EventEmitter {
const initState = opts.initState || {}
this.recordFirstTimeInfo(initState)
+ // this keeps track of how many "controllerStream" connections are open
+ // the only thing that uses controller connections are open metamask UI instances
+ this.activeControllerConnections = 0
+
// platform-specific api
this.platform = opts.platform
@@ -79,15 +85,11 @@ module.exports = class MetamaskController extends EventEmitter {
// network store
this.networkController = new NetworkController(initState.NetworkController)
- // config manager
- this.configManager = new ConfigManager({
- store: this.store,
- })
-
// preferences controller
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
+ showWatchAssetUi: opts.showWatchAssetUi,
network: this.networkController,
})
@@ -108,8 +110,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.blacklistController.scheduleUpdates()
// rpc provider
- this.provider = this.initializeProvider()
- this.blockTracker = this.provider._blockTracker
+ this.initializeProvider()
+ this.provider = this.networkController.getProviderAndBlockTracker().provider
+ this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker
// token exchange rate tracker
this.tokenRatesController = new TokenRatesController({
@@ -126,6 +129,14 @@ module.exports = class MetamaskController extends EventEmitter {
provider: this.provider,
blockTracker: this.blockTracker,
})
+ // start and stop polling for balances based on activeControllerConnections
+ this.on('controllerConnectionChanged', (activeControllerConnections) => {
+ if (activeControllerConnections > 0) {
+ this.accountTracker.start()
+ } else {
+ this.accountTracker.stop()
+ }
+ })
// key mgmt
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
@@ -136,19 +147,7 @@ module.exports = class MetamaskController extends EventEmitter {
encryptor: opts.encryptor || undefined,
})
- // If only one account exists, make sure it is selected.
- this.keyringController.memStore.subscribe((state) => {
- const addresses = state.keyrings.reduce((res, keyring) => {
- return res.concat(keyring.accounts)
- }, [])
- if (addresses.length === 1) {
- const address = addresses[0]
- this.preferencesController.setSelectedAddress(address)
- }
- // ensure preferences + identities controller know about all addresses
- this.preferencesController.addAddresses(addresses)
- this.accountTracker.syncWithAddresses(addresses)
- })
+ this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s))
// detect tokens controller
this.detectTokensController = new DetectTokensController({
@@ -175,7 +174,7 @@ module.exports = class MetamaskController extends EventEmitter {
blockTracker: this.blockTracker,
getGasPrice: this.getGasPrice.bind(this),
})
- this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts))
+ this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
this.txController.on(`tx:status-update`, (txId, status) => {
if (status === 'confirmed' || status === 'failed') {
@@ -209,7 +208,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.networkController.lookupNetwork()
this.messageManager = new MessageManager()
this.personalMessageManager = new PersonalMessageManager()
- this.typedMessageManager = new TypedMessageManager()
+ this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
this.publicConfigStore = this.initPublicConfigStore()
this.store.updateStructure({
@@ -253,30 +252,25 @@ module.exports = class MetamaskController extends EventEmitter {
static: {
eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`,
- eth_sendTransaction: (payload, next, end) => {
- const origin = payload.origin
- const txParams = payload.params[0]
- nodeify(this.txController.newUnapprovedTransaction, this.txController)(txParams, { origin }, end)
- },
},
+ version,
// account mgmt
- getAccounts: (cb) => {
+ getAccounts: async () => {
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
- const result = []
const selectedAddress = this.preferencesController.getSelectedAddress()
-
// only show address if account is unlocked
if (isUnlocked && selectedAddress) {
- result.push(selectedAddress)
+ return [selectedAddress]
+ } else {
+ return []
}
- cb(null, result)
},
// tx signing
- // old style msg signing
- processMessage: this.newUnsignedMessage.bind(this),
- // personal_sign msg signing
+ processTransaction: this.newUnapprovedTransaction.bind(this),
+ // msg signing
+ processEthSignMessage: this.newUnsignedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
- processTypedMessage: this.newUnsignedTypedMessage.bind(this),
+ getPendingNonce: this.getPendingNonce.bind(this),
}
const providerProxy = this.networkController.initializeProvider(providerOpts)
return providerProxy
@@ -318,18 +312,15 @@ module.exports = class MetamaskController extends EventEmitter {
* @returns {Object} status
*/
getState () {
- const wallet = this.configManager.getWallet()
const vault = this.keyringController.store.getState().vault
- const isInitialized = (!!wallet || !!vault)
+ const isInitialized = !!vault
return {
...{ isInitialized },
...this.memStore.getFlatState(),
- ...this.configManager.getConfig(),
...{
- lostAccounts: this.configManager.getLostAccounts(),
- seedWords: this.configManager.getSeedWords(),
- forgottenPassword: this.configManager.getPasswordForgotten(),
+ // TODO: Remove usages of lost accounts
+ lostAccounts: [],
},
}
}
@@ -386,15 +377,20 @@ module.exports = class MetamaskController extends EventEmitter {
// network management
setProviderType: nodeify(networkController.setProviderType, networkController),
setCustomRpc: nodeify(this.setCustomRpc, this),
+ delCustomRpc: nodeify(this.delCustomRpc, this),
// PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController),
+ removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController),
setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
+ // BlacklistController
+ whitelistPhishingDomain: this.whitelistPhishingDomain.bind(this),
+
// AddressController
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
@@ -410,6 +406,7 @@ module.exports = class MetamaskController extends EventEmitter {
updateTransaction: nodeify(txController.updateTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this),
+ createCancelTransaction: nodeify(this.createCancelTransaction, this),
getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
estimateGas: nodeify(this.estimateGas, this),
@@ -480,12 +477,32 @@ module.exports = class MetamaskController extends EventEmitter {
async createNewVaultAndRestore (password, seed) {
const releaseLock = await this.createVaultMutex.acquire()
try {
+ let accounts, lastBalance
+
+ const keyringController = this.keyringController
+
// clear known identities
this.preferencesController.setAddresses([])
// create new vault
- const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
+ const vault = await keyringController.createNewVaultAndRestore(password, seed)
+
+ const ethQuery = new EthQuery(this.provider)
+ accounts = await keyringController.getAccounts()
+ lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery)
+
+ const primaryKeyring = keyringController.getKeyringsByType('HD Key Tree')[0]
+ if (!primaryKeyring) {
+ throw new Error('MetamaskController - No HD Key Tree found')
+ }
+
+ // seek out the first zero balance
+ while (lastBalance !== '0x0') {
+ await keyringController.addNewAccount(primaryKeyring)
+ accounts = await keyringController.getAccounts()
+ lastBalance = await this.getBalance(accounts[accounts.length - 1], ethQuery)
+ }
+
// set new identities
- const accounts = await this.keyringController.getAccounts()
this.preferencesController.setAddresses(accounts)
this.selectFirstIdentity()
releaseLock()
@@ -496,6 +513,30 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
+ /**
+ * Get an account balance from the AccountTracker or request it directly from the network.
+ * @param {string} address - The account address
+ * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network
+ */
+ getBalance (address, ethQuery) {
+ return new Promise((resolve, reject) => {
+ const cached = this.accountTracker.store.getState().accounts[address]
+
+ if (cached && cached.balance) {
+ resolve(cached.balance)
+ } else {
+ ethQuery.getBalance(address, (error, balance) => {
+ if (error) {
+ reject(error)
+ log.error(error)
+ } else {
+ resolve(balance || '0x0')
+ }
+ })
+ }
+ })
+ }
+
/*
* Submits the user's password and attempts to unlock the vault.
* Also synchronizes the preferencesController, to ensure its schema
@@ -515,6 +556,8 @@ module.exports = class MetamaskController extends EventEmitter {
}
await this.preferencesController.syncAddresses(accounts)
+ await this.balancesController.updateAllBalances()
+ await this.txController.pendingTxTracker.updatePendingTxs()
return this.keyringController.fullUpdate()
}
@@ -629,7 +672,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController.setAddresses(newAccounts)
newAccounts.forEach(address => {
if (!oldAccounts.includes(address)) {
- this.preferencesController.setAccountLabel(address, `${deviceName.toUpperCase()} ${parseInt(index, 10) + 1}`)
+ // Set the account label to Trezor 1 / Ledger 1, etc
+ this.preferencesController.setAccountLabel(address, `${deviceName[0].toUpperCase()}${deviceName.slice(1)} ${parseInt(index, 10) + 1}`)
+ // Select the account
this.preferencesController.setSelectedAddress(address)
}
})
@@ -683,7 +728,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.verifySeedPhrase()
.then((seedWords) => {
- this.configManager.setSeedWords(seedWords)
+ this.preferencesController.setSeedWords(seedWords)
return cb(null, seedWords)
})
.catch((err) => {
@@ -732,7 +777,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {function} cb Callback function called with the current address.
*/
clearSeedWordCache (cb) {
- this.configManager.setSeedWords(null)
+ this.preferencesController.setSeedWords(null)
cb(null, this.preferencesController.getSelectedAddress())
}
@@ -761,7 +806,8 @@ module.exports = class MetamaskController extends EventEmitter {
// Remove account from the preferences controller
this.preferencesController.removeAddress(address)
// Remove account from the account tracker controller
- this.accountTracker.removeAccount(address)
+ this.accountTracker.removeAccount([address])
+
// Remove account from the keyring
await this.keyringController.removeAccount(address)
return address
@@ -791,6 +837,18 @@ module.exports = class MetamaskController extends EventEmitter {
// ---------------------------------------------------------------------------
// Identity Management (signature operations)
+ /**
+ * Called when a Dapp suggests a new tx to be signed.
+ * this wrapper needs to exist so we can provide a reference to
+ * "newUnapprovedTransaction" before "txController" is instantiated
+ *
+ * @param {Object} msgParams - The params passed to eth_sign.
+ * @param {Object} req - (optional) the original request, containing the origin
+ */
+ async newUnapprovedTransaction (txParams, req) {
+ return await this.txController.newUnapprovedTransaction(txParams, req)
+ }
+
// eth_sign methods:
/**
@@ -802,20 +860,11 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_sign.
* @param {Function} cb = The callback function called with the signature.
*/
- newUnsignedMessage (msgParams, cb) {
- const msgId = this.messageManager.addUnapprovedMessage(msgParams)
+ newUnsignedMessage (msgParams, req) {
+ const promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
- this.messageManager.once(`${msgId}:finished`, (data) => {
- switch (data.status) {
- case 'signed':
- return cb(null, data.rawSig)
- case 'rejected':
- return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
- default:
- return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
- }
- })
+ return promise
}
/**
@@ -869,24 +918,11 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - The callback function called with the signature.
* Passed back to the requesting Dapp.
*/
- newUnsignedPersonalMessage (msgParams, cb) {
- if (!msgParams.from) {
- return cb(cleanErrorStack(new Error('MetaMask Message Signature: from field is required.')))
- }
-
- const msgId = this.personalMessageManager.addUnapprovedMessage(msgParams)
+ async newUnsignedPersonalMessage (msgParams, req) {
+ const promise = this.personalMessageManager.addUnapprovedMessageAsync(msgParams, req)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
- this.personalMessageManager.once(`${msgId}:finished`, (data) => {
- switch (data.status) {
- case 'signed':
- return cb(null, data.rawSig)
- case 'rejected':
- return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
- default:
- return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
- }
- })
+ return promise
}
/**
@@ -935,26 +971,11 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @param {Function} cb - The callback function, called with the signature.
*/
- newUnsignedTypedMessage (msgParams, cb) {
- let msgId
- try {
- msgId = this.typedMessageManager.addUnapprovedMessage(msgParams)
- this.sendUpdate()
- this.opts.showUnconfirmedMessage()
- } catch (e) {
- return cb(e)
- }
-
- this.typedMessageManager.once(`${msgId}:finished`, (data) => {
- switch (data.status) {
- case 'signed':
- return cb(null, data.rawSig)
- case 'rejected':
- return cb(cleanErrorStack(new Error('MetaMask Message Signature: User denied message signature.')))
- default:
- return cb(cleanErrorStack(new Error(`MetaMask Message Signature: Unknown problem: ${JSON.stringify(msgParams)}`)))
- }
- })
+ newUnsignedTypedMessage (msgParams, req) {
+ const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req)
+ this.sendUpdate()
+ this.opts.showUnconfirmedMessage()
+ return promise
}
/**
@@ -964,22 +985,31 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @returns {Object} Full state update.
*/
- signTypedMessage (msgParams) {
- log.info('MetaMaskController - signTypedMessage')
+ async signTypedMessage (msgParams) {
+ log.info('MetaMaskController - eth_signTypedData')
const msgId = msgParams.metamaskId
- // sets the status op the message to 'approved'
- // and removes the metamaskId for signing
- return this.typedMessageManager.approveMessage(msgParams)
- .then((cleanMsgParams) => {
- // signs the message
- return this.keyringController.signTypedMessage(cleanMsgParams)
- })
- .then((rawSig) => {
- // tells the listener that the message has been signed
- // and can be returned to the dapp
- this.typedMessageManager.setMsgStatusSigned(msgId, rawSig)
- return this.getState()
- })
+ const version = msgParams.version
+ try {
+ const cleanMsgParams = await this.typedMessageManager.approveMessage(msgParams)
+ const address = sigUtil.normalize(cleanMsgParams.from)
+ const keyring = await this.keyringController.getKeyringForAccount(address)
+ const wallet = keyring._getWalletForAccount(address)
+ const privKey = ethUtil.toBuffer(wallet.getPrivateKey())
+ let signature
+ switch (version) {
+ case 'V1':
+ signature = sigUtil.signTypedDataLegacy(privKey, { data: cleanMsgParams.data })
+ break
+ case 'V3':
+ signature = sigUtil.signTypedData(privKey, { data: JSON.parse(cleanMsgParams.data) })
+ break
+ }
+ this.typedMessageManager.setMsgStatusSigned(msgId, signature)
+ return this.getState()
+ } catch (error) {
+ log.info('MetaMaskController - eth_signTypedData failed.', error)
+ this.typedMessageManager.errorMessage(msgId, error)
+ }
}
/**
@@ -1017,36 +1047,17 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A legacy method used to record user confirmation that they understand
* that some of their accounts have been recovered but should be backed up.
+ * This function no longer does anything and will be removed.
*
* @deprecated
* @param {Function} cb - A callback function called with a full state update.
*/
markAccountsFound (cb) {
- this.configManager.setLostAccounts([])
- this.sendUpdate()
+ // TODO Remove me
cb(null, this.getState())
}
/**
- * A legacy method (probably dead code) that was used when we swapped out our
- * key management library that we depended on.
- *
- * Described in:
- * https://medium.com/metamask/metamask-3-migration-guide-914b79533cdd
- *
- * @deprecated
- * @param {} migratorOutput
- */
- restoreOldLostAccounts (migratorOutput) {
- const { lostAccounts } = migratorOutput
- if (lostAccounts) {
- this.configManager.setLostAccounts(lostAccounts.map(acct => acct.address))
- return this.importLostAccounts(migratorOutput)
- }
- return Promise.resolve(migratorOutput)
- }
-
- /**
* An account object
* @typedef Account
* @property string privateKey - The private key of the account.
@@ -1090,6 +1101,19 @@ module.exports = class MetamaskController extends EventEmitter {
return state
}
+ /**
+ * Allows a user to attempt to cancel a previously submitted transaction by creating a new
+ * transaction.
+ * @param {number} originalTxId - the id of the txMeta that you want to attempt to cancel
+ * @param {string=} customGasPrice - the hex value to use for the cancel transaction
+ * @returns {object} MetaMask state
+ */
+ async createCancelTransaction (originalTxId, customGasPrice, cb) {
+ await this.txController.createCancelTransaction(originalTxId, customGasPrice)
+ const state = await this.getState()
+ return state
+ }
+
estimateGas (estimateGasParams) {
return new Promise((resolve, reject) => {
return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => {
@@ -1111,7 +1135,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - A callback function called when complete.
*/
markPasswordForgotten (cb) {
- this.configManager.setPasswordForgotten(true)
+ this.preferencesController.setPasswordForgotten(true)
this.sendUpdate()
cb()
}
@@ -1121,7 +1145,7 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - A callback function called when complete.
*/
unMarkPasswordForgotten (cb) {
- this.configManager.setPasswordForgotten(false)
+ this.preferencesController.setPasswordForgotten(false)
this.sendUpdate()
cb()
}
@@ -1192,18 +1216,28 @@ module.exports = class MetamaskController extends EventEmitter {
setupControllerConnection (outStream) {
const api = this.getApi()
const dnode = Dnode(api)
+ // report new active controller connection
+ this.activeControllerConnections++
+ this.emit('controllerConnectionChanged', this.activeControllerConnections)
+ // connect dnode api to remote connection
pump(
outStream,
dnode,
outStream,
(err) => {
+ // report new active controller connection
+ this.activeControllerConnections--
+ this.emit('controllerConnectionChanged', this.activeControllerConnections)
+ // report any error
if (err) log.error(err)
}
)
dnode.on('remote', (remote) => {
// push updates to popup
- const sendUpdate = remote.sendUpdate.bind(remote)
+ const sendUpdate = (update) => remote.sendUpdate(update)
this.on('update', sendUpdate)
+ // remove update listener once the connection ends
+ dnode.on('end', () => this.removeListener('update', sendUpdate))
})
}
@@ -1215,20 +1249,33 @@ module.exports = class MetamaskController extends EventEmitter {
setupProviderConnection (outStream, origin) {
// setup json rpc engine stack
const engine = new RpcEngine()
+ const provider = this.provider
+ const blockTracker = this.blockTracker
// create filter polyfill middleware
- const filterMiddleware = createFilterMiddleware({
- provider: this.provider,
- blockTracker: this.provider._blockTracker,
- })
+ const filterMiddleware = createFilterMiddleware({ provider, blockTracker })
+ // create subscription polyfill middleware
+ const subscriptionManager = createSubscriptionManager({ provider, blockTracker })
+ subscriptionManager.events.on('notification', (message) => engine.emit('notification', message))
+ // metadata
engine.push(createOriginMiddleware({ origin }))
engine.push(createLoggerMiddleware({ origin }))
+ // filter and subscription polyfills
engine.push(filterMiddleware)
- engine.push(createProviderMiddleware({ provider: this.provider }))
+ engine.push(subscriptionManager.middleware)
+ // watch asset
+ engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
+ // sign typed data middleware
+ engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this))
+ engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this))
+ engine.push(this.createTypedDataMiddleware('eth_signTypedData_v3', 'V3', true).bind(this))
+ // forward to metamask primary provider
+ engine.push(createProviderMiddleware({ provider }))
// setup connection
const providerStream = createEngineStream({ engine })
+
pump(
outStream,
providerStream,
@@ -1252,16 +1299,46 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {*} outStream - The stream to provide public config over.
*/
setupPublicConfig (outStream) {
+ const configStream = asStream(this.publicConfigStore)
pump(
- asStream(this.publicConfigStore),
+ configStream,
outStream,
(err) => {
+ configStream.destroy()
if (err) log.error(err)
}
)
}
/**
+ * Handle a KeyringController update
+ * @param {object} state the KC state
+ * @return {Promise<void>}
+ * @private
+ */
+ async _onKeyringControllerUpdate (state) {
+ const {isUnlocked, keyrings} = state
+ const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), [])
+
+ if (!addresses.length) {
+ return
+ }
+
+ // Ensure preferences + identities controller know about all addresses
+ this.preferencesController.addAddresses(addresses)
+ this.accountTracker.syncWithAddresses(addresses)
+
+ const wasLocked = !isUnlocked
+ if (wasLocked) {
+ const oldSelectedAddress = this.preferencesController.getSelectedAddress()
+ if (!addresses.includes(oldSelectedAddress)) {
+ const address = addresses[0]
+ await this.preferencesController.setSelectedAddress(address)
+ }
+ }
+ }
+
+ /**
* A method for emitting the full MetaMask state to all registered listeners.
* @private
*/
@@ -1298,11 +1375,24 @@ module.exports = class MetamaskController extends EventEmitter {
})
.map(number => number.div(GWEI_BN).toNumber())
- const percentileNum = percentile(50, lowestPrices)
+ const percentileNum = percentile(65, lowestPrices)
const percentileNumBn = new BN(percentileNum)
return '0x' + percentileNumBn.mul(GWEI_BN).toString(16)
}
+ /**
+ * Returns the nonce that will be associated with a transaction once approved
+ * @param address {string} - The hex string address for the transaction
+ * @returns Promise<number>
+ */
+ async getPendingNonce (address) {
+ const { nonceDetails, releaseLock} = await this.txController.nonceTracker.getNonceLock(address)
+ const pendingNonce = nonceDetails.params.highestSuggested
+
+ releaseLock()
+ return pendingNonce
+ }
+
//=============================================================================
// CONFIG
//=============================================================================
@@ -1366,6 +1456,14 @@ module.exports = class MetamaskController extends EventEmitter {
}
/**
+ * A method for deleting a selected custom URL.
+ * @param {string} rpcTarget - A RPC URL to delete.
+ */
+ async delCustomRpc (rpcTarget) {
+ await this.preferencesController.updateFrequentRpcList(rpcTarget, true)
+ }
+
+ /**
* Sets whether or not to use the blockie identicon format.
* @param {boolean} val - True for bockie, false for jazzicon.
* @param {Function} cb - A callback function called when complete.
@@ -1407,6 +1505,7 @@ module.exports = class MetamaskController extends EventEmitter {
}
}
+ // TODO: Replace isClientOpen methods with `controllerConnectionChanged` events.
/**
* A method for recording whether the MetaMask user interface is open or not.
* @private
@@ -1427,4 +1526,42 @@ module.exports = class MetamaskController extends EventEmitter {
set isClientOpenAndUnlocked (active) {
this.tokenRatesController.isActive = active
}
+
+ /**
+ * Creates RPC engine middleware for processing eth_signTypedData requests
+ *
+ * @param {Object} req - request object
+ * @param {Object} res - response object
+ * @param {Function} - next
+ * @param {Function} - end
+ */
+ createTypedDataMiddleware (methodName, version, reverse) {
+ return async (req, res, next, end) => {
+ const { method, params } = req
+ if (method === methodName) {
+ const promise = this.typedMessageManager.addUnapprovedMessageAsync({
+ data: reverse ? params[1] : params[0],
+ from: reverse ? params[0] : params[1],
+ }, req, version)
+ this.sendUpdate()
+ this.opts.showUnconfirmedMessage()
+ try {
+ res.result = await promise
+ end()
+ } catch (error) {
+ end(error)
+ }
+ } else {
+ next()
+ }
+ }
+ }
+
+ /**
+ * Adds a domain to the {@link BlacklistController} whitelist
+ * @param {string} hostname the domain to whitelist
+ */
+ whitelistPhishingDomain (hostname) {
+ return this.blacklistController.whitelistDomain(hostname)
+ }
}
diff --git a/app/scripts/migrations/_multi-keyring.js b/app/scripts/migrations/_multi-keyring.js
deleted file mode 100644
index 7a4578ea7..000000000
--- a/app/scripts/migrations/_multi-keyring.js
+++ /dev/null
@@ -1,50 +0,0 @@
-const version = 5
-
-/*
-
-This is an incomplete migration bc it requires post-decrypted data
-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('eth-keyring-controller')
-
-const password = 'obviously not correct'
-
-module.exports = {
- version,
-
- migrate: function (versionedData) {
- versionedData.meta.version = version
-
- const store = new ObservableStore(versionedData.data)
- const configManager = new ConfigManager({ store })
- const idStoreMigrator = new IdentityStoreMigrator({ configManager })
- const keyringController = new KeyringController({
- configManager: configManager,
- })
-
- // attempt to migrate to multiVault
- return idStoreMigrator.migratedVaultForPassword(password)
- .then((result) => {
- // skip if nothing to migrate
- if (!result) return Promise.resolve(versionedData)
- delete versionedData.data.wallet
- // create new keyrings
- const privKeys = result.lostAccounts.map(acct => acct.privateKey)
- return Promise.all([
- keyringController.restoreKeyring(result.serialized),
- keyringController.restoreKeyring({ type: 'Simple Key Pair', data: privKeys }),
- ]).then(() => {
- return keyringController.persistAllKeyrings(password)
- }).then(() => {
- // copy result on to state object
- versionedData.data = store.get()
- return Promise.resolve(versionedData)
- })
- })
- },
-}
diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js
index 2def4371e..ce686d9d1 100644
--- a/app/scripts/notice-controller.js
+++ b/app/scripts/notice-controller.js
@@ -7,7 +7,7 @@ const uniqBy = require('lodash.uniqby')
module.exports = class NoticeController extends EventEmitter {
- constructor (opts) {
+ constructor (opts = {}) {
super()
this.noticePoller = null
this.firstVersion = opts.firstVersion
diff --git a/app/scripts/phishing-detect.js b/app/scripts/phishing-detect.js
new file mode 100644
index 000000000..0889c831e
--- /dev/null
+++ b/app/scripts/phishing-detect.js
@@ -0,0 +1,59 @@
+window.onload = function () {
+ if (window.location.pathname === '/phishing.html') {
+ const {hostname} = parseHash()
+ document.getElementById('esdbLink').innerHTML = '<b>To read more about this scam, navigate to: <a href="https://etherscamdb.info/domain/' + hostname + '"> https://etherscamdb.info/domain/' + hostname + '</a></b>'
+ }
+}
+
+const querystring = require('querystring')
+const dnode = require('dnode')
+const { EventEmitter } = require('events')
+const PortStream = require('extension-port-stream')
+const extension = require('extensionizer')
+const setupMultiplex = require('./lib/stream-utils.js').setupMultiplex
+const { getEnvironmentType } = require('./lib/util')
+const ExtensionPlatform = require('./platforms/extension')
+
+document.addEventListener('DOMContentLoaded', start)
+
+function start () {
+ const windowType = getEnvironmentType(window.location.href)
+
+ global.platform = new ExtensionPlatform()
+ global.METAMASK_UI_TYPE = windowType
+
+ const extensionPort = extension.runtime.connect({ name: windowType })
+ const connectionStream = new PortStream(extensionPort)
+ const mx = setupMultiplex(connectionStream)
+ setupControllerConnection(mx.createStream('controller'), (err, metaMaskController) => {
+ if (err) {
+ return
+ }
+
+ const suspect = parseHash()
+ const unsafeContinue = () => {
+ window.location.href = suspect.href
+ }
+ const continueLink = document.getElementById('unsafe-continue')
+ continueLink.addEventListener('click', () => {
+ metaMaskController.whitelistPhishingDomain(suspect.hostname)
+ unsafeContinue()
+ })
+ })
+}
+
+function setupControllerConnection (connectionStream, cb) {
+ const eventEmitter = new EventEmitter()
+ const accountManagerDnode = dnode({
+ sendUpdate (state) {
+ eventEmitter.emit('update', state)
+ },
+ })
+ connectionStream.pipe(accountManagerDnode).pipe(connectionStream)
+ accountManagerDnode.once('remote', (accountManager) => cb(null, accountManager))
+}
+
+function parseHash () {
+ const hash = window.location.hash.substring(1)
+ return querystring.parse(hash)
+}
diff --git a/app/scripts/ui.js b/app/scripts/ui.js
index da100f928..98a036338 100644
--- a/app/scripts/ui.js
+++ b/app/scripts/ui.js
@@ -2,7 +2,7 @@ const injectCss = require('inject-css')
const OldMetaMaskUiCss = require('../../old-ui/css')
const NewMetaMaskUiCss = require('../../ui/css')
const startPopup = require('./popup-core')
-const PortStream = require('./lib/port-stream.js')
+const PortStream = require('extension-port-stream')
const { getEnvironmentType } = require('./lib/util')
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('./lib/enums')
const extension = require('extensionizer')