aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts
diff options
context:
space:
mode:
authorAlexander Tseung <alextsg@users.noreply.github.com>2017-12-23 03:40:20 +0800
committerGitHub <noreply@github.com>2017-12-23 03:40:20 +0800
commit409d1d30e9d836926e5361fb9e4b5b025b66e313 (patch)
treec7f247d1e973c50697af8b9905d9fd40d4575659 /app/scripts
parentb944a63ff89e3c45f7d7e49b2d93a5442cde4462 (diff)
parent5a58add797fcdbb023678af84a61f1d2bfdafaf1 (diff)
downloadtangerine-wallet-browser-409d1d30e9d836926e5361fb9e4b5b025b66e313.tar.gz
tangerine-wallet-browser-409d1d30e9d836926e5361fb9e4b5b025b66e313.tar.zst
tangerine-wallet-browser-409d1d30e9d836926e5361fb9e4b5b025b66e313.zip
Merge pull request #2799 from MetaMask/NewUI-flat
Update UAT to version 4.0.5
Diffstat (limited to 'app/scripts')
-rw-r--r--app/scripts/background.js9
-rw-r--r--app/scripts/config.js22
-rw-r--r--app/scripts/contentscript.js12
-rw-r--r--app/scripts/controllers/network.js103
-rw-r--r--app/scripts/controllers/preferences.js34
-rw-r--r--app/scripts/controllers/recent-blocks.js44
-rw-r--r--app/scripts/controllers/transactions.js40
-rw-r--r--app/scripts/inpage.js7
-rw-r--r--app/scripts/lib/account-tracker.js2
-rw-r--r--app/scripts/lib/inpage-provider.js4
-rw-r--r--app/scripts/lib/pending-tx-tracker.js21
-rw-r--r--app/scripts/lib/tx-gas-utils.js6
-rw-r--r--app/scripts/lib/tx-state-manager.js6
-rw-r--r--app/scripts/metamask-controller.js108
-rw-r--r--app/scripts/migrations/020.js41
-rw-r--r--app/scripts/migrations/index.js1
-rw-r--r--app/scripts/notice-controller.js44
-rw-r--r--app/scripts/platforms/extension.js5
-rw-r--r--app/scripts/popup.js20
19 files changed, 446 insertions, 83 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js
index 3e560d302..da022c490 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -1,10 +1,11 @@
const urlUtil = require('url')
const endOfStream = require('end-of-stream')
-const pipe = require('pump')
+const pump = require('pump')
const log = require('loglevel')
const extension = require('extensionizer')
const LocalStorageStore = require('obs-store/lib/localStorage')
const storeTransform = require('obs-store/lib/transform')
+const asStream = require('obs-store/lib/asStream')
const ExtensionPlatform = require('./platforms/extension')
const Migrator = require('./lib/migrator/')
const migrations = require('./migrations/')
@@ -72,10 +73,10 @@ function setupController (initState) {
global.metamaskController = controller
// setup state persistence
- pipe(
- controller.store,
+ pump(
+ asStream(controller.store),
storeTransform(versionifyData),
- diskStore
+ asStream(diskStore)
)
function versionifyData (state) {
diff --git a/app/scripts/config.js b/app/scripts/config.js
index 1d4ff7c0d..74c5b576e 100644
--- a/app/scripts/config.js
+++ b/app/scripts/config.js
@@ -4,6 +4,15 @@ const KOVAN_RPC_URL = 'https://kovan.infura.io/metamask'
const RINKEBY_RPC_URL = 'https://rinkeby.infura.io/metamask'
const LOCALHOST_RPC_URL = 'http://localhost:8545'
+const MAINET_RPC_URL_BETA = 'https://mainnet.infura.io/metamask2'
+const ROPSTEN_RPC_URL_BETA = 'https://ropsten.infura.io/metamask2'
+const KOVAN_RPC_URL_BETA = 'https://kovan.infura.io/metamask2'
+const RINKEBY_RPC_URL_BETA = 'https://rinkeby.infura.io/metamask2'
+
+const DEFAULT_RPC = 'rinkeby'
+const OLD_UI_NETWORK_TYPE = 'network'
+const BETA_UI_NETWORK_TYPE = 'networkBeta'
+
global.METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
module.exports = {
@@ -14,9 +23,22 @@ module.exports = {
kovan: KOVAN_RPC_URL,
rinkeby: RINKEBY_RPC_URL,
},
+ // Used for beta UI
+ networkBeta: {
+ localhost: LOCALHOST_RPC_URL,
+ mainnet: MAINET_RPC_URL_BETA,
+ ropsten: ROPSTEN_RPC_URL_BETA,
+ kovan: KOVAN_RPC_URL_BETA,
+ rinkeby: RINKEBY_RPC_URL_BETA,
+ },
networkNames: {
3: 'Ropsten',
4: 'Rinkeby',
42: 'Kovan',
},
+ enums: {
+ DEFAULT_RPC,
+ OLD_UI_NETWORK_TYPE,
+ BETA_UI_NETWORK_TYPE,
+ },
}
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index ffbbc73cc..2ed7c87b6 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -96,7 +96,7 @@ function logStreamDisconnectWarning (remoteLabel, err) {
}
function shouldInjectWeb3 () {
- return doctypeCheck() || suffixCheck()
+ return doctypeCheck() && suffixCheck() && documentElementCheck()
}
function doctypeCheck () {
@@ -104,7 +104,7 @@ function doctypeCheck () {
if (doctype) {
return doctype.name === 'html'
} else {
- return false
+ return true
}
}
@@ -121,6 +121,14 @@ function suffixCheck () {
return true
}
+function documentElementCheck () {
+ var documentElement = document.documentElement.nodeName
+ if (documentElement) {
+ return documentElement.toLowerCase() === 'html'
+ }
+ return true
+}
+
function redirectToPhishingWarning () {
console.log('MetaMask - redirecting to phishing warning')
window.location.href = 'https://metamask.io/phishing.html'
diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js
index 23afdc12b..db1a5b374 100644
--- a/app/scripts/controllers/network.js
+++ b/app/scripts/controllers/network.js
@@ -1,18 +1,25 @@
const assert = require('assert')
const EventEmitter = require('events')
const createMetamaskProvider = require('web3-provider-engine/zero.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 RPC_ADDRESS_LIST = require('../config.js').network
-const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
+const networkConfig = require('../config.js')
+const { OLD_UI_NETWORK_TYPE, DEFAULT_RPC } = networkConfig.enums
+const INFURA_PROVIDER_TYPES = ['ropsten', 'rinkeby', 'kovan', 'mainnet']
module.exports = class NetworkController extends EventEmitter {
constructor (config) {
super()
+
+ this._networkEndpointVersion = OLD_UI_NETWORK_TYPE
+ this._networkEndpoints = this.getNetworkEndpoints(OLD_UI_NETWORK_TYPE)
+ this._defaultRpc = this._networkEndpoints[DEFAULT_RPC]
+
config.provider.rpcTarget = this.getRpcAddressForType(config.provider.type, config.provider)
this.networkStore = new ObservableStore('loading')
this.providerStore = new ObservableStore(config.provider)
@@ -22,10 +29,32 @@ module.exports = class NetworkController extends EventEmitter {
this.on('networkDidChange', this.lookupNetwork)
}
+ async setNetworkEndpoints (version) {
+ if (version === this._networkEndpointVersion) {
+ return
+ }
+
+ this._networkEndpointVersion = version
+ this._networkEndpoints = this.getNetworkEndpoints(version)
+ this._defaultRpc = this._networkEndpoints[DEFAULT_RPC]
+ const { type } = this.getProviderConfig()
+
+ return this.setProviderType(type, true)
+ }
+
+ getNetworkEndpoints (version = OLD_UI_NETWORK_TYPE) {
+ return networkConfig[version]
+ }
+
initializeProvider (_providerParams) {
this._baseProviderParams = _providerParams
- const rpcUrl = this.getCurrentRpcAddress()
- this._configureStandardProvider({ rpcUrl })
+ const { type, rpcTarget } = this.providerStore.getState()
+ // map rpcTarget to rpcUrl
+ const opts = {
+ type,
+ rpcUrl: rpcTarget,
+ }
+ this._configureProvider(opts)
this._proxy.on('block', this._logBlock.bind(this))
this._proxy.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this._proxy)
@@ -53,7 +82,7 @@ module.exports = class NetworkController extends EventEmitter {
lookupNetwork () {
// Prevent firing when provider is not defined.
if (!this.ethQuery || !this.ethQuery.sendAsync) {
- return
+ return log.warn('NetworkController - lookupNetwork aborted due to missing ethQuery')
}
this.ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading')
@@ -76,14 +105,17 @@ module.exports = class NetworkController extends EventEmitter {
return this.getRpcAddressForType(provider.type)
}
- async setProviderType (type) {
+ async setProviderType (type, forceUpdate = false) {
assert(type !== 'rpc', `NetworkController.setProviderType - cannot connect by type "rpc"`)
// skip if type already matches
- if (type === this.getProviderConfig().type) return
+ if (type === this.getProviderConfig().type && !forceUpdate) {
+ return
+ }
+
const rpcTarget = this.getRpcAddressForType(type)
assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`)
this.providerStore.updateState({ type, rpcTarget })
- this._switchNetwork({ rpcUrl: rpcTarget })
+ this._switchNetwork({ type })
}
getProviderConfig () {
@@ -91,22 +123,65 @@ module.exports = class NetworkController extends EventEmitter {
}
getRpcAddressForType (type, provider = this.getProviderConfig()) {
- if (RPC_ADDRESS_LIST[type]) return RPC_ADDRESS_LIST[type]
- return provider && provider.rpcTarget ? provider.rpcTarget : DEFAULT_RPC
+ if (this._networkEndpoints[type]) {
+ return this._networkEndpoints[type]
+ }
+
+ return provider && provider.rpcTarget ? provider.rpcTarget : this._defaultRpc
}
//
// Private
//
- _switchNetwork (providerParams) {
+ _switchNetwork (opts) {
this.setNetworkState('loading')
- this._configureStandardProvider(providerParams)
+ this._configureProvider(opts)
this.emit('networkDidChange')
}
- _configureStandardProvider (_providerParams) {
- const providerParams = extend(this._baseProviderParams, _providerParams)
+ _configureProvider (opts) {
+ // type-based rpc endpoints
+ const { type } = opts
+ if (type) {
+ // type-based infura rpc endpoints
+ const isInfura = INFURA_PROVIDER_TYPES.includes(type)
+ opts.rpcUrl = this.getRpcAddressForType(type)
+ if (isInfura) {
+ this._configureInfuraProvider(opts)
+ // other type-based rpc endpoints
+ } else {
+ this._configureStandardProvider(opts)
+ }
+ // url-based rpc endpoints
+ } else {
+ this._configureStandardProvider(opts)
+ }
+ }
+
+ _configureInfuraProvider (opts) {
+ log.info('_configureInfuraProvider', opts)
+ const blockTrackerProvider = createInfuraProvider({
+ network: opts.type,
+ })
+ const providerParams = extend(this._baseProviderParams, {
+ rpcUrl: opts.rpcUrl,
+ engineParams: {
+ pollingInterval: 8000,
+ blockTrackerProvider,
+ },
+ })
+ const provider = createMetamaskProvider(providerParams)
+ this._setProvider(provider)
+ }
+
+ _configureStandardProvider ({ rpcUrl }) {
+ const providerParams = extend(this._baseProviderParams, {
+ rpcUrl,
+ engineParams: {
+ pollingInterval: 8000,
+ },
+ })
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
}
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 10004caad..39d15fd83 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -9,11 +9,21 @@ class PreferencesController {
frequentRpcList: [],
currentAccountTab: 'history',
tokens: [],
+ useBlockie: false,
+ featureFlags: {},
}, opts.initState)
this.store = new ObservableStore(initState)
}
// PUBLIC METHODS
+ setUseBlockie (val) {
+ this.store.updateState({ useBlockie: val })
+ }
+
+ getUseBlockie () {
+ return this.store.getState().useBlockie
+ }
+
setSelectedAddress (_address) {
return new Promise((resolve, reject) => {
const address = normalizeAddress(_address)
@@ -26,22 +36,24 @@ class PreferencesController {
return this.store.getState().selectedAddress
}
- addToken (rawAddress, symbol, decimals) {
+ async addToken (rawAddress, symbol, decimals) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const tokens = this.store.getState().tokens
- const previousIndex = tokens.find((token, index) => {
+ const previousEntry = tokens.find((token, index) => {
return token.address === address
})
+ const previousIndex = tokens.indexOf(previousEntry)
- if (previousIndex) {
+ if (previousEntry) {
tokens[previousIndex] = newEntry
} else {
tokens.push(newEntry)
}
this.store.updateState({ tokens })
+
return Promise.resolve(tokens)
}
@@ -91,6 +103,22 @@ class PreferencesController {
getFrequentRpcList () {
return this.store.getState().frequentRpcList
}
+
+ setFeatureFlag (feature, activated) {
+ const currentFeatureFlags = this.store.getState().featureFlags
+ const updatedFeatureFlags = {
+ ...currentFeatureFlags,
+ [feature]: activated,
+ }
+
+ this.store.updateState({ featureFlags: updatedFeatureFlags })
+
+ return Promise.resolve(updatedFeatureFlags)
+ }
+
+ getFeatureFlags () {
+ return this.store.getState().featureFlags
+ }
//
// PRIVATE METHODS
//
diff --git a/app/scripts/controllers/recent-blocks.js b/app/scripts/controllers/recent-blocks.js
new file mode 100644
index 000000000..4a906261e
--- /dev/null
+++ b/app/scripts/controllers/recent-blocks.js
@@ -0,0 +1,44 @@
+const ObservableStore = require('obs-store')
+const extend = require('xtend')
+
+class RecentBlocksController {
+
+ constructor (opts = {}) {
+ const { blockTracker } = opts
+ this.blockTracker = blockTracker
+ this.historyLength = opts.historyLength || 40
+
+ const initState = extend({
+ recentBlocks: [],
+ }, opts.initState)
+ this.store = new ObservableStore(initState)
+
+ this.blockTracker.on('block', this.processBlock.bind(this))
+ }
+
+ resetState () {
+ this.store.updateState({
+ recentBlocks: [],
+ })
+ }
+
+ processBlock (newBlock) {
+ const block = extend(newBlock, {
+ gasPrices: newBlock.transactions.map((tx) => {
+ return tx.gasPrice
+ }),
+ })
+ delete block.transactions
+
+ const state = this.store.getState()
+ state.recentBlocks.push(block)
+
+ while (state.recentBlocks.length > this.historyLength) {
+ state.recentBlocks.shift()
+ }
+
+ this.store.updateState(state)
+ }
+}
+
+module.exports = RecentBlocksController
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index a861c0342..7f3130540 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -59,7 +59,6 @@ module.exports = class TransactionController extends EventEmitter {
this.pendingTxTracker = new PendingTransactionTracker({
provider: this.provider,
nonceTracker: this.nonceTracker,
- retryTimePeriod: 86400000, // Retry 3500 blocks, or about 1 day.
publishTransaction: (rawTx) => this.query.sendRawTransaction(rawTx),
getPendingTransactions: this.txStateManager.getPendingTransactions.bind(this.txStateManager),
getCompletedTransactions: this.txStateManager.getConfirmedTransactions.bind(this.txStateManager),
@@ -72,6 +71,12 @@ module.exports = class TransactionController extends EventEmitter {
})
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager))
+ this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
+ if (!txMeta.firstRetryBlockNumber) {
+ txMeta.firstRetryBlockNumber = latestBlockNumber
+ this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
+ }
+ })
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++
@@ -132,18 +137,20 @@ module.exports = class TransactionController extends EventEmitter {
async newUnapprovedTransaction (txParams) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
- const txMeta = await this.addUnapprovedTransaction(txParams)
- this.emit('newUnapprovedTx', txMeta)
+ const initialTxMeta = await this.addUnapprovedTransaction(txParams)
+ this.emit('newUnapprovedTx', initialTxMeta)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
- this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => {
- switch (completedTx.status) {
+ this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
+ switch (finishedTxMeta.status) {
case 'submitted':
- return resolve(completedTx.hash)
+ return resolve(finishedTxMeta.hash)
case 'rejected':
return reject(new Error('MetaMask Tx Signature: User denied transaction signature.'))
+ case 'failed':
+ return reject(new Error(finishedTxMeta.err.message))
default:
- return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`))
+ return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))
}
})
})
@@ -171,6 +178,7 @@ module.exports = class TransactionController extends EventEmitter {
const txParams = txMeta.txParams
// ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
+ txMeta.nonceSpecified = Boolean(txParams.nonce)
const gasPrice = txParams.gasPrice || await this.query.gasPrice()
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0'
@@ -178,6 +186,17 @@ module.exports = class TransactionController extends EventEmitter {
return await this.txGasUtil.analyzeGasUsage(txMeta)
}
+ async retryTransaction (txId) {
+ this.txStateManager.setTxStatusUnapproved(txId)
+ const txMeta = this.txStateManager.getTx(txId)
+ txMeta.lastGasPrice = txMeta.txParams.gasPrice
+ this.txStateManager.updateTx(txMeta, 'retryTransaction: manual retry')
+ }
+
+ async updateTransaction (txMeta) {
+ this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction')
+ }
+
async updateAndApproveTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id)
@@ -194,7 +213,12 @@ module.exports = class TransactionController extends EventEmitter {
// wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams
- txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16))
+ const nonce = txMeta.nonceSpecified ? txMeta.txParams.nonce : nonceLock.nextNonce
+ if (nonce > nonceLock.nextNonce) {
+ const message = `Specified nonce may not be larger than account's next valid nonce.`
+ throw new Error(message)
+ }
+ txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16))
// add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails
this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction')
diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js
index b6889b00f..9261e7d64 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -31,6 +31,13 @@ var inpageProvider = new MetamaskInpageProvider(metamaskStream)
// setup web3
//
+if (typeof window.web3 !== 'undefined') {
+ throw new Error(`MetaMask detected another web3.
+ MetaMask will not work reliably with another web3 extension.
+ This usually happens if you have two MetaMasks installed,
+ or MetaMask and another web3 extension. Please remove one
+ and try again.`)
+}
var web3 = new Web3(inpageProvider)
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 ce6642150..8c3dd8c71 100644
--- a/app/scripts/lib/account-tracker.js
+++ b/app/scripts/lib/account-tracker.js
@@ -117,8 +117,6 @@ class AccountTracker extends EventEmitter {
const query = this._query
async.parallel({
balance: query.getBalance.bind(query, address),
- nonce: query.getTransactionCount.bind(query, address),
- code: query.getCode.bind(query, address),
}, cb)
}
diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js
index da75c4be2..99cc5d2cf 100644
--- a/app/scripts/lib/inpage-provider.js
+++ b/app/scripts/lib/inpage-provider.js
@@ -3,6 +3,7 @@ const RpcEngine = require('json-rpc-engine')
const createIdRemapMiddleware = require('json-rpc-engine/src/idRemapMiddleware')
const createStreamMiddleware = require('json-rpc-middleware-stream')
const LocalStorageStore = require('obs-store')
+const asStream = require('obs-store/lib/asStream')
const ObjectMultiplex = require('obj-multiplex')
module.exports = MetamaskInpageProvider
@@ -21,9 +22,10 @@ function MetamaskInpageProvider (connectionStream) {
// subscribe to metamask public config (one-way)
self.publicConfigStore = new LocalStorageStore({ storageKey: 'MetaMask-Config' })
+
pump(
mux.createStream('publicConfig'),
- self.publicConfigStore,
+ asStream(self.publicConfigStore),
(err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err)
)
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index 0d7c6a92c..7956a3329 100644
--- a/app/scripts/lib/pending-tx-tracker.js
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -23,7 +23,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.query = new EthQuery(config.provider)
this.nonceTracker = config.nonceTracker
// default is one day
- this.retryTimePeriod = config.retryTimePeriod || 86400000
this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions
this.publishTransaction = config.publishTransaction
@@ -65,11 +64,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
- resubmitPendingTxs () {
+ resubmitPendingTxs (block) {
const pending = this.getPendingTransactions()
// only try resubmitting if their are transactions to resubmit
if (!pending.length) return
- pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => {
+ pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => {
/*
Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce
@@ -101,13 +100,19 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}))
}
- async _resubmitTx (txMeta) {
- if (Date.now() > txMeta.time + this.retryTimePeriod) {
- const hours = (this.retryTimePeriod / 3.6e+6).toFixed(1)
- const err = new Error(`Gave up submitting after ${hours} hours.`)
- return this.emit('tx:failed', txMeta.id, err)
+ async _resubmitTx (txMeta, latestBlockNumber) {
+ if (!txMeta.firstRetryBlockNumber) {
+ this.emit('tx:block-update', txMeta, latestBlockNumber)
}
+ const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber
+ const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16)
+
+ const retryCount = txMeta.retryCount || 0
+
+ // Exponential backoff to limit retries at publishing
+ if (txBlockDistance <= Math.pow(2, retryCount) - 1) return
+
// Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) return
diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/lib/tx-gas-utils.js
index 7e72ea71d..56bee19f7 100644
--- a/app/scripts/lib/tx-gas-utils.js
+++ b/app/scripts/lib/tx-gas-utils.js
@@ -22,7 +22,11 @@ module.exports = class txProvideUtil {
try {
estimatedGasHex = await this.estimateTxGas(txMeta, block.gasLimit)
} catch (err) {
- if (err.message.includes('Transaction execution error.')) {
+ const simulationFailed = (
+ err.message.includes('Transaction execution error.') ||
+ err.message.includes('gas required exceeds allowance or always failing transaction')
+ )
+ if ( simulationFailed ) {
txMeta.simulationFails = true
return txMeta
}
diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js
index 0fd6bed4b..a8ef39891 100644
--- a/app/scripts/lib/tx-state-manager.js
+++ b/app/scripts/lib/tx-state-manager.js
@@ -187,6 +187,10 @@ module.exports = class TransactionStateManger extends EventEmitter {
this._setTxStatus(txId, 'rejected')
}
+ // should update the status of the tx to 'unapproved'.
+ setTxStatusUnapproved (txId) {
+ this._setTxStatus(txId, 'unapproved')
+ }
// should update the status of the tx to 'approved'.
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
@@ -236,7 +240,7 @@ module.exports = class TransactionStateManger extends EventEmitter {
txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`tx:status-update`, txId, status)
- if (status === 'submitted' || status === 'rejected') {
+ if (['submitted', 'rejected', 'failed'].includes(status)) {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index bd71da8e0..b50a04703 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -3,6 +3,7 @@ const extend = require('xtend')
const pump = require('pump')
const Dnode = require('dnode')
const ObservableStore = require('obs-store')
+const asStream = require('obs-store/lib/asStream')
const AccountTracker = require('./lib/account-tracker')
const EthQuery = require('eth-query')
const RpcEngine = require('json-rpc-engine')
@@ -22,6 +23,7 @@ const ShapeShiftController = require('./controllers/shapeshift')
const AddressBookController = require('./controllers/address-book')
const InfuraController = require('./controllers/infura')
const BlacklistController = require('./controllers/blacklist')
+const RecentBlocksController = require('./controllers/recent-blocks')
const MessageManager = require('./lib/message-manager')
const PersonalMessageManager = require('./lib/personal-message-manager')
const TypedMessageManager = require('./lib/typed-message-manager')
@@ -31,6 +33,7 @@ const ConfigManager = require('./lib/config-manager')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
+const Mutex = require('await-semaphore').Mutex
const version = require('../manifest.json').version
module.exports = class MetamaskController extends EventEmitter {
@@ -38,10 +41,12 @@ module.exports = class MetamaskController extends EventEmitter {
constructor (opts) {
super()
+
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
this.opts = opts
const initState = opts.initState || {}
+ this.recordFirstTimeInfo(initState)
// platform-specific api
this.platform = opts.platform
@@ -49,6 +54,9 @@ module.exports = class MetamaskController extends EventEmitter {
// observable state store
this.store = new ObservableStore(initState)
+ // lock to ensure only one vault created at once
+ this.createVaultMutex = new Mutex()
+
// network store
this.networkController = new NetworkController(initState.NetworkController)
@@ -84,6 +92,10 @@ module.exports = class MetamaskController extends EventEmitter {
this.provider = this.initializeProvider()
this.blockTracker = this.provider._blockTracker
+ this.recentBlocksController = new RecentBlocksController({
+ blockTracker: this.blockTracker,
+ })
+
// eth data query tools
this.ethQuery = new EthQuery(this.provider)
// account tracker watches balances, nonces, and any code at their address.
@@ -144,6 +156,8 @@ module.exports = class MetamaskController extends EventEmitter {
// notices
this.noticeController = new NoticeController({
initState: initState.NoticeController,
+ version,
+ firstVersion: initState.firstTimeInfo.version,
})
this.noticeController.updateNoticesList()
// to be uncommented when retrieving notices from a remote server.
@@ -187,25 +201,30 @@ module.exports = class MetamaskController extends EventEmitter {
this.blacklistController.store.subscribe((state) => {
this.store.updateState({ BlacklistController: state })
})
+ this.recentBlocksController.store.subscribe((state) => {
+ this.store.updateState({ RecentBlocks: state })
+ })
this.infuraController.store.subscribe((state) => {
this.store.updateState({ InfuraController: state })
})
// manual mem state subscriptions
- this.networkController.store.subscribe(this.sendUpdate.bind(this))
- this.accountTracker.store.subscribe(this.sendUpdate.bind(this))
- this.txController.memStore.subscribe(this.sendUpdate.bind(this))
- this.balancesController.store.subscribe(this.sendUpdate.bind(this))
- this.messageManager.memStore.subscribe(this.sendUpdate.bind(this))
- this.personalMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
- this.typedMessageManager.memStore.subscribe(this.sendUpdate.bind(this))
- this.keyringController.memStore.subscribe(this.sendUpdate.bind(this))
- this.preferencesController.store.subscribe(this.sendUpdate.bind(this))
- this.addressBookController.store.subscribe(this.sendUpdate.bind(this))
- this.currencyController.store.subscribe(this.sendUpdate.bind(this))
- this.noticeController.memStore.subscribe(this.sendUpdate.bind(this))
- this.shapeshiftController.store.subscribe(this.sendUpdate.bind(this))
- this.infuraController.store.subscribe(this.sendUpdate.bind(this))
+ const sendUpdate = this.sendUpdate.bind(this)
+ this.networkController.store.subscribe(sendUpdate)
+ this.accountTracker.store.subscribe(sendUpdate)
+ this.txController.memStore.subscribe(sendUpdate)
+ this.balancesController.store.subscribe(sendUpdate)
+ this.messageManager.memStore.subscribe(sendUpdate)
+ this.personalMessageManager.memStore.subscribe(sendUpdate)
+ this.typedMessageManager.memStore.subscribe(sendUpdate)
+ this.keyringController.memStore.subscribe(sendUpdate)
+ this.preferencesController.store.subscribe(sendUpdate)
+ this.recentBlocksController.store.subscribe(sendUpdate)
+ this.addressBookController.store.subscribe(sendUpdate)
+ this.currencyController.store.subscribe(sendUpdate)
+ this.noticeController.memStore.subscribe(sendUpdate)
+ this.shapeshiftController.store.subscribe(sendUpdate)
+ this.infuraController.store.subscribe(sendUpdate)
}
//
@@ -289,6 +308,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.currencyController.store.getState(),
this.noticeController.memStore.getState(),
this.infuraController.store.getState(),
+ this.recentBlocksController.store.getState(),
// config manager
this.configManager.getConfig(),
this.shapeshiftController.store.getState(),
@@ -315,6 +335,7 @@ module.exports = class MetamaskController extends EventEmitter {
// etc
getState: (cb) => cb(null, this.getState()),
setCurrentCurrency: this.setCurrentCurrency.bind(this),
+ setUseBlockie: this.setUseBlockie.bind(this),
markAccountsFound: this.markAccountsFound.bind(this),
// coinbase
@@ -332,6 +353,7 @@ module.exports = class MetamaskController extends EventEmitter {
submitPassword: nodeify(keyringController.submitPassword, keyringController),
// network management
+ setNetworkEndpoints: nodeify(networkController.setNetworkEndpoints, networkController),
setProviderType: nodeify(networkController.setProviderType, networkController),
setCustomRpc: nodeify(this.setCustomRpc, this),
@@ -340,6 +362,7 @@ module.exports = class MetamaskController extends EventEmitter {
addToken: nodeify(preferencesController.addToken, preferencesController),
removeToken: nodeify(preferencesController.removeToken, preferencesController),
setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController),
+ setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController),
// AddressController
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
@@ -354,7 +377,9 @@ module.exports = class MetamaskController extends EventEmitter {
// txController
cancelTransaction: nodeify(txController.cancelTransaction, txController),
+ updateTransaction: nodeify(txController.updateTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
+ retryTransaction: nodeify(this.retryTransaction, this),
// messageManager
signMessage: nodeify(this.signMessage, this),
@@ -452,7 +477,7 @@ module.exports = class MetamaskController extends EventEmitter {
setupPublicConfig (outStream) {
pump(
- this.publicConfigStore,
+ asStream(this.publicConfigStore),
outStream,
(err) => {
if (err) log.error(err)
@@ -468,15 +493,34 @@ module.exports = class MetamaskController extends EventEmitter {
// Vault Management
//
- async createNewVaultAndKeychain (password, cb) {
- const vault = await this.keyringController.createNewVaultAndKeychain(password)
- this.selectFirstIdentity(vault)
+ async createNewVaultAndKeychain (password) {
+ const release = await this.createVaultMutex.acquire()
+ let vault
+
+ try {
+ const accounts = await this.keyringController.getAccounts()
+
+ if (accounts.length > 0) {
+ vault = await this.keyringController.fullUpdate()
+
+ } else {
+ vault = await this.keyringController.createNewVaultAndKeychain(password)
+ this.selectFirstIdentity(vault)
+ }
+ release()
+ } catch (err) {
+ release()
+ throw err
+ }
+
return vault
}
- async createNewVaultAndRestore (password, seed, cb) {
+ async createNewVaultAndRestore (password, seed) {
+ const release = await this.createVaultMutex.acquire()
const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
this.selectFirstIdentity(vault)
+ release()
return vault
}
@@ -546,6 +590,14 @@ module.exports = class MetamaskController extends EventEmitter {
//
// Identity Management
//
+ //
+
+ async retryTransaction (txId, cb) {
+ await this.txController.retryTransaction(txId)
+ const state = await this.getState()
+ return state
+ }
+
newUnsignedMessage (msgParams, cb) {
const msgId = this.messageManager.addUnapprovedMessage(msgParams)
@@ -774,4 +826,22 @@ module.exports = class MetamaskController extends EventEmitter {
return rpcTarget
}
+ setUseBlockie (val, cb) {
+ try {
+ this.preferencesController.setUseBlockie(val)
+ cb(null)
+ } catch (err) {
+ cb(err)
+ }
+ }
+
+ recordFirstTimeInfo (initState) {
+ if (!('firstTimeInfo' in initState)) {
+ initState.firstTimeInfo = {
+ version,
+ date: Date.now(),
+ }
+ }
+ }
+
}
diff --git a/app/scripts/migrations/020.js b/app/scripts/migrations/020.js
new file mode 100644
index 000000000..8159b3e70
--- /dev/null
+++ b/app/scripts/migrations/020.js
@@ -0,0 +1,41 @@
+const version = 20
+
+/*
+
+This migration ensures previous installations
+get a `firstTimeInfo` key on the metamask state,
+so that we can version notices in the future.
+
+*/
+
+const clone = require('clone')
+
+module.exports = {
+ version,
+
+ migrate: function (originalVersionedData) {
+ const versionedData = clone(originalVersionedData)
+ versionedData.meta.version = version
+ try {
+ const state = versionedData.data
+ const newState = transformState(state)
+ versionedData.data = newState
+ } catch (err) {
+ console.warn(`MetaMask Migration #${version}` + err.stack)
+ }
+ return Promise.resolve(versionedData)
+ },
+}
+
+function transformState (state) {
+ const newState = state
+ if ('metamask' in newState &&
+ !('firstTimeInfo' in newState.metamask)) {
+ newState.metamask.firstTimeInfo = {
+ version: '3.12.0',
+ date: Date.now(),
+ }
+ }
+ return newState
+}
+
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
index e9cbd7b98..9d0631042 100644
--- a/app/scripts/migrations/index.js
+++ b/app/scripts/migrations/index.js
@@ -30,4 +30,5 @@ module.exports = [
require('./017'),
require('./018'),
require('./019'),
+ require('./020'),
]
diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js
index 57aad40c5..db2b8c4f4 100644
--- a/app/scripts/notice-controller.js
+++ b/app/scripts/notice-controller.js
@@ -1,13 +1,17 @@
const EventEmitter = require('events').EventEmitter
+const semver = require('semver')
const extend = require('xtend')
const ObservableStore = require('obs-store')
const hardCodedNotices = require('../../notices/notices.json')
+const uniqBy = require('lodash.uniqby')
module.exports = class NoticeController extends EventEmitter {
constructor (opts) {
super()
this.noticePoller = null
+ this.firstVersion = opts.firstVersion
+ this.version = opts.version
const initState = extend({
noticesList: [],
}, opts.initState)
@@ -30,9 +34,9 @@ module.exports = class NoticeController extends EventEmitter {
return unreadNotices[unreadNotices.length - 1]
}
- setNoticesList (noticesList) {
+ async setNoticesList (noticesList) {
this.store.updateState({ noticesList })
- return Promise.resolve(true)
+ return true
}
markNoticeRead (noticeToMark, cb) {
@@ -50,12 +54,14 @@ module.exports = class NoticeController extends EventEmitter {
}
}
- updateNoticesList () {
- return this._retrieveNoticeData().then((newNotices) => {
- var oldNotices = this.getNoticesList()
- var combinedNotices = this._mergeNotices(oldNotices, newNotices)
- return Promise.resolve(this.setNoticesList(combinedNotices))
- })
+ async updateNoticesList () {
+ const newNotices = await this._retrieveNoticeData()
+ const oldNotices = this.getNoticesList()
+ const combinedNotices = this._mergeNotices(oldNotices, newNotices)
+ const filteredNotices = this._filterNotices(combinedNotices)
+ const result = this.setNoticesList(filteredNotices)
+ this._updateMemstore()
+ return result
}
startPolling () {
@@ -68,22 +74,30 @@ module.exports = class NoticeController extends EventEmitter {
}
_mergeNotices (oldNotices, newNotices) {
- var noticeMap = this._mapNoticeIds(oldNotices)
- newNotices.forEach((notice) => {
- if (noticeMap.indexOf(notice.id) === -1) {
- oldNotices.push(notice)
+ return uniqBy(oldNotices.concat(newNotices), 'id')
+ }
+
+ _filterNotices(notices) {
+ return notices.filter((newNotice) => {
+ if ('version' in newNotice) {
+ const satisfied = semver.satisfies(this.version, newNotice.version)
+ return satisfied
+ }
+ if ('firstVersion' in newNotice) {
+ const satisfied = semver.satisfies(this.firstVersion, newNotice.firstVersion)
+ return satisfied
}
+ return true
})
- return oldNotices
}
_mapNoticeIds (notices) {
return notices.map((notice) => notice.id)
}
- _retrieveNoticeData () {
+ async _retrieveNoticeData () {
// Placeholder for the API.
- return Promise.resolve(hardCodedNotices)
+ return hardCodedNotices
}
_updateMemstore () {
diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js
index 2f47512eb..f5cc255d1 100644
--- a/app/scripts/platforms/extension.js
+++ b/app/scripts/platforms/extension.js
@@ -17,6 +17,11 @@ class ExtensionPlatform {
return extension.runtime.getManifest().version
}
+ openExtensionInBrowser () {
+ const extensionURL = extension.runtime.getURL('home.html')
+ this.openWindow({ url: extensionURL })
+ }
+
getPlatformInfo (cb) {
try {
extension.runtime.getPlatformInfo((platform) => {
diff --git a/app/scripts/popup.js b/app/scripts/popup.js
index 5f17f0651..d0952af6a 100644
--- a/app/scripts/popup.js
+++ b/app/scripts/popup.js
@@ -1,5 +1,6 @@
const injectCss = require('inject-css')
-const MetaMaskUiCss = require('../../ui/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 isPopupOrNotification = require('./lib/is-popup-or-notification')
@@ -11,10 +12,6 @@ const notificationManager = new NotificationManager()
// create platform global
global.platform = new ExtensionPlatform()
-// inject css
-const css = MetaMaskUiCss()
-injectCss(css)
-
// identify window type (popup, notification)
const windowType = isPopupOrNotification()
global.METAMASK_UI_TYPE = windowType
@@ -28,8 +25,21 @@ const connectionStream = new PortStream(extensionPort)
const container = document.getElementById('app-content')
startPopup({ container, connectionStream }, (err, store) => {
if (err) return displayCriticalError(err)
+
+ let betaUIState = store.getState().metamask.featureFlags.betaUI
+ let css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
+ let deleteInjectedCss = injectCss(css)
+ let newBetaUIState
+
store.subscribe(() => {
const state = store.getState()
+ newBetaUIState = state.metamask.featureFlags.betaUI
+ if (newBetaUIState !== betaUIState) {
+ deleteInjectedCss()
+ betaUIState = newBetaUIState
+ css = betaUIState ? NewMetaMaskUiCss() : OldMetaMaskUiCss()
+ deleteInjectedCss = injectCss(css)
+ }
if (state.appState.shouldClose) notificationManager.closePopup()
})
})