aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/scripts')
-rw-r--r--app/scripts/background.js38
-rw-r--r--app/scripts/config.js22
-rw-r--r--app/scripts/controllers/blacklist.js1
-rw-r--r--app/scripts/controllers/network.js47
-rw-r--r--app/scripts/controllers/preferences.js37
-rw-r--r--app/scripts/controllers/recent-blocks.js80
-rw-r--r--app/scripts/controllers/transactions.js57
-rw-r--r--app/scripts/edge-encryptor.js69
-rw-r--r--app/scripts/lib/config-manager.js11
-rw-r--r--app/scripts/lib/environment-type.js10
-rw-r--r--app/scripts/lib/is-popup-or-notification.js5
-rw-r--r--app/scripts/lib/nonce-tracker.js2
-rw-r--r--app/scripts/lib/notification-manager.js2
-rw-r--r--app/scripts/lib/pending-tx-tracker.js10
-rw-r--r--app/scripts/lib/reportFailedTxToSentry.js38
-rw-r--r--app/scripts/lib/setupMetamaskMeshMetrics.js9
-rw-r--r--app/scripts/lib/setupRaven.js26
-rw-r--r--app/scripts/lib/tx-gas-utils.js59
-rw-r--r--app/scripts/lib/tx-state-manager.js13
-rw-r--r--app/scripts/metamask-controller.js100
-rw-r--r--app/scripts/migrations/021.js34
-rw-r--r--app/scripts/migrations/index.js1
-rw-r--r--app/scripts/notice-controller.js2
-rw-r--r--app/scripts/platforms/extension.js5
-rw-r--r--app/scripts/popup-core.js2
-rw-r--r--app/scripts/popup.js34
-rw-r--r--app/scripts/vendor/raven.min.js3
27 files changed, 649 insertions, 68 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js
index da022c490..601ae0372 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -13,6 +13,11 @@ const PortStream = require('./lib/port-stream.js')
const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller')
const firstTimeState = require('./first-time-state')
+const setupRaven = require('./lib/setupRaven')
+const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
+const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
+const EdgeEncryptor = require('./edge-encryptor')
+
const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
@@ -24,7 +29,18 @@ const platform = new ExtensionPlatform()
const notificationManager = new NotificationManager()
global.METAMASK_NOTIFIER = notificationManager
+// setup sentry error reporting
+const release = platform.getVersion()
+const raven = setupRaven({ release })
+
+// browser check if it is Edge - https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
+// Internet Explorer 6-11
+const isIE = !!document.documentMode
+// Edge 20+
+const isEdge = !isIE && !!window.StyleMedia
+
let popupIsOpen = false
+let openMetamaskTabsIDs = {}
// state persistence
const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
@@ -32,6 +48,9 @@ const diskStore = new LocalStorageStore({ storageKey: STORAGE_KEY })
// initialization flow
initialize().catch(log.error)
+// setup metamask mesh testing container
+setupMetamaskMeshMetrics()
+
async function initialize () {
const initState = await loadStateFromPersistence()
await setupController(initState)
@@ -69,9 +88,17 @@ function setupController (initState) {
initState,
// platform specific api
platform,
+ encryptor: isEdge ? new EdgeEncryptor() : undefined,
})
global.metamaskController = controller
+ // report failed transactions to Sentry
+ controller.txController.on(`tx:status-update`, (txId, status) => {
+ if (status !== 'failed') return
+ const txMeta = controller.txController.txStateManager.getTx(txId)
+ reportFailedTxToSentry({ raven, txMeta })
+ })
+
// setup state persistence
pump(
asStream(controller.store),
@@ -98,9 +125,15 @@ function setupController (initState) {
popupIsOpen = popupIsOpen || (remotePort.name === 'popup')
controller.setupTrustedCommunication(portStream, 'MetaMask')
// record popup as closed
+ if (remotePort.sender.url.match(/home.html$/)) {
+ openMetamaskTabsIDs[remotePort.sender.tab.id] = true
+ }
if (remotePort.name === 'popup') {
endOfStream(portStream, () => {
popupIsOpen = false
+ if (remotePort.sender.url.match(/home.html$/)) {
+ openMetamaskTabsIDs[remotePort.sender.tab.id] = false
+ }
})
}
} else {
@@ -143,7 +176,10 @@ function setupController (initState) {
// popup trigger
function triggerUi () {
- if (!popupIsOpen) notificationManager.showPopup()
+ extension.tabs.query({ active: true }, (tabs) => {
+ const currentlyActiveMetamaskTab = tabs.find(tab => openMetamaskTabsIDs[tab.id])
+ if (!popupIsOpen && !currentlyActiveMetamaskTab) notificationManager.showPopup()
+ })
}
// On first install, open a window to MetaMask website to how-it-works.
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/controllers/blacklist.js b/app/scripts/controllers/blacklist.js
index dd671943f..33c31dab9 100644
--- a/app/scripts/controllers/blacklist.js
+++ b/app/scripts/controllers/blacklist.js
@@ -57,3 +57,4 @@ class BlacklistController {
}
module.exports = BlacklistController
+
diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js
index 377ba6eca..617456cd7 100644
--- a/app/scripts/controllers/network.js
+++ b/app/scripts/controllers/network.js
@@ -1,20 +1,26 @@
const assert = require('assert')
const EventEmitter = require('events')
const createMetamaskProvider = require('web3-provider-engine/zero.js')
+const SubproviderFromProvider = require('web3-provider-engine/subproviders/web3.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)
@@ -24,6 +30,23 @@ 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 { type, rpcTarget } = this.providerStore.getState()
@@ -83,10 +106,13 @@ 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 })
@@ -98,8 +124,11 @@ 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
}
//
@@ -133,15 +162,17 @@ module.exports = class NetworkController extends EventEmitter {
_configureInfuraProvider (opts) {
log.info('_configureInfuraProvider', opts)
- const blockTrackerProvider = createInfuraProvider({
+ const infuraProvider = createInfuraProvider({
network: opts.type,
})
+ const infuraSubprovider = new SubproviderFromProvider(infuraProvider)
const providerParams = extend(this._baseProviderParams, {
rpcUrl: opts.rpcUrl,
engineParams: {
pollingInterval: 8000,
- blockTrackerProvider,
+ blockTrackerProvider: infuraProvider,
},
+ dataSubprovider: infuraSubprovider,
})
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index c42f47037..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)
@@ -43,6 +53,17 @@ class PreferencesController {
}
this.store.updateState({ tokens })
+
+ return Promise.resolve(tokens)
+ }
+
+ removeToken (rawAddress) {
+ const tokens = this.store.getState().tokens
+
+ const updatedTokens = tokens.filter(token => token.address !== rawAddress)
+
+ this.store.updateState({ tokens: updatedTokens })
+ return Promise.resolve(updatedTokens)
}
getTokens () {
@@ -82,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
index 4a906261e..4ae3810eb 100644
--- a/app/scripts/controllers/recent-blocks.js
+++ b/app/scripts/controllers/recent-blocks.js
@@ -1,11 +1,14 @@
const ObservableStore = require('obs-store')
const extend = require('xtend')
+const BN = require('ethereumjs-util').BN
+const EthQuery = require('eth-query')
class RecentBlocksController {
constructor (opts = {}) {
- const { blockTracker } = opts
+ const { blockTracker, provider } = opts
this.blockTracker = blockTracker
+ this.ethQuery = new EthQuery(provider)
this.historyLength = opts.historyLength || 40
const initState = extend({
@@ -14,6 +17,7 @@ class RecentBlocksController {
this.store = new ObservableStore(initState)
this.blockTracker.on('block', this.processBlock.bind(this))
+ this.backfill()
}
resetState () {
@@ -23,12 +27,7 @@ class RecentBlocksController {
}
processBlock (newBlock) {
- const block = extend(newBlock, {
- gasPrices: newBlock.transactions.map((tx) => {
- return tx.gasPrice
- }),
- })
- delete block.transactions
+ const block = this.mapTransactionsToPrices(newBlock)
const state = this.store.getState()
state.recentBlocks.push(block)
@@ -39,6 +38,73 @@ class RecentBlocksController {
this.store.updateState(state)
}
+
+ backfillBlock (newBlock) {
+ const block = this.mapTransactionsToPrices(newBlock)
+
+ const state = this.store.getState()
+
+ if (state.recentBlocks.length < this.historyLength) {
+ state.recentBlocks.unshift(block)
+ }
+
+ this.store.updateState(state)
+ }
+
+ mapTransactionsToPrices (newBlock) {
+ const block = extend(newBlock, {
+ gasPrices: newBlock.transactions.map((tx) => {
+ return tx.gasPrice
+ }),
+ })
+ delete block.transactions
+ return block
+ }
+
+ async backfill() {
+ this.blockTracker.once('block', async (block) => {
+ let blockNum = block.number
+ let recentBlocks
+ let state = this.store.getState()
+ recentBlocks = state.recentBlocks
+
+ while (recentBlocks.length < this.historyLength) {
+ try {
+ let blockNumBn = new BN(blockNum.substr(2), 16)
+ const newNum = blockNumBn.subn(1).toString(10)
+ const newBlock = await this.getBlockByNumber(newNum)
+
+ if (newBlock) {
+ this.backfillBlock(newBlock)
+ blockNum = newBlock.number
+ }
+
+ state = this.store.getState()
+ recentBlocks = state.recentBlocks
+ } catch (e) {
+ log.error(e)
+ }
+ await this.wait()
+ }
+ })
+ }
+
+ async wait () {
+ return new Promise((resolve) => {
+ setTimeout(resolve, 100)
+ })
+ }
+
+ 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)
+ })
+ })
+ }
+
}
module.exports = RecentBlocksController
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index f95b5e39a..9c2ca0dc8 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -3,7 +3,7 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const Transaction = require('ethereumjs-tx')
const EthQuery = require('ethjs-query')
-const TransactionStateManger = require('../lib/tx-state-manager')
+const TransactionStateManager = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id')
@@ -32,16 +32,39 @@ module.exports = class TransactionController extends EventEmitter {
this.provider = opts.provider
this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction
+ this.getGasPrice = opts.getGasPrice
this.memStore = new ObservableStore({})
this.query = new EthQuery(this.provider)
this.txGasUtil = new TxGasUtil(this.provider)
- this.txStateManager = new TransactionStateManger({
+ this.txStateManager = new TransactionStateManager({
initState: opts.initState,
txHistoryLimit: opts.txHistoryLimit,
getNetwork: this.getNetwork.bind(this),
})
+
+ this.txStateManager.getFilteredTxList({
+ status: 'unapproved',
+ loadingDefaults: true,
+ }).forEach((tx) => {
+ this.addTxDefaults(tx)
+ .then((txMeta) => {
+ txMeta.loadingDefaults = false
+ this.txStateManager.updateTx(txMeta, 'transactions: gas estimation for tx on boot')
+ }).catch((error) => {
+ this.txStateManager.setTxStatusFailed(tx.id, error)
+ })
+ })
+
+ this.txStateManager.getFilteredTxList({
+ status: 'approved',
+ }).forEach((txMeta) => {
+ const txSignError = new Error('Transaction found as "approved" during boot - possibly stuck during signing')
+ this.txStateManager.setTxStatusFailed(txMeta.id, txSignError)
+ })
+
+
this.store = this.txStateManager.store
this.txStateManager.on('tx:status-update', this.emit.bind(this, 'tx:status-update'))
this.nonceTracker = new NonceTracker({
@@ -59,7 +82,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),
@@ -130,6 +152,10 @@ module.exports = class TransactionController extends EventEmitter {
}
}
+ wipeTransactions (address) {
+ this.txStateManager.wipeTransactions(address)
+ }
+
// Adds a tx to the txlist
addTx (txMeta) {
this.txStateManager.addTx(txMeta)
@@ -139,7 +165,6 @@ module.exports = class TransactionController extends EventEmitter {
async newUnapprovedTransaction (txParams) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
const initialTxMeta = await this.addUnapprovedTransaction(txParams)
- this.emit('newUnapprovedTx', initialTxMeta)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
@@ -167,11 +192,22 @@ module.exports = class TransactionController extends EventEmitter {
status: 'unapproved',
metamaskNetworkId: this.getNetwork(),
txParams: txParams,
+ loadingDefaults: true,
}
+ this.addTx(txMeta)
+ this.emit('newUnapprovedTx', txMeta)
// add default tx params
- await this.addTxDefaults(txMeta)
+ try {
+ await this.addTxDefaults(txMeta)
+ } catch (error) {
+ console.log(error)
+ this.txStateManager.setTxStatusFailed(txMeta.id, error)
+ throw error
+ }
+ txMeta.loadingDefaults = false
// save txMeta
- this.addTx(txMeta)
+ this.txStateManager.updateTx(txMeta)
+
return txMeta
}
@@ -180,7 +216,10 @@ module.exports = class TransactionController extends EventEmitter {
// ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
txMeta.nonceSpecified = Boolean(txParams.nonce)
- const gasPrice = txParams.gasPrice || await this.query.gasPrice()
+ let gasPrice = txParams.gasPrice
+ if (!gasPrice) {
+ gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
+ }
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0'
// set gasLimit
@@ -194,6 +233,10 @@ module.exports = class TransactionController extends EventEmitter {
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)
diff --git a/app/scripts/edge-encryptor.js b/app/scripts/edge-encryptor.js
new file mode 100644
index 000000000..24c0c93a8
--- /dev/null
+++ b/app/scripts/edge-encryptor.js
@@ -0,0 +1,69 @@
+const asmcrypto = require('asmcrypto.js')
+const Unibabel = require('browserify-unibabel')
+
+class EdgeEncryptor {
+
+ encrypt (password, dataObject) {
+
+ var salt = this._generateSalt()
+ return this._keyFromPassword(password, salt)
+ .then(function (key) {
+
+ var data = JSON.stringify(dataObject)
+ var dataBuffer = Unibabel.utf8ToBuffer(data)
+ var vector = global.crypto.getRandomValues(new Uint8Array(16))
+ var resultbuffer = asmcrypto.AES_GCM.encrypt(dataBuffer, key, vector)
+
+ var buffer = new Uint8Array(resultbuffer)
+ var vectorStr = Unibabel.bufferToBase64(vector)
+ var vaultStr = Unibabel.bufferToBase64(buffer)
+ return JSON.stringify({
+ data: vaultStr,
+ iv: vectorStr,
+ salt: salt,
+ })
+ })
+ }
+
+ decrypt (password, text) {
+
+ const payload = JSON.parse(text)
+ const salt = payload.salt
+ return this._keyFromPassword(password, salt)
+ .then(function (key) {
+ const encryptedData = Unibabel.base64ToBuffer(payload.data)
+ const vector = Unibabel.base64ToBuffer(payload.iv)
+ return new Promise((resolve, reject) => {
+ var result
+ try {
+ result = asmcrypto.AES_GCM.decrypt(encryptedData, key, vector)
+ } catch (err) {
+ return reject(new Error('Incorrect password'))
+ }
+ const decryptedData = new Uint8Array(result)
+ const decryptedStr = Unibabel.bufferToUtf8(decryptedData)
+ const decryptedObj = JSON.parse(decryptedStr)
+ resolve(decryptedObj)
+ })
+ })
+ }
+
+ _keyFromPassword (password, salt) {
+
+ var passBuffer = Unibabel.utf8ToBuffer(password)
+ var saltBuffer = Unibabel.base64ToBuffer(salt)
+ return new Promise((resolve) => {
+ var key = asmcrypto.PBKDF2_HMAC_SHA256.bytes(passBuffer, saltBuffer, 10000)
+ resolve(key)
+ })
+ }
+
+ _generateSalt (byteCount = 32) {
+ var view = new Uint8Array(byteCount)
+ global.crypto.getRandomValues(view)
+ var b64encoded = btoa(String.fromCharCode.apply(null, view))
+ return b64encoded
+ }
+}
+
+module.exports = EdgeEncryptor
diff --git a/app/scripts/lib/config-manager.js b/app/scripts/lib/config-manager.js
index 9c0dffe9c..34b603b96 100644
--- a/app/scripts/lib/config-manager.js
+++ b/app/scripts/lib/config-manager.js
@@ -42,6 +42,17 @@ 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
diff --git a/app/scripts/lib/environment-type.js b/app/scripts/lib/environment-type.js
new file mode 100644
index 000000000..7966926eb
--- /dev/null
+++ b/app/scripts/lib/environment-type.js
@@ -0,0 +1,10 @@
+module.exports = function environmentType () {
+ const url = window.location.href
+ if (url.match(/popup.html$/)) {
+ return 'popup'
+ } else if (url.match(/home.html$/)) {
+ return 'responsive'
+ } else {
+ return 'notification'
+ }
+}
diff --git a/app/scripts/lib/is-popup-or-notification.js b/app/scripts/lib/is-popup-or-notification.js
index 693fa8751..e2999411f 100644
--- a/app/scripts/lib/is-popup-or-notification.js
+++ b/app/scripts/lib/is-popup-or-notification.js
@@ -1,6 +1,9 @@
module.exports = function isPopupOrNotification () {
const url = window.location.href
- if (url.match(/popup.html$/)) {
+ // if (url.match(/popup.html$/) || url.match(/home.html$/)) {
+ // Below regexes needed for feature toggles (e.g. see line ~340 in ui/app/app.js)
+ // Revert below regexes to above commented out regexes before merge to master
+ if (url.match(/popup.html(?:\?.+)*$/) || url.match(/home.html(?:\?.+)*$/)) {
return 'popup'
} else {
return 'notification'
diff --git a/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js
index 0029ac953..ed9dd3f11 100644
--- a/app/scripts/lib/nonce-tracker.js
+++ b/app/scripts/lib/nonce-tracker.js
@@ -56,7 +56,7 @@ class NonceTracker {
const blockTracker = this._getBlockTracker()
const currentBlock = blockTracker.getCurrentBlock()
if (currentBlock) return currentBlock
- return await Promise((reject, resolve) => {
+ return await new Promise((reject, resolve) => {
blockTracker.once('latest', resolve)
})
}
diff --git a/app/scripts/lib/notification-manager.js b/app/scripts/lib/notification-manager.js
index 7846ef7f0..adaf60c65 100644
--- a/app/scripts/lib/notification-manager.js
+++ b/app/scripts/lib/notification-manager.js
@@ -1,5 +1,5 @@
const extension = require('extensionizer')
-const height = 520
+const height = 620
const width = 360
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index dc6e526fd..e8869e6b8 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
@@ -106,12 +105,6 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
this.emit('tx:block-update', txMeta, latestBlockNumber)
}
- 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)
- }
-
const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber
const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16)
@@ -185,7 +178,8 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
async _checkIfNonceIsTaken (txMeta) {
- const completed = this.getCompletedTransactions()
+ const address = txMeta.txParams.from
+ const completed = this.getCompletedTransactions(address)
const sameNonce = completed.filter((otherMeta) => {
return otherMeta.txParams.nonce === txMeta.txParams.nonce
})
diff --git a/app/scripts/lib/reportFailedTxToSentry.js b/app/scripts/lib/reportFailedTxToSentry.js
new file mode 100644
index 000000000..ee73f6845
--- /dev/null
+++ b/app/scripts/lib/reportFailedTxToSentry.js
@@ -0,0 +1,38 @@
+const ethJsRpcSlug = 'Error: [ethjs-rpc] rpc error with payload '
+const errorLabelPrefix = 'Error: '
+
+module.exports = reportFailedTxToSentry
+
+//
+// utility for formatting failed transaction messages
+// for sending to sentry
+//
+
+function reportFailedTxToSentry({ raven, txMeta }) {
+ const errorMessage = extractErrorMessage(txMeta.err.message)
+ raven.captureMessage(errorMessage, {
+ // "extra" key is required by Sentry
+ extra: txMeta,
+ })
+}
+
+//
+// ethjs-rpc provides overly verbose error messages
+// if we detect this type of message, we extract the important part
+// Below is an example input and output
+//
+// Error: [ethjs-rpc] rpc error with payload {"id":3947817945380,"jsonrpc":"2.0","params":["0xf8eb8208708477359400830398539406012c8cf97bead5deae237070f9587f8e7a266d80b8843d7d3f5a0000000000000000000000000000000000000000000000000000000000081d1a000000000000000000000000000000000000000000000000001ff973cafa800000000000000000000000000000000000000000000000000000038d7ea4c68000000000000000000000000000000000000000000000000000000000000003f48025a04c32a9b630e0d9e7ff361562d850c86b7a884908135956a7e4a336fa0300d19ca06830776423f25218e8d19b267161db526e66895567147015b1f3fc47aef9a3c7"],"method":"eth_sendRawTransaction"} Error: replacement transaction underpriced
+//
+// Transaction Failed: replacement transaction underpriced
+//
+
+function extractErrorMessage(errorMessage) {
+ const isEthjsRpcError = errorMessage.includes(ethJsRpcSlug)
+ if (isEthjsRpcError) {
+ const payloadAndError = errorMessage.slice(ethJsRpcSlug.length)
+ const originalError = payloadAndError.slice(payloadAndError.indexOf(errorLabelPrefix) + errorLabelPrefix.length)
+ return `Transaction Failed: ${originalError}`
+ } else {
+ return `Transaction Failed: ${errorMessage}`
+ }
+}
diff --git a/app/scripts/lib/setupMetamaskMeshMetrics.js b/app/scripts/lib/setupMetamaskMeshMetrics.js
new file mode 100644
index 000000000..40343f017
--- /dev/null
+++ b/app/scripts/lib/setupMetamaskMeshMetrics.js
@@ -0,0 +1,9 @@
+
+module.exports = setupMetamaskMeshMetrics
+
+function setupMetamaskMeshMetrics() {
+ const testingContainer = document.createElement('iframe')
+ testingContainer.src = 'https://metamask.github.io/mesh-testing/'
+ console.log('Injecting MetaMask Mesh testing client')
+ document.head.appendChild(testingContainer)
+}
diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupRaven.js
new file mode 100644
index 000000000..42e48cb90
--- /dev/null
+++ b/app/scripts/lib/setupRaven.js
@@ -0,0 +1,26 @@
+const Raven = require('../vendor/raven.min.js')
+const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
+const PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
+const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
+
+module.exports = setupRaven
+
+// Setup raven / sentry remote error reporting
+function setupRaven(opts) {
+ const { release } = opts
+ let ravenTarget
+
+ if (METAMASK_DEBUG) {
+ console.log('Setting up Sentry Remote Error Reporting: DEV')
+ ravenTarget = DEV
+ } else {
+ console.log('Setting up Sentry Remote Error Reporting: PROD')
+ ravenTarget = PROD
+ }
+
+ Raven.config(ravenTarget, {
+ release,
+ }).install()
+
+ return Raven
+}
diff --git a/app/scripts/lib/tx-gas-utils.js b/app/scripts/lib/tx-gas-utils.js
index 56bee19f7..6f6ff7852 100644
--- a/app/scripts/lib/tx-gas-utils.js
+++ b/app/scripts/lib/tx-gas-utils.js
@@ -4,6 +4,8 @@ const {
BnMultiplyByFraction,
bnToHex,
} = require('./util')
+const addHexPrefix = require('ethereumjs-util').addHexPrefix
+const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
/*
tx-utils are utility methods for Transaction manager
@@ -11,7 +13,8 @@ its passed ethquery
and used to do things like calculate gas of a tx.
*/
-module.exports = class txProvideUtil {
+module.exports = class TxGasUtil {
+
constructor (provider) {
this.query = new EthQuery(provider)
}
@@ -26,7 +29,7 @@ module.exports = class txProvideUtil {
err.message.includes('Transaction execution error.') ||
err.message.includes('gas required exceeds allowance or always failing transaction')
)
- if ( simulationFailed ) {
+ if (simulationFailed) {
txMeta.simulationFails = true
return txMeta
}
@@ -37,25 +40,41 @@ module.exports = class txProvideUtil {
async estimateTxGas (txMeta, blockGasLimitHex) {
const txParams = txMeta.txParams
+
// check if gasLimit is already specified
txMeta.gasLimitSpecified = Boolean(txParams.gas)
- // if not, fallback to block gasLimit
- if (!txMeta.gasLimitSpecified) {
- const blockGasLimitBN = hexToBn(blockGasLimitHex)
- const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
- txParams.gas = bnToHex(saferGasLimitBN)
+
+ // if it is, use that value
+ if (txMeta.gasLimitSpecified) {
+ return txParams.gas
}
+
+ // if recipient has no code, gas is 21k max:
+ const recipient = txParams.to
+ const hasRecipient = Boolean(recipient)
+ const code = await this.query.getCode(recipient)
+ if (hasRecipient && (!code || code === '0x')) {
+ txParams.gas = SIMPLE_GAS_COST
+ txMeta.simpleSend = true // Prevents buffer addition
+ return SIMPLE_GAS_COST
+ }
+
+ // if not, fall back to block gasLimit
+ const blockGasLimitBN = hexToBn(blockGasLimitHex)
+ const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
+ txParams.gas = bnToHex(saferGasLimitBN)
+
// run tx
return await this.query.estimateGas(txParams)
}
setTxGas (txMeta, blockGasLimitHex, estimatedGasHex) {
- txMeta.estimatedGas = estimatedGasHex
+ txMeta.estimatedGas = addHexPrefix(estimatedGasHex)
const txParams = txMeta.txParams
// if gasLimit was specified and doesnt OOG,
// use original specified amount
- if (txMeta.gasLimitSpecified) {
+ if (txMeta.gasLimitSpecified || txMeta.simpleSend) {
txMeta.estimatedGas = txParams.gas
return
}
@@ -81,8 +100,26 @@ module.exports = class txProvideUtil {
}
async validateTxParams (txParams) {
- if (('value' in txParams) && txParams.value.indexOf('-') === 0) {
- throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
+ this.validateRecipient(txParams)
+ if ('value' in txParams) {
+ const value = txParams.value.toString()
+ if (value.includes('-')) {
+ throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`)
+ }
+
+ if (value.includes('.')) {
+ throw new Error(`Invalid transaction value of ${txParams.value} number must be in wei`)
+ }
+ }
+ }
+ validateRecipient (txParams) {
+ if (txParams.to === '0x') {
+ if (txParams.data) {
+ delete txParams.to
+ } else {
+ throw new Error('Invalid recipient address')
+ }
}
+ return txParams
}
}
diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js
index a8ef39891..2eb006380 100644
--- a/app/scripts/lib/tx-state-manager.js
+++ b/app/scripts/lib/tx-state-manager.js
@@ -4,7 +4,7 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper')
-module.exports = class TransactionStateManger extends EventEmitter {
+module.exports = class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
super()
@@ -221,6 +221,17 @@ module.exports = class TransactionStateManger extends EventEmitter {
this._setTxStatus(txId, 'failed')
}
+ wipeTransactions (address) {
+ // network only tx
+ const txs = this.getFullTxList()
+ const network = this.getNetwork()
+
+ // Filter out the ones from the current account and network
+ const otherAccountTxs = txs.filter((txMeta) => !(txMeta.txParams.from === address && txMeta.metamaskNetworkId === network))
+
+ // Update state
+ this._saveTxList(otherAccountTxs)
+ }
//
// PRIVATE METHODS
//
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 23f2a1598..ad4e71792 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -5,7 +5,6 @@ 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')
const debounce = require('debounce')
const createEngineStream = require('json-rpc-middleware-stream/engineStream')
@@ -35,12 +34,16 @@ 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
+const BN = require('ethereumjs-util').BN
+const GWEI_BN = new BN('1000000000')
+const percentile = require('percentile')
module.exports = class MetamaskController extends EventEmitter {
constructor (opts) {
super()
+ this.defaultMaxListeners = 20
this.sendUpdate = debounce(this.privateSendUpdate.bind(this), 200)
@@ -83,9 +86,7 @@ module.exports = class MetamaskController extends EventEmitter {
})
this.infuraController.scheduleInfuraNetworkCheck()
- this.blacklistController = new BlacklistController({
- initState: initState.BlacklistController,
- })
+ this.blacklistController = new BlacklistController()
this.blacklistController.scheduleUpdates()
// rpc provider
@@ -94,10 +95,9 @@ module.exports = class MetamaskController extends EventEmitter {
this.recentBlocksController = new RecentBlocksController({
blockTracker: this.blockTracker,
+ provider: this.provider,
})
- // eth data query tools
- this.ethQuery = new EthQuery(this.provider)
// account tracker watches balances, nonces, and any code at their address.
this.accountTracker = new AccountTracker({
provider: this.provider,
@@ -138,7 +138,7 @@ module.exports = class MetamaskController extends EventEmitter {
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
provider: this.provider,
blockTracker: this.blockTracker,
- ethQuery: this.ethQuery,
+ getGasPrice: this.getGasPrice.bind(this),
})
this.txController.on('newUnapprovedTx', opts.showUnapprovedTx.bind(opts))
@@ -198,12 +198,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.networkController.store.subscribe((state) => {
this.store.updateState({ NetworkController: state })
})
- 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 })
})
@@ -315,6 +310,7 @@ module.exports = class MetamaskController extends EventEmitter {
{
lostAccounts: this.configManager.getLostAccounts(),
seedWords: this.configManager.getSeedWords(),
+ forgottenPassword: this.configManager.getPasswordForgotten(),
}
)
}
@@ -335,7 +331,10 @@ 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),
+ markPasswordForgotten: this.markPasswordForgotten.bind(this),
+ unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this),
// coinbase
buyEth: this.buyEth.bind(this),
@@ -346,19 +345,23 @@ module.exports = class MetamaskController extends EventEmitter {
addNewAccount: nodeify(this.addNewAccount, this),
placeSeedWords: this.placeSeedWords.bind(this),
clearSeedWordCache: this.clearSeedWordCache.bind(this),
+ resetAccount: this.resetAccount.bind(this),
importAccountWithStrategy: this.importAccountWithStrategy.bind(this),
// vault management
submitPassword: nodeify(keyringController.submitPassword, keyringController),
// network management
+ setNetworkEndpoints: nodeify(networkController.setNetworkEndpoints, networkController),
setProviderType: nodeify(networkController.setProviderType, networkController),
setCustomRpc: nodeify(this.setCustomRpc, this),
// PreferencesController
setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController),
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),
@@ -373,6 +376,7 @@ 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),
@@ -448,7 +452,7 @@ module.exports = class MetamaskController extends EventEmitter {
// create filter polyfill middleware
const filterMiddleware = createFilterMiddleware({
provider: this.provider,
- blockTracker: this.blockTracker,
+ blockTracker: this.provider._blockTracker,
})
engine.push(createOriginMiddleware({ origin }))
@@ -484,6 +488,33 @@ module.exports = class MetamaskController extends EventEmitter {
this.emit('update', this.getState())
}
+ getGasPrice () {
+ const { recentBlocksController } = this
+ const { recentBlocks } = recentBlocksController.store.getState()
+
+ // Return 1 gwei if no blocks have been observed:
+ if (recentBlocks.length === 0) {
+ return '0x' + GWEI_BN.toString(16)
+ }
+
+ const lowestPrices = recentBlocks.map((block) => {
+ if (!block.gasPrices || block.gasPrices.length < 1) {
+ return GWEI_BN
+ }
+ return block.gasPrices
+ .map(hexPrefix => hexPrefix.substr(2))
+ .map(hex => new BN(hex, 16))
+ .sort((a, b) => {
+ return a.gt(b) ? 1 : -1
+ })[0]
+ })
+ .map(number => number.div(GWEI_BN).toNumber())
+
+ const percentileNum = percentile(50, lowestPrices)
+ const percentileNumBn = new BN(percentileNum)
+ return '0x' + percentileNumBn.mul(GWEI_BN).toString(16)
+ }
+
//
// Vault Management
//
@@ -513,10 +544,15 @@ module.exports = class MetamaskController extends EventEmitter {
async createNewVaultAndRestore (password, seed) {
const release = await this.createVaultMutex.acquire()
- const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
- this.selectFirstIdentity(vault)
- release()
- return vault
+ try {
+ const vault = await this.keyringController.createNewVaultAndRestore(password, seed)
+ this.selectFirstIdentity(vault)
+ release()
+ return vault
+ } catch (err) {
+ release()
+ throw err
+ }
}
selectFirstIdentity (vault) {
@@ -570,6 +606,13 @@ module.exports = class MetamaskController extends EventEmitter {
cb(null, this.preferencesController.getSelectedAddress())
}
+ resetAccount (cb) {
+ const selectedAddress = this.preferencesController.getSelectedAddress()
+ this.txController.wipeTransactions(selectedAddress)
+ cb(null, selectedAddress)
+ }
+
+
importAccountWithStrategy (strategy, args, cb) {
accountImporter.importAccount(strategy, args)
.then((privateKey) => {
@@ -754,6 +797,18 @@ module.exports = class MetamaskController extends EventEmitter {
cb(null, this.getState())
}
+ markPasswordForgotten(cb) {
+ this.configManager.setPasswordForgotten(true)
+ this.sendUpdate()
+ cb()
+ }
+
+ unMarkPasswordForgotten(cb) {
+ this.configManager.setPasswordForgotten(false)
+ this.sendUpdate()
+ cb()
+ }
+
restoreOldVaultAccounts (migratorOutput) {
const { serialized } = migratorOutput
return this.keyringController.restoreKeyring(serialized)
@@ -821,6 +876,15 @@ 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 = {
diff --git a/app/scripts/migrations/021.js b/app/scripts/migrations/021.js
new file mode 100644
index 000000000..d84e77b50
--- /dev/null
+++ b/app/scripts/migrations/021.js
@@ -0,0 +1,34 @@
+const version = 21
+
+/*
+
+This migration removes the BlackListController from disk state
+
+*/
+
+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
+ delete newState.BlacklistController
+ delete newState.RecentBlocks
+ return newState
+}
+
diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js
index 9d0631042..a0cf5f4d4 100644
--- a/app/scripts/migrations/index.js
+++ b/app/scripts/migrations/index.js
@@ -31,4 +31,5 @@ module.exports = [
require('./018'),
require('./019'),
require('./020'),
+ require('./021'),
]
diff --git a/app/scripts/notice-controller.js b/app/scripts/notice-controller.js
index db2b8c4f4..14a63eae7 100644
--- a/app/scripts/notice-controller.js
+++ b/app/scripts/notice-controller.js
@@ -77,7 +77,7 @@ module.exports = class NoticeController extends EventEmitter {
return uniqBy(oldNotices.concat(newNotices), 'id')
}
- _filterNotices(notices) {
+ _filterNotices (notices) {
return notices.filter((newNotice) => {
if ('version' in newNotice) {
const satisfied = semver.satisfies(this.version, newNotice.version)
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-core.js b/app/scripts/popup-core.js
index f1eb394d7..2e4334bb1 100644
--- a/app/scripts/popup-core.js
+++ b/app/scripts/popup-core.js
@@ -1,6 +1,7 @@
const EventEmitter = require('events').EventEmitter
const async = require('async')
const Dnode = require('dnode')
+const Eth = require('ethjs')
const EthQuery = require('eth-query')
const launchMetamaskUi = require('../../ui')
const StreamProvider = require('web3-stream-provider')
@@ -34,6 +35,7 @@ function setupWeb3Connection (connectionStream) {
providerStream.on('error', console.error.bind(console))
global.ethereumProvider = providerStream
global.ethQuery = new EthQuery(providerStream)
+ global.eth = new Eth(providerStream)
}
function setupControllerConnection (connectionStream, cb) {
diff --git a/app/scripts/popup.js b/app/scripts/popup.js
index 5f17f0651..11d50ee87 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')
@@ -7,13 +8,18 @@ const extension = require('extensionizer')
const ExtensionPlatform = require('./platforms/extension')
const NotificationManager = require('./lib/notification-manager')
const notificationManager = new NotificationManager()
+const setupRaven = require('./lib/setupRaven')
// create platform global
global.platform = new ExtensionPlatform()
+// setup sentry error reporting
+const release = global.platform.getVersion()
+setupRaven({ release })
+
// inject css
-const css = MetaMaskUiCss()
-injectCss(css)
+// const css = MetaMaskUiCss()
+// injectCss(css)
// identify window type (popup, notification)
const windowType = isPopupOrNotification()
@@ -28,8 +34,30 @@ const connectionStream = new PortStream(extensionPort)
const container = document.getElementById('app-content')
startPopup({ container, connectionStream }, (err, store) => {
if (err) return displayCriticalError(err)
+
+ // Code commented out until we begin auto adding users to NewUI
+ // const { isMascara, identities = {}, featureFlags = {} } = store.getState().metamask
+ // const firstTime = Object.keys(identities).length === 0
+ const { isMascara, featureFlags = {} } = store.getState().metamask
+ let betaUIState = featureFlags.betaUI
+
+ // Code commented out until we begin auto adding users to NewUI
+ // const useBetaCss = isMascara || firstTime || betaUIState
+ const useBetaCss = isMascara || betaUIState
+
+ let css = useBetaCss ? 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()
})
})
diff --git a/app/scripts/vendor/raven.min.js b/app/scripts/vendor/raven.min.js
new file mode 100644
index 000000000..b439aeae6
--- /dev/null
+++ b/app/scripts/vendor/raven.min.js
@@ -0,0 +1,3 @@
+/*! Raven.js 3.22.1 (7584197) | github.com/getsentry/raven-js */
+!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.Raven=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){function d(a){this.name="RavenConfigError",this.message=a}d.prototype=new Error,d.prototype.constructor=d,b.exports=d},{}],2:[function(a,b,c){var d=function(a,b,c){var d=a[b],e=a;if(b in a){var f="warn"===b?"warning":b;a[b]=function(){var a=[].slice.call(arguments),g=""+a.join(" "),h={level:f,logger:"console",extra:{arguments:a}};"assert"===b?a[0]===!1&&(g="Assertion failed: "+(a.slice(1).join(" ")||"console.assert"),h.extra.arguments=a.slice(1),c&&c(g,h)):c&&c(g,h),d&&Function.prototype.apply.call(d,e,a)}}};b.exports={wrapMethod:d}},{}],3:[function(a,b,c){(function(c){function d(){return+new Date}function e(a,b){return o(b)?function(c){return b(c,a)}:b}function f(){this.a=!("object"!=typeof JSON||!JSON.stringify),this.b=!n(K),this.c=!n(L),this.d=null,this.e=null,this.f=null,this.g=null,this.h=null,this.i=null,this.j={},this.k={release:J.SENTRY_RELEASE&&J.SENTRY_RELEASE.id,logger:"javascript",ignoreErrors:[],ignoreUrls:[],whitelistUrls:[],includePaths:[],headers:null,collectWindowErrors:!0,maxMessageLength:0,maxUrlLength:250,stackTraceLimit:50,autoBreadcrumbs:!0,instrument:!0,sampleRate:1},this.l={method:"POST",keepalive:!0,referrerPolicy:"origin"},this.m=0,this.n=!1,this.o=Error.stackTraceLimit,this.p=J.console||{},this.q={},this.r=[],this.s=d(),this.t=[],this.u=[],this.v=null,this.w=J.location,this.x=this.w&&this.w.href,this.y();for(var a in this.p)this.q[a]=this.p[a]}var g=a(6),h=a(7),i=a(1),j=a(5),k=j.isError,l=j.isObject,m=j.isErrorEvent,n=j.isUndefined,o=j.isFunction,p=j.isString,q=j.isArray,r=j.isEmptyObject,s=j.each,t=j.objectMerge,u=j.truncate,v=j.objectFrozen,w=j.hasKey,x=j.joinRegExp,y=j.urlencode,z=j.uuid4,A=j.htmlTreeAsString,B=j.isSameException,C=j.isSameStacktrace,D=j.parseUrl,E=j.fill,F=j.supportsFetch,G=a(2).wrapMethod,H="source protocol user pass host port path".split(" "),I=/^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/,J="undefined"!=typeof window?window:"undefined"!=typeof c?c:"undefined"!=typeof self?self:{},K=J.document,L=J.navigator;f.prototype={VERSION:"3.22.1",debug:!1,TraceKit:g,config:function(a,b){var c=this;if(c.g)return this.z("error","Error: Raven has already been configured"),c;if(!a)return c;var d=c.k;b&&s(b,function(a,b){"tags"===a||"extra"===a||"user"===a?c.j[a]=b:d[a]=b}),c.setDSN(a),d.ignoreErrors.push(/^Script error\.?$/),d.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/),d.ignoreErrors=x(d.ignoreErrors),d.ignoreUrls=!!d.ignoreUrls.length&&x(d.ignoreUrls),d.whitelistUrls=!!d.whitelistUrls.length&&x(d.whitelistUrls),d.includePaths=x(d.includePaths),d.maxBreadcrumbs=Math.max(0,Math.min(d.maxBreadcrumbs||100,100));var e={xhr:!0,console:!0,dom:!0,location:!0,sentry:!0},f=d.autoBreadcrumbs;"[object Object]"==={}.toString.call(f)?f=t(e,f):f!==!1&&(f=e),d.autoBreadcrumbs=f;var h={tryCatch:!0},i=d.instrument;return"[object Object]"==={}.toString.call(i)?i=t(h,i):i!==!1&&(i=h),d.instrument=i,g.collectWindowErrors=!!d.collectWindowErrors,c},install:function(){var a=this;return a.isSetup()&&!a.n&&(g.report.subscribe(function(){a.A.apply(a,arguments)}),a.B(),a.k.instrument&&a.k.instrument.tryCatch&&a.C(),a.k.autoBreadcrumbs&&a.D(),a.E(),a.n=!0),Error.stackTraceLimit=a.k.stackTraceLimit,this},setDSN:function(a){var b=this,c=b.F(a),d=c.path.lastIndexOf("/"),e=c.path.substr(1,d);b.G=a,b.h=c.user,b.H=c.pass&&c.pass.substr(1),b.i=c.path.substr(d+1),b.g=b.I(c),b.J=b.g+"/"+e+"api/"+b.i+"/store/",this.y()},context:function(a,b,c){return o(a)&&(c=b||[],b=a,a=void 0),this.wrap(a,b).apply(this,c)},wrap:function(a,b,c){function d(){var d=[],f=arguments.length,g=!a||a&&a.deep!==!1;for(c&&o(c)&&c.apply(this,arguments);f--;)d[f]=g?e.wrap(a,arguments[f]):arguments[f];try{return b.apply(this,d)}catch(h){throw e.K(),e.captureException(h,a),h}}var e=this;if(n(b)&&!o(a))return a;if(o(a)&&(b=a,a=void 0),!o(b))return b;try{if(b.L)return b;if(b.M)return b.M}catch(f){return b}for(var g in b)w(b,g)&&(d[g]=b[g]);return d.prototype=b.prototype,b.M=d,d.L=!0,d.N=b,d},uninstall:function(){return g.report.uninstall(),this.O(),this.P(),Error.stackTraceLimit=this.o,this.n=!1,this},captureException:function(a,b){var c=!k(a),d=!m(a),e=m(a)&&!a.error;if(c&&d||e)return this.captureMessage(a,t({trimHeadFrames:1,stacktrace:!0},b));m(a)&&(a=a.error),this.d=a;try{var f=g.computeStackTrace(a);this.Q(f,b)}catch(h){if(a!==h)throw h}return this},captureMessage:function(a,b){if(!this.k.ignoreErrors.test||!this.k.ignoreErrors.test(a)){b=b||{};var c,d=t({message:a+""},b);try{throw new Error(a)}catch(e){c=e}c.name=null;var f=g.computeStackTrace(c),h=q(f.stack)&&f.stack[1],i=h&&h.url||"";if((!this.k.ignoreUrls.test||!this.k.ignoreUrls.test(i))&&(!this.k.whitelistUrls.test||this.k.whitelistUrls.test(i))){if(this.k.stacktrace||b&&b.stacktrace){b=t({fingerprint:a,trimHeadFrames:(b.trimHeadFrames||0)+1},b);var j=this.R(f,b);d.stacktrace={frames:j.reverse()}}return this.S(d),this}}},captureBreadcrumb:function(a){var b=t({timestamp:d()/1e3},a);if(o(this.k.breadcrumbCallback)){var c=this.k.breadcrumbCallback(b);if(l(c)&&!r(c))b=c;else if(c===!1)return this}return this.u.push(b),this.u.length>this.k.maxBreadcrumbs&&this.u.shift(),this},addPlugin:function(a){var b=[].slice.call(arguments,1);return this.r.push([a,b]),this.n&&this.E(),this},setUserContext:function(a){return this.j.user=a,this},setExtraContext:function(a){return this.T("extra",a),this},setTagsContext:function(a){return this.T("tags",a),this},clearContext:function(){return this.j={},this},getContext:function(){return JSON.parse(h(this.j))},setEnvironment:function(a){return this.k.environment=a,this},setRelease:function(a){return this.k.release=a,this},setDataCallback:function(a){var b=this.k.dataCallback;return this.k.dataCallback=e(b,a),this},setBreadcrumbCallback:function(a){var b=this.k.breadcrumbCallback;return this.k.breadcrumbCallback=e(b,a),this},setShouldSendCallback:function(a){var b=this.k.shouldSendCallback;return this.k.shouldSendCallback=e(b,a),this},setTransport:function(a){return this.k.transport=a,this},lastException:function(){return this.d},lastEventId:function(){return this.f},isSetup:function(){return!!this.a&&(!!this.g||(this.ravenNotConfiguredError||(this.ravenNotConfiguredError=!0,this.z("error","Error: Raven has not been configured.")),!1))},afterLoad:function(){var a=J.RavenConfig;a&&this.config(a.dsn,a.config).install()},showReportDialog:function(a){if(K){a=a||{};var b=a.eventId||this.lastEventId();if(!b)throw new i("Missing eventId");var c=a.dsn||this.G;if(!c)throw new i("Missing DSN");var d=encodeURIComponent,e="";e+="?eventId="+d(b),e+="&dsn="+d(c);var f=a.user||this.j.user;f&&(f.name&&(e+="&name="+d(f.name)),f.email&&(e+="&email="+d(f.email)));var g=this.I(this.F(c)),h=K.createElement("script");h.async=!0,h.src=g+"/api/embed/error-page/"+e,(K.head||K.body).appendChild(h)}},K:function(){var a=this;this.m+=1,setTimeout(function(){a.m-=1})},U:function(a,b){var c,d;if(this.b){b=b||{},a="raven"+a.substr(0,1).toUpperCase()+a.substr(1),K.createEvent?(c=K.createEvent("HTMLEvents"),c.initEvent(a,!0,!0)):(c=K.createEventObject(),c.eventType=a);for(d in b)w(b,d)&&(c[d]=b[d]);if(K.createEvent)K.dispatchEvent(c);else try{K.fireEvent("on"+c.eventType.toLowerCase(),c)}catch(e){}}},V:function(a){var b=this;return function(c){if(b.W=null,b.v!==c){b.v=c;var d;try{d=A(c.target)}catch(e){d="<unknown>"}b.captureBreadcrumb({category:"ui."+a,message:d})}}},X:function(){var a=this,b=1e3;return function(c){var d;try{d=c.target}catch(e){return}var f=d&&d.tagName;if(f&&("INPUT"===f||"TEXTAREA"===f||d.isContentEditable)){var g=a.W;g||a.V("input")(c),clearTimeout(g),a.W=setTimeout(function(){a.W=null},b)}}},Y:function(a,b){var c=D(this.w.href),d=D(b),e=D(a);this.x=b,c.protocol===d.protocol&&c.host===d.host&&(b=d.relative),c.protocol===e.protocol&&c.host===e.host&&(a=e.relative),this.captureBreadcrumb({category:"navigation",data:{to:b,from:a}})},B:function(){var a=this;a.Z=Function.prototype.toString,Function.prototype.toString=function(){return"function"==typeof this&&this.L?a.Z.apply(this.N,arguments):a.Z.apply(this,arguments)}},O:function(){this.Z&&(Function.prototype.toString=this.Z)},C:function(){function a(a){return function(b,d){for(var e=new Array(arguments.length),f=0;f<e.length;++f)e[f]=arguments[f];var g=e[0];return o(g)&&(e[0]=c.wrap(g)),a.apply?a.apply(this,e):a(e[0],e[1])}}function b(a){var b=J[a]&&J[a].prototype;b&&b.hasOwnProperty&&b.hasOwnProperty("addEventListener")&&(E(b,"addEventListener",function(b){return function(d,f,g,h){try{f&&f.handleEvent&&(f.handleEvent=c.wrap(f.handleEvent))}catch(i){}var j,k,l;return e&&e.dom&&("EventTarget"===a||"Node"===a)&&(k=c.V("click"),l=c.X(),j=function(a){if(a){var b;try{b=a.type}catch(c){return}return"click"===b?k(a):"keypress"===b?l(a):void 0}}),b.call(this,d,c.wrap(f,void 0,j),g,h)}},d),E(b,"removeEventListener",function(a){return function(b,c,d,e){try{c=c&&(c.M?c.M:c)}catch(f){}return a.call(this,b,c,d,e)}},d))}var c=this,d=c.t,e=this.k.autoBreadcrumbs;E(J,"setTimeout",a,d),E(J,"setInterval",a,d),J.requestAnimationFrame&&E(J,"requestAnimationFrame",function(a){return function(b){return a(c.wrap(b))}},d);for(var f=["EventTarget","Window","Node","ApplicationCache","AudioTrackList","ChannelMergerNode","CryptoOperation","EventSource","FileReader","HTMLUnknownElement","IDBDatabase","IDBRequest","IDBTransaction","KeyOperation","MediaController","MessagePort","ModalWindow","Notification","SVGElementInstance","Screen","TextTrack","TextTrackCue","TextTrackList","WebSocket","WebSocketWorker","Worker","XMLHttpRequest","XMLHttpRequestEventTarget","XMLHttpRequestUpload"],g=0;g<f.length;g++)b(f[g])},D:function(){function a(a,c){a in c&&o(c[a])&&E(c,a,function(a){return b.wrap(a)})}var b=this,c=this.k.autoBreadcrumbs,d=b.t;if(c.xhr&&"XMLHttpRequest"in J){var e=XMLHttpRequest.prototype;E(e,"open",function(a){return function(c,d){return p(d)&&d.indexOf(b.h)===-1&&(this.$={method:c,url:d,status_code:null}),a.apply(this,arguments)}},d),E(e,"send",function(c){return function(){function d(){if(e.$&&4===e.readyState){try{e.$.status_code=e.status}catch(a){}b.captureBreadcrumb({type:"http",category:"xhr",data:e.$})}}for(var e=this,f=["onload","onerror","onprogress"],g=0;g<f.length;g++)a(f[g],e);return"onreadystatechange"in e&&o(e.onreadystatechange)?E(e,"onreadystatechange",function(a){return b.wrap(a,void 0,d)}):e.onreadystatechange=d,c.apply(this,arguments)}},d)}c.xhr&&F()&&E(J,"fetch",function(a){return function(){for(var c=new Array(arguments.length),d=0;d<c.length;++d)c[d]=arguments[d];var e,f=c[0],g="GET";if("string"==typeof f?e=f:"Request"in J&&f instanceof J.Request?(e=f.url,f.method&&(g=f.method)):e=""+f,e.indexOf(b.h)!==-1)return a.apply(this,c);c[1]&&c[1].method&&(g=c[1].method);var h={method:g,url:e,status_code:null};return a.apply(this,c).then(function(a){return h.status_code=a.status,b.captureBreadcrumb({type:"http",category:"fetch",data:h}),a})}},d),c.dom&&this.b&&(K.addEventListener?(K.addEventListener("click",b.V("click"),!1),K.addEventListener("keypress",b.X(),!1)):(K.attachEvent("onclick",b.V("click")),K.attachEvent("onkeypress",b.X())));var f=J.chrome,g=f&&f.app&&f.app.runtime,h=!g&&J.history&&history.pushState&&history.replaceState;if(c.location&&h){var i=J.onpopstate;J.onpopstate=function(){var a=b.w.href;if(b.Y(b.x,a),i)return i.apply(this,arguments)};var j=function(a){return function(){var c=arguments.length>2?arguments[2]:void 0;return c&&b.Y(b.x,c+""),a.apply(this,arguments)}};E(history,"pushState",j,d),E(history,"replaceState",j,d)}if(c.console&&"console"in J&&console.log){var k=function(a,c){b.captureBreadcrumb({message:a,level:c.level,category:"console"})};s(["debug","info","warn","error","log"],function(a,b){G(console,b,k)})}},P:function(){for(var a;this.t.length;){a=this.t.shift();var b=a[0],c=a[1],d=a[2];b[c]=d}},E:function(){var a=this;s(this.r,function(b,c){var d=c[0],e=c[1];d.apply(a,[a].concat(e))})},F:function(a){var b=I.exec(a),c={},d=7;try{for(;d--;)c[H[d]]=b[d]||""}catch(e){throw new i("Invalid DSN: "+a)}if(c.pass&&!this.k.allowSecretKey)throw new i("Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key");return c},I:function(a){var b="//"+a.host+(a.port?":"+a.port:"");return a.protocol&&(b=a.protocol+":"+b),b},A:function(){this.m||this.Q.apply(this,arguments)},Q:function(a,b){var c=this.R(a,b);this.U("handle",{stackInfo:a,options:b}),this._(a.name,a.message,a.url,a.lineno,c,b)},R:function(a,b){var c=this,d=[];if(a.stack&&a.stack.length&&(s(a.stack,function(b,e){var f=c.aa(e,a.url);f&&d.push(f)}),b&&b.trimHeadFrames))for(var e=0;e<b.trimHeadFrames&&e<d.length;e++)d[e].in_app=!1;return d=d.slice(0,this.k.stackTraceLimit)},aa:function(a,b){var c={filename:a.url,lineno:a.line,colno:a.column,"function":a.func||"?"};return a.url||(c.filename=b),c.in_app=!(this.k.includePaths.test&&!this.k.includePaths.test(c.filename)||/(Raven|TraceKit)\./.test(c["function"])||/raven\.(min\.)?js$/.test(c.filename)),c},_:function(a,b,c,d,e,f){var g=(a?a+": ":"")+(b||"");if(!this.k.ignoreErrors.test||!this.k.ignoreErrors.test(b)&&!this.k.ignoreErrors.test(g)){var h;if(e&&e.length?(c=e[0].filename||c,e.reverse(),h={frames:e}):c&&(h={frames:[{filename:c,lineno:d,in_app:!0}]}),(!this.k.ignoreUrls.test||!this.k.ignoreUrls.test(c))&&(!this.k.whitelistUrls.test||this.k.whitelistUrls.test(c))){var i=t({exception:{values:[{type:a,value:b,stacktrace:h}]},culprit:c},f);this.S(i)}}},ba:function(a){var b=this.k.maxMessageLength;if(a.message&&(a.message=u(a.message,b)),a.exception){var c=a.exception.values[0];c.value=u(c.value,b)}var d=a.request;return d&&(d.url&&(d.url=u(d.url,this.k.maxUrlLength)),d.Referer&&(d.Referer=u(d.Referer,this.k.maxUrlLength))),a.breadcrumbs&&a.breadcrumbs.values&&this.ca(a.breadcrumbs),a},ca:function(a){for(var b,c,d,e=["to","from","url"],f=0;f<a.values.length;++f)if(c=a.values[f],c.hasOwnProperty("data")&&l(c.data)&&!v(c.data)){d=t({},c.data);for(var g=0;g<e.length;++g)b=e[g],d.hasOwnProperty(b)&&d[b]&&(d[b]=u(d[b],this.k.maxUrlLength));a.values[f].data=d}},da:function(){if(this.c||this.b){var a={};return this.c&&L.userAgent&&(a.headers={"User-Agent":navigator.userAgent}),J.location&&J.location.href&&(a.url=J.location.href),this.b&&K.referrer&&(a.headers||(a.headers={}),a.headers.Referer=K.referrer),a}},y:function(){this.ea=0,this.fa=null},ga:function(){return this.ea&&d()-this.fa<this.ea},ha:function(a){var b=this.e;return!(!b||a.message!==b.message||a.culprit!==b.culprit)&&(a.stacktrace||b.stacktrace?C(a.stacktrace,b.stacktrace):!a.exception&&!b.exception||B(a.exception,b.exception))},ia:function(a){if(!this.ga()){var b=a.status;if(400===b||401===b||429===b){var c;try{c=F()?a.headers.get("Retry-After"):a.getResponseHeader("Retry-After"),c=1e3*parseInt(c,10)}catch(e){}this.ea=c?c:2*this.ea||1e3,this.fa=d()}}},S:function(a){var b=this.k,c={project:this.i,logger:b.logger,platform:"javascript"},e=this.da();if(e&&(c.request=e),a.trimHeadFrames&&delete a.trimHeadFrames,a=t(c,a),a.tags=t(t({},this.j.tags),a.tags),a.extra=t(t({},this.j.extra),a.extra),a.extra["session:duration"]=d()-this.s,this.u&&this.u.length>0&&(a.breadcrumbs={values:[].slice.call(this.u,0)}),this.j.user&&(a.user=this.j.user),b.environment&&(a.environment=b.environment),b.release&&(a.release=b.release),b.serverName&&(a.server_name=b.serverName),Object.keys(a).forEach(function(b){(null==a[b]||""===a[b]||r(a[b]))&&delete a[b]}),o(b.dataCallback)&&(a=b.dataCallback(a)||a),a&&!r(a)&&(!o(b.shouldSendCallback)||b.shouldSendCallback(a)))return this.ga()?void this.z("warn","Raven dropped error due to backoff: ",a):void("number"==typeof b.sampleRate?Math.random()<b.sampleRate&&this.ja(a):this.ja(a))},ka:function(){return z()},ja:function(a,b){var c=this,d=this.k;if(this.isSetup()){if(a=this.ba(a),!this.k.allowDuplicates&&this.ha(a))return void this.z("warn","Raven dropped repeat event: ",a);this.f=a.event_id||(a.event_id=this.ka()),this.e=a,this.z("debug","Raven about to send:",a);var e={sentry_version:"7",sentry_client:"raven-js/"+this.VERSION,sentry_key:this.h};this.H&&(e.sentry_secret=this.H);var f=a.exception&&a.exception.values[0];this.k.autoBreadcrumbs&&this.k.autoBreadcrumbs.sentry&&this.captureBreadcrumb({category:"sentry",message:f?(f.type?f.type+": ":"")+f.value:a.message,event_id:a.event_id,level:a.level||"error"});var g=this.J;(d.transport||this.la).call(this,{url:g,auth:e,data:a,options:d,onSuccess:function(){c.y(),c.U("success",{data:a,src:g}),b&&b()},onError:function(d){c.z("error","Raven transport failed to send: ",d),d.request&&c.ia(d.request),c.U("failure",{data:a,src:g}),d=d||new Error("Raven send failed (no additional details provided)"),b&&b(d)}})}},la:function(a){var b=a.url+"?"+y(a.auth),c=null,d={};if(a.options.headers&&(c=this.ma(a.options.headers)),a.options.fetchParameters&&(d=this.ma(a.options.fetchParameters)),F()){d.body=h(a.data);var e=t({},this.l),f=t(e,d);return c&&(f.headers=c),J.fetch(b,f).then(function(b){if(b.ok)a.onSuccess&&a.onSuccess();else{var c=new Error("Sentry error code: "+b.status);c.request=b,a.onError&&a.onError(c)}})["catch"](function(){a.onError&&a.onError(new Error("Sentry error code: network unavailable"))})}var g=J.XMLHttpRequest&&new J.XMLHttpRequest;if(g){var i="withCredentials"in g||"undefined"!=typeof XDomainRequest;i&&("withCredentials"in g?g.onreadystatechange=function(){if(4===g.readyState)if(200===g.status)a.onSuccess&&a.onSuccess();else if(a.onError){var b=new Error("Sentry error code: "+g.status);b.request=g,a.onError(b)}}:(g=new XDomainRequest,b=b.replace(/^https?:/,""),a.onSuccess&&(g.onload=a.onSuccess),a.onError&&(g.onerror=function(){var b=new Error("Sentry error code: XDomainRequest");b.request=g,a.onError(b)})),g.open("POST",b),c&&s(c,function(a,b){g.setRequestHeader(a,b)}),g.send(h(a.data)))}},ma:function(a){var b={};for(var c in a)if(a.hasOwnProperty(c)){var d=a[c];b[c]="function"==typeof d?d():d}return b},z:function(a){this.q[a]&&this.debug&&Function.prototype.apply.call(this.q[a],this.p,[].slice.call(arguments,1))},T:function(a,b){n(b)?delete this.j[a]:this.j[a]=t(this.j[a]||{},b)}},f.prototype.setUser=f.prototype.setUserContext,f.prototype.setReleaseContext=f.prototype.setRelease,b.exports=f}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{1:1,2:2,5:5,6:6,7:7}],4:[function(a,b,c){(function(c){var d=a(3),e="undefined"!=typeof window?window:"undefined"!=typeof c?c:"undefined"!=typeof self?self:{},f=e.Raven,g=new d;g.noConflict=function(){return e.Raven=f,g},g.afterLoad(),b.exports=g}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{3:3}],5:[function(a,b,c){(function(a){function c(a){return"object"==typeof a&&null!==a}function d(a){switch({}.toString.call(a)){case"[object Error]":return!0;case"[object Exception]":return!0;case"[object DOMException]":return!0;default:return a instanceof Error}}function e(a){return l()&&"[object ErrorEvent]"==={}.toString.call(a)}function f(a){return void 0===a}function g(a){return"function"==typeof a}function h(a){return"[object Object]"===Object.prototype.toString.call(a)}function i(a){return"[object String]"===Object.prototype.toString.call(a)}function j(a){return"[object Array]"===Object.prototype.toString.call(a)}function k(a){if(!h(a))return!1;for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}function l(){try{return new ErrorEvent(""),!0}catch(a){return!1}}function m(){if(!("fetch"in E))return!1;try{return new Headers,new Request(""),new Response,!0}catch(a){return!1}}function n(a){function b(b,c){var d=a(b)||b;return c?c(d)||d:d}return b}function o(a,b){var c,d;if(f(a.length))for(c in a)s(a,c)&&b.call(null,c,a[c]);else if(d=a.length)for(c=0;c<d;c++)b.call(null,c,a[c])}function p(a,b){return b?(o(b,function(b,c){a[b]=c}),a):a}function q(a){return!!Object.isFrozen&&Object.isFrozen(a)}function r(a,b){return!b||a.length<=b?a:a.substr(0,b)+"…"}function s(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function t(a){for(var b,c=[],d=0,e=a.length;d<e;d++)b=a[d],i(b)?c.push(b.replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")):b&&b.source&&c.push(b.source);return new RegExp(c.join("|"),"i")}function u(a){var b=[];return o(a,function(a,c){b.push(encodeURIComponent(a)+"="+encodeURIComponent(c))}),b.join("&")}function v(a){if("string"!=typeof a)return{};var b=a.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/),c=b[6]||"",d=b[8]||"";return{protocol:b[2],host:b[4],path:b[5],relative:b[5]+c+d}}function w(){var a=E.crypto||E.msCrypto;if(!f(a)&&a.getRandomValues){var b=new Uint16Array(8);a.getRandomValues(b),b[3]=4095&b[3]|16384,b[4]=16383&b[4]|32768;var c=function(a){for(var b=a.toString(16);b.length<4;)b="0"+b;return b};return c(b[0])+c(b[1])+c(b[2])+c(b[3])+c(b[4])+c(b[5])+c(b[6])+c(b[7])}return"xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0,c="x"===a?b:3&b|8;return c.toString(16)})}function x(a){for(var b,c=5,d=80,e=[],f=0,g=0,h=" > ",i=h.length;a&&f++<c&&(b=y(a),!("html"===b||f>1&&g+e.length*i+b.length>=d));)e.push(b),g+=b.length,a=a.parentNode;return e.reverse().join(h)}function y(a){var b,c,d,e,f,g=[];if(!a||!a.tagName)return"";if(g.push(a.tagName.toLowerCase()),a.id&&g.push("#"+a.id),b=a.className,b&&i(b))for(c=b.split(/\s+/),f=0;f<c.length;f++)g.push("."+c[f]);var h=["type","name","title","alt"];for(f=0;f<h.length;f++)d=h[f],e=a.getAttribute(d),e&&g.push("["+d+'="'+e+'"]');return g.join("")}function z(a,b){return!!(!!a^!!b)}function A(a,b){return f(a)&&f(b)}function B(a,b){return!z(a,b)&&(a=a.values[0],b=b.values[0],a.type===b.type&&a.value===b.value&&(!A(a.stacktrace,b.stacktrace)&&C(a.stacktrace,b.stacktrace)))}function C(a,b){if(z(a,b))return!1;var c=a.frames,d=b.frames;if(c.length!==d.length)return!1;for(var e,f,g=0;g<c.length;g++)if(e=c[g],f=d[g],e.filename!==f.filename||e.lineno!==f.lineno||e.colno!==f.colno||e["function"]!==f["function"])return!1;return!0}function D(a,b,c,d){var e=a[b];a[b]=c(e),a[b].L=!0,a[b].N=e,d&&d.push([a,b,e])}var E="undefined"!=typeof window?window:"undefined"!=typeof a?a:"undefined"!=typeof self?self:{};b.exports={isObject:c,isError:d,isErrorEvent:e,isUndefined:f,isFunction:g,isPlainObject:h,isString:i,isArray:j,isEmptyObject:k,supportsErrorEvent:l,supportsFetch:m,wrappedCallback:n,each:o,objectMerge:p,truncate:r,objectFrozen:q,hasKey:s,joinRegExp:t,urlencode:u,uuid4:w,htmlTreeAsString:x,htmlElementAsString:y,isSameException:B,isSameStacktrace:C,parseUrl:v,fill:D}}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],6:[function(a,b,c){(function(c){function d(){return"undefined"==typeof document||null==document.location?"":document.location.href}var e=a(5),f={collectWindowErrors:!0,debug:!1},g="undefined"!=typeof window?window:"undefined"!=typeof c?c:"undefined"!=typeof self?self:{},h=[].slice,i="?",j=/^(?:[Uu]ncaught (?:exception: )?)?(?:((?:Eval|Internal|Range|Reference|Syntax|Type|URI|)Error): )?(.*)$/;f.report=function(){function a(a){m(),s.push(a)}function b(a){for(var b=s.length-1;b>=0;--b)s[b]===a&&s.splice(b,1)}function c(){n(),s=[]}function k(a,b){var c=null;if(!b||f.collectWindowErrors){for(var d in s)if(s.hasOwnProperty(d))try{s[d].apply(null,[a].concat(h.call(arguments,2)))}catch(e){c=e}if(c)throw c}}function l(a,b,c,g,h){var l=null,m=e.isErrorEvent(h)?h.error:h,n=e.isErrorEvent(a)?a.message:a;if(v)f.computeStackTrace.augmentStackTraceWithInitialElement(v,b,c,n),o();else if(m&&e.isError(m))l=f.computeStackTrace(m),k(l,!0);else{var p,r={url:b,line:c,column:g},s=void 0;if("[object String]"==={}.toString.call(n)){var p=n.match(j);p&&(s=p[1],n=p[2])}r.func=i,l={name:s,message:n,url:d(),stack:[r]},k(l,!0)}return!!q&&q.apply(this,arguments)}function m(){r||(q=g.onerror,g.onerror=l,r=!0)}function n(){r&&(g.onerror=q,r=!1,q=void 0)}function o(){var a=v,b=t;t=null,v=null,u=null,k.apply(null,[a,!1].concat(b))}function p(a,b){var c=h.call(arguments,1);if(v){if(u===a)return;o()}var d=f.computeStackTrace(a);if(v=d,u=a,t=c,setTimeout(function(){u===a&&o()},d.incomplete?2e3:0),b!==!1)throw a}var q,r,s=[],t=null,u=null,v=null;return p.subscribe=a,p.unsubscribe=b,p.uninstall=c,p}(),f.computeStackTrace=function(){function a(a){if("undefined"!=typeof a.stack&&a.stack){for(var b,c,e,f=/^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|webpack|<anonymous>|[a-z]:|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,g=/^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|webpack|resource|\[native).*?|[^@]*bundle)(?::(\d+))?(?::(\d+))?\s*$/i,h=/^\s*at (?:((?:\[object object\])?.+) )?\(?((?:file|ms-appx(?:-web)|https?|webpack|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,j=/(\S+) line (\d+)(?: > eval line \d+)* > eval/i,k=/\((\S*)(?::(\d+))(?::(\d+))\)/,l=a.stack.split("\n"),m=[],n=(/^(.*) is undefined$/.exec(a.message),0),o=l.length;n<o;++n){if(c=f.exec(l[n])){var p=c[2]&&0===c[2].indexOf("native"),q=c[2]&&0===c[2].indexOf("eval");q&&(b=k.exec(c[2]))&&(c[2]=b[1],c[3]=b[2],c[4]=b[3]),e={url:p?null:c[2],func:c[1]||i,args:p?[c[2]]:[],line:c[3]?+c[3]:null,column:c[4]?+c[4]:null}}else if(c=h.exec(l[n]))e={url:c[2],func:c[1]||i,args:[],line:+c[3],column:c[4]?+c[4]:null};else{if(!(c=g.exec(l[n])))continue;var q=c[3]&&c[3].indexOf(" > eval")>-1;q&&(b=j.exec(c[3]))?(c[3]=b[1],c[4]=b[2],c[5]=null):0!==n||c[5]||"undefined"==typeof a.columnNumber||(m[0].column=a.columnNumber+1),e={url:c[3],func:c[1]||i,args:c[2]?c[2].split(","):[],line:c[4]?+c[4]:null,column:c[5]?+c[5]:null}}!e.func&&e.line&&(e.func=i),m.push(e)}return m.length?{name:a.name,message:a.message,url:d(),stack:m}:null}}function b(a,b,c,d){var e={url:b,line:c};if(e.url&&e.line){if(a.incomplete=!1,e.func||(e.func=i),a.stack.length>0&&a.stack[0].url===e.url){if(a.stack[0].line===e.line)return!1;if(!a.stack[0].line&&a.stack[0].func===e.func)return a.stack[0].line=e.line,!1}return a.stack.unshift(e),a.partial=!0,!0}return a.incomplete=!0,!1}function c(a,g){for(var h,j,k=/function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i,l=[],m={},n=!1,o=c.caller;o&&!n;o=o.caller)if(o!==e&&o!==f.report){if(j={url:null,func:i,line:null,column:null},o.name?j.func=o.name:(h=k.exec(o.toString()))&&(j.func=h[1]),"undefined"==typeof j.func)try{j.func=h.input.substring(0,h.input.indexOf("{"))}catch(p){}m[""+o]?n=!0:m[""+o]=!0,l.push(j)}g&&l.splice(0,g);var q={name:a.name,message:a.message,url:d(),stack:l};return b(q,a.sourceURL||a.fileName,a.line||a.lineNumber,a.message||a.description),q}function e(b,e){var g=null;e=null==e?0:+e;try{if(g=a(b))return g}catch(h){if(f.debug)throw h}try{if(g=c(b,e+1))return g}catch(h){if(f.debug)throw h}return{name:b.name,message:b.message,url:d()}}return e.augmentStackTraceWithInitialElement=b,e.computeStackTraceFromStackProp=a,e}(),b.exports=f}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{5:5}],7:[function(a,b,c){function d(a,b){for(var c=0;c<a.length;++c)if(a[c]===b)return c;return-1}function e(a,b,c,d){return JSON.stringify(a,g(b,d),c)}function f(a){var b={stack:a.stack,message:a.message,name:a.name};for(var c in a)Object.prototype.hasOwnProperty.call(a,c)&&(b[c]=a[c]);return b}function g(a,b){var c=[],e=[];return null==b&&(b=function(a,b){return c[0]===b?"[Circular ~]":"[Circular ~."+e.slice(0,d(c,b)).join(".")+"]"}),function(g,h){if(c.length>0){var i=d(c,this);~i?c.splice(i+1):c.push(this),~i?e.splice(i,1/0,g):e.push(g),~d(c,h)&&(h=b.call(this,g,h))}else c.push(h);return null==a?h instanceof Error?f(h):h:a.call(this,g,h)}}c=b.exports=e,c.getSerialize=g},{}]},{},[4])(4)});
+//# sourceMappingURL=raven.min.js.map \ No newline at end of file