aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts
diff options
context:
space:
mode:
authorbrunobar79 <brunobar79@gmail.com>2018-11-07 09:21:19 +0800
committerbrunobar79 <brunobar79@gmail.com>2018-11-07 09:21:19 +0800
commitc651212025ab7cd0309d96be6687c1eba35ab9fa (patch)
treefa5a6020daae332c5df47bdee3492e8a079375e3 /app/scripts
parent9c1b2108f69334884473eb95758e2d1c02c984d6 (diff)
parent7f944923d170d6e7e04f6e238d2fd5f80abe3161 (diff)
downloadtangerine-wallet-browser-c651212025ab7cd0309d96be6687c1eba35ab9fa.tar.gz
tangerine-wallet-browser-c651212025ab7cd0309d96be6687c1eba35ab9fa.tar.zst
tangerine-wallet-browser-c651212025ab7cd0309d96be6687c1eba35ab9fa.zip
fix merge conflicts
Diffstat (limited to 'app/scripts')
-rw-r--r--app/scripts/background.js27
-rw-r--r--app/scripts/contentscript.js128
-rw-r--r--app/scripts/controllers/blacklist.js25
-rw-r--r--app/scripts/controllers/currency.js80
-rw-r--r--app/scripts/controllers/network/createInfuraClient.js33
-rw-r--r--app/scripts/controllers/network/createMetamaskMiddleware.js2
-rw-r--r--app/scripts/controllers/network/network.js60
-rw-r--r--app/scripts/controllers/preferences.js39
-rw-r--r--app/scripts/controllers/provider-approval.js152
-rw-r--r--app/scripts/controllers/token-rates.js11
-rw-r--r--app/scripts/controllers/transactions/tx-gas-utils.js35
-rw-r--r--app/scripts/inpage.js151
-rw-r--r--app/scripts/lib/ens-ipfs/contracts/registrar.js (renamed from app/scripts/lib/contracts/registrar.js)0
-rw-r--r--app/scripts/lib/ens-ipfs/contracts/resolver.js (renamed from app/scripts/lib/contracts/resolver.js)0
-rw-r--r--app/scripts/lib/ens-ipfs/resolver.js54
-rw-r--r--app/scripts/lib/ens-ipfs/setup.js63
-rw-r--r--app/scripts/lib/ipfsContent.js46
-rw-r--r--app/scripts/lib/reportFailedTxToSentry.js6
-rw-r--r--app/scripts/lib/resolver.js71
-rw-r--r--app/scripts/lib/setupFetchDebugging.js10
-rw-r--r--app/scripts/lib/setupSentry.js (renamed from app/scripts/lib/setupRaven.js)75
-rw-r--r--app/scripts/metamask-controller.js83
-rw-r--r--app/scripts/platforms/extension.js12
-rw-r--r--app/scripts/ui.js14
24 files changed, 886 insertions, 291 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js
index d20ffe6e3..2dcb79bef 100644
--- a/app/scripts/background.js
+++ b/app/scripts/background.js
@@ -23,13 +23,13 @@ const createStreamSink = require('./lib/createStreamSink')
const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller')
const rawFirstTimeState = require('./first-time-state')
-const setupRaven = require('./lib/setupRaven')
+const setupSentry = require('./lib/setupSentry')
const reportFailedTxToSentry = require('./lib/reportFailedTxToSentry')
const setupMetamaskMeshMetrics = require('./lib/setupMetamaskMeshMetrics')
const EdgeEncryptor = require('./edge-encryptor')
const getFirstPreferredLangCode = require('./lib/get-first-preferred-lang-code')
const getObjStructure = require('./lib/getObjStructure')
-const ipfsContent = require('./lib/ipfsContent.js')
+const setupEnsIpfsResolver = require('./lib/ens-ipfs/setup')
const {
ENVIRONMENT_TYPE_POPUP,
@@ -50,7 +50,7 @@ global.METAMASK_NOTIFIER = notificationManager
// setup sentry error reporting
const release = platform.getVersion()
-const raven = setupRaven({ release })
+const sentry = setupSentry({ 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
@@ -58,7 +58,6 @@ const isIE = !!document.documentMode
// Edge 20+
const isEdge = !isIE && !!window.StyleMedia
-let ipfsHandle
let popupIsOpen = false
let notificationIsOpen = false
const openMetamaskTabsIDs = {}
@@ -164,7 +163,6 @@ async function initialize () {
const initLangCode = await getFirstPreferredLangCode()
await setupController(initState, initLangCode)
log.debug('MetaMask initialization complete.')
- ipfsHandle = ipfsContent(initState.NetworkController.provider)
}
//
@@ -197,14 +195,14 @@ async function loadStateFromPersistence () {
// we were able to recover (though it might be old)
versionedData = diskStoreState
const vaultStructure = getObjStructure(versionedData)
- raven.captureMessage('MetaMask - Empty vault found - recovered from diskStore', {
+ sentry.captureMessage('MetaMask - Empty vault found - recovered from diskStore', {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
} else {
// unable to recover, clear state
versionedData = migrator.generateInitialState(firstTimeState)
- raven.captureMessage('MetaMask - Empty vault found - unable to recover')
+ sentry.captureMessage('MetaMask - Empty vault found - unable to recover')
}
}
@@ -212,7 +210,7 @@ async function loadStateFromPersistence () {
migrator.on('error', (err) => {
// get vault structure without secrets
const vaultStructure = getObjStructure(versionedData)
- raven.captureException(err, {
+ sentry.captureException(err, {
// "extra" key is required by Sentry
extra: { vaultStructure },
})
@@ -258,7 +256,8 @@ function setupController (initState, initLangCode) {
showUnconfirmedMessage: triggerUi,
unlockAccountMessage: triggerUi,
showUnapprovedTx: triggerUi,
- showWatchAssetUi: showWatchAssetUi,
+ openPopup: openPopup,
+ closePopup: notificationManager.closePopup.bind(notificationManager),
// initial state
initState,
// initial locale code
@@ -269,17 +268,15 @@ function setupController (initState, initLangCode) {
})
global.metamaskController = controller
- controller.networkController.on('networkDidChange', () => {
- ipfsHandle && ipfsHandle.remove()
- ipfsHandle = ipfsContent(controller.networkController.providerStore.getState())
- })
+ const provider = controller.provider
+ setupEnsIpfsResolver({ provider })
// report failed transactions to Sentry
controller.txController.on(`tx:status-update`, (txId, status) => {
if (status !== 'failed') return
const txMeta = controller.txController.txStateManager.getTx(txId)
try {
- reportFailedTxToSentry({ raven, txMeta })
+ reportFailedTxToSentry({ sentry, txMeta })
} catch (e) {
console.error(e)
}
@@ -459,7 +456,7 @@ function triggerUi () {
* Opens the browser popup for user confirmation of watchAsset
* then it waits until user interact with the UI
*/
-function showWatchAssetUi () {
+function openPopup () {
triggerUi()
return new Promise(
(resolve) => {
diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js
index 33523eb46..1a10cdb34 100644
--- a/app/scripts/contentscript.js
+++ b/app/scripts/contentscript.js
@@ -7,10 +7,12 @@ const PongStream = require('ping-pong-stream/pong')
const ObjectMultiplex = require('obj-multiplex')
const extension = require('extensionizer')
const PortStream = require('extension-port-stream')
+const TransformStream = require('stream').Transform
const inpageContent = fs.readFileSync(path.join(__dirname, '..', '..', 'dist', 'chrome', 'inpage.js')).toString()
const inpageSuffix = '//# sourceURL=' + extension.extension.getURL('inpage.js') + '\n'
const inpageBundle = inpageContent + inpageSuffix
+let isEnabled = false
// Eventually this streaming injection could be replaced with:
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Language_Bindings/Components.utils.exportFunction
@@ -20,24 +22,27 @@ const inpageBundle = inpageContent + inpageSuffix
// MetaMask will be much faster loading and performant on Firefox.
if (shouldInjectWeb3()) {
- setupInjection()
+ injectScript(inpageBundle)
setupStreams()
+ listenForProviderRequest()
+ checkPrivacyMode()
}
/**
- * Creates a script tag that injects inpage.js
+ * Injects a script tag into the current document
+ *
+ * @param {string} content - Code to be executed in the current document
*/
-function setupInjection () {
+function injectScript (content) {
try {
- // inject in-page script
- var scriptTag = document.createElement('script')
- scriptTag.textContent = inpageBundle
- scriptTag.onload = function () { this.parentNode.removeChild(this) }
- var container = document.head || document.documentElement
- // append as first child
+ const container = document.head || document.documentElement
+ const scriptTag = document.createElement('script')
+ scriptTag.setAttribute('async', false)
+ scriptTag.textContent = content
container.insertBefore(scriptTag, container.children[0])
+ container.removeChild(scriptTag)
} catch (e) {
- console.error('Metamask injection failed.', e)
+ console.error('MetaMask script injection failed', e)
}
}
@@ -54,10 +59,22 @@ function setupStreams () {
const pluginPort = extension.runtime.connect({ name: 'contentscript' })
const pluginStream = new PortStream(pluginPort)
+ // Filter out selectedAddress until this origin is enabled
+ const approvalTransform = new TransformStream({
+ objectMode: true,
+ transform: (data, _, done) => {
+ if (typeof data === 'object' && data.name && data.name === 'publicConfig' && !isEnabled) {
+ data.data.selectedAddress = undefined
+ }
+ done(null, { ...data })
+ },
+ })
+
// forward communication plugin->inpage
pump(
pageStream,
pluginStream,
+ approvalTransform,
pageStream,
(err) => logStreamDisconnectWarning('MetaMask Contentscript Forwarding', err)
)
@@ -97,6 +114,69 @@ function setupStreams () {
mux.ignoreStream('publicConfig')
}
+/**
+ * Establishes listeners for requests to fully-enable the provider from the dapp context
+ * and for full-provider approvals and rejections from the background script context. Dapps
+ * should not post messages directly and should instead call provider.enable(), which
+ * handles posting these messages internally.
+ */
+function listenForProviderRequest () {
+ window.addEventListener('message', ({ source, data }) => {
+ if (source !== window || !data || !data.type) { return }
+ switch (data.type) {
+ case 'ETHEREUM_ENABLE_PROVIDER':
+ extension.runtime.sendMessage({
+ action: 'init-provider-request',
+ force: data.force,
+ origin: source.location.hostname,
+ siteImage: getSiteIcon(source),
+ siteTitle: getSiteName(source),
+ })
+ break
+ case 'ETHEREUM_IS_APPROVED':
+ extension.runtime.sendMessage({
+ action: 'init-is-approved',
+ origin: source.location.hostname,
+ })
+ break
+ case 'METAMASK_IS_UNLOCKED':
+ extension.runtime.sendMessage({
+ action: 'init-is-unlocked',
+ })
+ break
+ }
+ })
+
+ extension.runtime.onMessage.addListener(({ action = '', isApproved, caching, isUnlocked }) => {
+ switch (action) {
+ case 'approve-provider-request':
+ isEnabled = true
+ injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: {}}))`)
+ break
+ case 'reject-provider-request':
+ injectScript(`window.dispatchEvent(new CustomEvent('ethereumprovider', { detail: { error: 'User rejected provider access' }}))`)
+ break
+ case 'answer-is-approved':
+ injectScript(`window.dispatchEvent(new CustomEvent('ethereumisapproved', { detail: { isApproved: ${isApproved}, caching: ${caching}}}))`)
+ break
+ case 'answer-is-unlocked':
+ injectScript(`window.dispatchEvent(new CustomEvent('metamaskisunlocked', { detail: { isUnlocked: ${isUnlocked}}}))`)
+ break
+ case 'metamask-set-locked':
+ isEnabled = false
+ injectScript(`window.dispatchEvent(new CustomEvent('metamasksetlocked', { detail: {}}))`)
+ break
+ }
+ })
+}
+
+/**
+ * Checks if MetaMask is currently operating in "privacy mode", meaning
+ * dapps must call ethereum.enable in order to access user accounts
+ */
+function checkPrivacyMode () {
+ extension.runtime.sendMessage({ action: 'init-privacy-request' })
+}
/**
* Error handler for page to plugin stream disconnections
@@ -210,3 +290,31 @@ function redirectToPhishingWarning () {
href: window.location.href,
})}`
}
+
+function getSiteName (window) {
+ const document = window.document
+ const siteName = document.querySelector('head > meta[property="og:site_name"]')
+ if (siteName) {
+ return siteName.content
+ }
+
+ return document.title
+}
+
+function getSiteIcon (window) {
+ const document = window.document
+
+ // Use the site's favicon if it exists
+ const shortcutIcon = document.querySelector('head > link[rel="shortcut icon"]')
+ if (shortcutIcon) {
+ return shortcutIcon.href
+ }
+
+ // Search through available icons in no particular order
+ const icon = Array.from(document.querySelectorAll('head > link[rel="icon"]')).find((icon) => Boolean(icon.href))
+ if (icon) {
+ return icon.href
+ }
+
+ return null
+}
diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js
index 89c7cc888..e55b09d03 100644
--- a/app/scripts/controllers/blacklist.js
+++ b/app/scripts/controllers/blacklist.js
@@ -83,8 +83,25 @@ class BlacklistController {
*
*/
async updatePhishingList () {
- const response = await fetch('https://api.infura.io/v2/blacklist')
- const phishing = await response.json()
+ // make request
+ let response
+ try {
+ response = await fetch('https://api.infura.io/v2/blacklist')
+ } catch (err) {
+ log.error(new Error(`BlacklistController - failed to fetch blacklist:\n${err.stack}`))
+ return
+ }
+ // parse response
+ let rawResponse
+ let phishing
+ try {
+ const rawResponse = await response.text()
+ phishing = JSON.parse(rawResponse)
+ } catch (err) {
+ log.error(new Error(`BlacklistController - failed to parse blacklist:\n${rawResponse}`))
+ return
+ }
+ // update current blacklist
this.store.updateState({ phishing })
this._setupPhishingDetector(phishing)
return phishing
@@ -97,9 +114,9 @@ class BlacklistController {
*/
scheduleUpdates () {
if (this._phishingUpdateIntervalRef) return
- this.updatePhishingList().catch(log.warn)
+ this.updatePhishingList()
this._phishingUpdateIntervalRef = setInterval(() => {
- this.updatePhishingList().catch(log.warn)
+ this.updatePhishingList()
}, POLLING_INTERVAL)
}
diff --git a/app/scripts/controllers/currency.js b/app/scripts/controllers/currency.js
index d5bc5fe2b..fce65fd9c 100644
--- a/app/scripts/controllers/currency.js
+++ b/app/scripts/controllers/currency.js
@@ -21,6 +21,7 @@ class CurrencyController {
* since midnight of January 1, 1970
* @property {number} conversionInterval The id of the interval created by the scheduleConversionInterval method.
* Used to clear an existing interval on subsequent calls of that method.
+ * @property {string} nativeCurrency The ticker/symbol of the native chain currency
*
*/
constructor (opts = {}) {
@@ -28,6 +29,7 @@ class CurrencyController {
currentCurrency: 'usd',
conversionRate: 0,
conversionDate: 'N/A',
+ nativeCurrency: 'ETH',
}, opts.initState)
this.store = new ObservableStore(initState)
}
@@ -37,6 +39,29 @@ class CurrencyController {
//
/**
+ * A getter for the nativeCurrency property
+ *
+ * @returns {string} A 2-4 character shorthand that describes the specific currency
+ *
+ */
+ getNativeCurrency () {
+ return this.store.getState().nativeCurrency
+ }
+
+ /**
+ * A setter for the nativeCurrency property
+ *
+ * @param {string} nativeCurrency The new currency to set as the nativeCurrency in the store
+ *
+ */
+ setNativeCurrency (nativeCurrency) {
+ this.store.updateState({
+ nativeCurrency,
+ ticker: nativeCurrency,
+ })
+ }
+
+ /**
* A getter for the currentCurrency property
*
* @returns {string} A 2-4 character shorthand that describes a specific currency, currently selected by the user
@@ -104,17 +129,60 @@ class CurrencyController {
*
*/
async updateConversionRate () {
- let currentCurrency
+ let currentCurrency, nativeCurrency
try {
currentCurrency = this.getCurrentCurrency()
- const response = await fetch(`https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`)
- const parsedResponse = await response.json()
- this.setConversionRate(Number(parsedResponse.bid))
- this.setConversionDate(Number(parsedResponse.timestamp))
+ nativeCurrency = this.getNativeCurrency()
+ // select api
+ let apiUrl
+ if (nativeCurrency === 'ETH') {
+ // ETH
+ apiUrl = `https://api.infura.io/v1/ticker/eth${currentCurrency.toLowerCase()}`
+ } else {
+ // ETC
+ apiUrl = `https://min-api.cryptocompare.com/data/price?fsym=${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}`
+ }
+ // attempt request
+ let response
+ try {
+ response = await fetch(apiUrl)
+ } catch (err) {
+ log.error(new Error(`CurrencyController - Failed to request currency from Infura:\n${err.stack}`))
+ return
+ }
+ // parse response
+ let rawResponse
+ let parsedResponse
+ try {
+ rawResponse = await response.text()
+ parsedResponse = JSON.parse(rawResponse)
+ } catch (err) {
+ log.error(new Error(`CurrencyController - Failed to parse response "${rawResponse}"`))
+ return
+ }
+ // set conversion rate
+ if (nativeCurrency === 'ETH') {
+ // ETH
+ this.setConversionRate(Number(parsedResponse.bid))
+ this.setConversionDate(Number(parsedResponse.timestamp))
+ } else {
+ // ETC
+ if (parsedResponse[currentCurrency.toUpperCase()]) {
+ this.setConversionRate(Number(parsedResponse[currentCurrency.toUpperCase()]))
+ this.setConversionDate(parseInt((new Date()).getTime() / 1000))
+ } else {
+ this.setConversionRate(0)
+ this.setConversionDate('N/A')
+ }
+ }
} catch (err) {
- log.warn(`MetaMask - Failed to query currency conversion:`, currentCurrency, err)
+ // reset current conversion rate
+ log.warn(`MetaMask - Failed to query currency conversion:`, nativeCurrency, currentCurrency, err)
this.setConversionRate(0)
this.setConversionDate('N/A')
+ // throw error
+ log.error(new Error(`CurrencyController - Failed to query rate for currency "${currentCurrency}":\n${err.stack}`))
+ return
}
}
diff --git a/app/scripts/controllers/network/createInfuraClient.js b/app/scripts/controllers/network/createInfuraClient.js
index 326bcb355..c70fa9e38 100644
--- a/app/scripts/controllers/network/createInfuraClient.js
+++ b/app/scripts/controllers/network/createInfuraClient.js
@@ -1,4 +1,5 @@
const mergeMiddleware = require('json-rpc-engine/src/mergeMiddleware')
+const createScaffoldMiddleware = require('json-rpc-engine/src/createScaffoldMiddleware')
const createBlockReRefMiddleware = require('eth-json-rpc-middleware/block-ref')
const createRetryOnEmptyMiddleware = require('eth-json-rpc-middleware/retryOnEmpty')
const createBlockCacheMiddleware = require('eth-json-rpc-middleware/block-cache')
@@ -16,6 +17,7 @@ function createInfuraClient ({ network }) {
const blockTracker = new BlockTracker({ provider: infuraProvider })
const networkMiddleware = mergeMiddleware([
+ createNetworkAndChainIdMiddleware({ network }),
createBlockCacheMiddleware({ blockTracker }),
createInflightMiddleware(),
createBlockReRefMiddleware({ blockTracker, provider: infuraProvider }),
@@ -25,3 +27,34 @@ function createInfuraClient ({ network }) {
])
return { networkMiddleware, blockTracker }
}
+
+function createNetworkAndChainIdMiddleware({ network }) {
+ let chainId
+ let netId
+
+ switch (network) {
+ case 'mainnet':
+ netId = '1'
+ chainId = '0x01'
+ break
+ case 'ropsten':
+ netId = '3'
+ chainId = '0x03'
+ break
+ case 'rinkeby':
+ netId = '4'
+ chainId = '0x04'
+ break
+ case 'kovan':
+ netId = '42'
+ chainId = '0x2a'
+ break
+ default:
+ throw new Error(`createInfuraClient - unknown network "${network}"`)
+ }
+
+ return createScaffoldMiddleware({
+ eth_chainId: chainId,
+ net_version: netId,
+ })
+}
diff --git a/app/scripts/controllers/network/createMetamaskMiddleware.js b/app/scripts/controllers/network/createMetamaskMiddleware.js
index 9e6a45888..319c5bf3e 100644
--- a/app/scripts/controllers/network/createMetamaskMiddleware.js
+++ b/app/scripts/controllers/network/createMetamaskMiddleware.js
@@ -11,6 +11,7 @@ function createMetamaskMiddleware ({
processTransaction,
processEthSignMessage,
processTypedMessage,
+ processTypedMessageV3,
processPersonalMessage,
getPendingNonce,
}) {
@@ -25,6 +26,7 @@ function createMetamaskMiddleware ({
processTransaction,
processEthSignMessage,
processTypedMessage,
+ processTypedMessageV3,
processPersonalMessage,
}),
createPendingNonceMiddleware({ getPendingNonce }),
diff --git a/app/scripts/controllers/network/network.js b/app/scripts/controllers/network/network.js
index c1667d9a6..b459b8013 100644
--- a/app/scripts/controllers/network/network.js
+++ b/app/scripts/controllers/network/network.js
@@ -11,6 +11,8 @@ const createInfuraClient = require('./createInfuraClient')
const createJsonRpcClient = require('./createJsonRpcClient')
const createLocalhostClient = require('./createLocalhostClient')
const { createSwappableProxy, createEventEmitterProxy } = require('swappable-obj-proxy')
+const extend = require('extend')
+const networks = { networkList: {} }
const {
ROPSTEN,
@@ -29,6 +31,10 @@ const defaultProviderConfig = {
type: testMode ? RINKEBY : MAINNET,
}
+const defaultNetworkConfig = {
+ ticker: 'ETH',
+}
+
module.exports = class NetworkController extends EventEmitter {
constructor (opts = {}) {
@@ -39,7 +45,8 @@ module.exports = class NetworkController extends EventEmitter {
// create stores
this.providerStore = new ObservableStore(providerConfig)
this.networkStore = new ObservableStore('loading')
- this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore })
+ this.networkConfig = new ObservableStore(defaultNetworkConfig)
+ this.store = new ComposedStore({ provider: this.providerStore, network: this.networkStore, settings: this.networkConfig })
this.on('networkDidChange', this.lookupNetwork)
// provider and block tracker
this._provider = null
@@ -51,8 +58,8 @@ module.exports = class NetworkController extends EventEmitter {
initializeProvider (providerParams) {
this._baseProviderParams = providerParams
- const { type, rpcTarget } = this.providerStore.getState()
- this._configureProvider({ type, rpcTarget })
+ const { type, rpcTarget, chainId, ticker, nickname } = this.providerStore.getState()
+ this._configureProvider({ type, rpcTarget, chainId, ticker, nickname })
this.lookupNetwork()
}
@@ -72,7 +79,20 @@ module.exports = class NetworkController extends EventEmitter {
return this.networkStore.getState()
}
- setNetworkState (network) {
+ getNetworkConfig () {
+ return this.networkConfig.getState()
+ }
+
+ setNetworkState (network, type) {
+ if (network === 'loading') {
+ return this.networkStore.putState(network)
+ }
+
+ // type must be defined
+ if (!type) {
+ return
+ }
+ network = networks.networkList[type] && networks.networkList[type].chainId ? networks.networkList[type].chainId : network
return this.networkStore.putState(network)
}
@@ -85,18 +105,22 @@ module.exports = class NetworkController extends EventEmitter {
if (!this._provider) {
return log.warn('NetworkController - lookupNetwork aborted due to missing provider')
}
+ var { type } = this.providerStore.getState()
const ethQuery = new EthQuery(this._provider)
ethQuery.sendAsync({ method: 'net_version' }, (err, network) => {
if (err) return this.setNetworkState('loading')
log.info('web3.getNetwork returned ' + network)
- this.setNetworkState(network)
+ this.setNetworkState(network, type)
})
}
- setRpcTarget (rpcTarget) {
+ setRpcTarget (rpcTarget, chainId, ticker = 'ETH', nickname = '') {
const providerConfig = {
type: 'rpc',
rpcTarget,
+ chainId,
+ ticker,
+ nickname,
}
this.providerConfig = providerConfig
}
@@ -132,7 +156,7 @@ module.exports = class NetworkController extends EventEmitter {
}
_configureProvider (opts) {
- const { type, rpcTarget } = opts
+ const { type, rpcTarget, chainId, ticker, nickname } = opts
// infura type-based endpoints
const isInfura = INFURA_PROVIDER_TYPES.includes(type)
if (isInfura) {
@@ -142,7 +166,7 @@ module.exports = class NetworkController extends EventEmitter {
this._configureLocalhostProvider()
// url-based rpc endpoints
} else if (type === 'rpc') {
- this._configureStandardProvider({ rpcUrl: rpcTarget })
+ this._configureStandardProvider({ rpcUrl: rpcTarget, chainId, ticker, nickname })
} else {
throw new Error(`NetworkController - _configureProvider - unknown type "${type}"`)
}
@@ -152,6 +176,11 @@ module.exports = class NetworkController extends EventEmitter {
log.info('NetworkController - configureInfuraProvider', type)
const networkClient = createInfuraClient({ network: type })
this._setNetworkClient(networkClient)
+ // setup networkConfig
+ var settings = {
+ ticker: 'ETH',
+ }
+ this.networkConfig.putState(settings)
}
_configureLocalhostProvider () {
@@ -160,9 +189,22 @@ module.exports = class NetworkController extends EventEmitter {
this._setNetworkClient(networkClient)
}
- _configureStandardProvider ({ rpcUrl }) {
+ _configureStandardProvider ({ rpcUrl, chainId, ticker, nickname }) {
log.info('NetworkController - configureStandardProvider', rpcUrl)
const networkClient = createJsonRpcClient({ rpcUrl })
+ // hack to add a 'rpc' network with chainId
+ networks.networkList['rpc'] = {
+ chainId: chainId,
+ rpcUrl,
+ ticker: ticker || 'ETH',
+ nickname,
+ }
+ // setup networkConfig
+ var settings = {
+ network: chainId,
+ }
+ settings = extend(settings, networks.networkList['rpc'])
+ this.networkConfig.putState(settings)
this._setNetworkClient(networkClient)
}
diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js
index 20b13398c..ffb593b09 100644
--- a/app/scripts/controllers/preferences.js
+++ b/app/scripts/controllers/preferences.js
@@ -25,7 +25,7 @@ class PreferencesController {
*/
constructor (opts = {}) {
const initState = extend({
- frequentRpcList: [],
+ frequentRpcListDetail: [],
currentAccountTab: 'history',
accountTokens: {},
assetImages: {},
@@ -39,14 +39,14 @@ class PreferencesController {
seedWords: null,
forgottenPassword: false,
preferences: {
- useETHAsPrimaryCurrency: true,
+ useNativeCurrencyAsPrimaryCurrency: true,
},
}, opts.initState)
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
- this.showWatchAssetUi = opts.showWatchAssetUi
+ this.openPopup = opts.openPopup
this._subscribeProviderType()
}
// PUBLIC METHODS
@@ -392,19 +392,22 @@ class PreferencesController {
* Adds custom RPC url to state.
*
* @param {string} url The RPC url to add to frequentRpcList.
+ * @param {number} chainId Optional chainId of the selected network.
+ * @param {string} ticker Optional ticker symbol of the selected network.
+ * @param {string} nickname Optional nickname of the selected network.
* @returns {Promise<array>} Promise resolving to updated frequentRpcList.
*
*/
- addToFrequentRpcList (url) {
- const rpcList = this.getFrequentRpcList()
- const index = rpcList.findIndex((element) => { return element === url })
+ addToFrequentRpcList (url, chainId, ticker = 'ETH', nickname = '') {
+ const rpcList = this.getFrequentRpcListDetail()
+ const index = rpcList.findIndex((element) => { return element.rpcUrl === url })
if (index !== -1) {
rpcList.splice(index, 1)
}
if (url !== 'http://localhost:8545') {
- rpcList.push(url)
+ rpcList.push({ rpcUrl: url, chainId, ticker, nickname })
}
- this.store.updateState({ frequentRpcList: rpcList })
+ this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
}
@@ -416,23 +419,23 @@ class PreferencesController {
*
*/
removeFromFrequentRpcList (url) {
- const rpcList = this.getFrequentRpcList()
- const index = rpcList.findIndex((element) => { return element === url })
+ const rpcList = this.getFrequentRpcListDetail()
+ const index = rpcList.findIndex((element) => { return element.rpcUrl === url })
if (index !== -1) {
rpcList.splice(index, 1)
}
- this.store.updateState({ frequentRpcList: rpcList })
+ this.store.updateState({ frequentRpcListDetail: rpcList })
return Promise.resolve(rpcList)
}
/**
- * Getter for the `frequentRpcList` property.
+ * Getter for the `frequentRpcListDetail` property.
*
- * @returns {array<string>} An array of one or two rpc urls.
+ * @returns {array<array>} An array of rpc urls.
*
*/
- getFrequentRpcList () {
- return this.store.getState().frequentRpcList
+ getFrequentRpcListDetail () {
+ return this.store.getState().frequentRpcListDetail
}
/**
@@ -564,7 +567,7 @@ class PreferencesController {
}
const tokenOpts = { rawAddress, decimals, symbol, image }
this.addSuggestedERC20Asset(tokenOpts)
- return this.showWatchAssetUi().then(() => {
+ return this.openPopup().then(() => {
const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
return tokenAddresses.length > 0
})
@@ -580,8 +583,8 @@ class PreferencesController {
*/
_validateERC20AssetParams (opts) {
const { rawAddress, symbol, decimals } = opts
- if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`)
- if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`)
+ if (!rawAddress || !symbol || typeof decimals === 'undefined') throw new Error(`Cannot suggest token without address, symbol, and decimals`)
+ if (!(symbol.length < 7)) throw new Error(`Invalid symbol ${symbol} more than six characters`)
const numDecimals = parseInt(decimals, 10)
if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) {
throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`)
diff --git a/app/scripts/controllers/provider-approval.js b/app/scripts/controllers/provider-approval.js
new file mode 100644
index 000000000..d3b7f6dff
--- /dev/null
+++ b/app/scripts/controllers/provider-approval.js
@@ -0,0 +1,152 @@
+const ObservableStore = require('obs-store')
+
+/**
+ * A controller that services user-approved requests for a full Ethereum provider API
+ */
+class ProviderApprovalController {
+ /**
+ * Determines if caching is enabled
+ */
+ caching = true
+
+ /**
+ * Creates a ProviderApprovalController
+ *
+ * @param {Object} [config] - Options to configure controller
+ */
+ constructor ({ closePopup, keyringController, openPopup, platform, preferencesController, publicConfigStore } = {}) {
+ this.approvedOrigins = {}
+ this.closePopup = closePopup
+ this.keyringController = keyringController
+ this.openPopup = openPopup
+ this.platform = platform
+ this.preferencesController = preferencesController
+ this.publicConfigStore = publicConfigStore
+ this.store = new ObservableStore()
+
+ if (platform && platform.addMessageListener) {
+ platform.addMessageListener(({ action = '', force, origin, siteTitle, siteImage }) => {
+ switch (action) {
+ case 'init-provider-request':
+ this._handleProviderRequest(origin, siteTitle, siteImage, force)
+ break
+ case 'init-is-approved':
+ this._handleIsApproved(origin)
+ break
+ case 'init-is-unlocked':
+ this._handleIsUnlocked()
+ break
+ case 'init-privacy-request':
+ this._handlePrivacyRequest()
+ break
+ }
+ })
+ }
+ }
+
+ /**
+ * Called when a tab requests access to a full Ethereum provider API
+ *
+ * @param {string} origin - Origin of the window requesting full provider access
+ * @param {string} siteTitle - The title of the document requesting full provider access
+ * @param {string} siteImage - The icon of the window requesting full provider access
+ */
+ _handleProviderRequest (origin, siteTitle, siteImage, force) {
+ this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] })
+ const isUnlocked = this.keyringController.memStore.getState().isUnlocked
+ if (!force && this.approvedOrigins[origin] && this.caching && isUnlocked) {
+ this.approveProviderRequest(origin)
+ return
+ }
+ this.openPopup && this.openPopup()
+ }
+
+ /**
+ * Called by a tab to determine if an origin has been approved in the past
+ *
+ * @param {string} origin - Origin of the window
+ */
+ _handleIsApproved (origin) {
+ this.platform && this.platform.sendMessage({
+ action: 'answer-is-approved',
+ isApproved: this.approvedOrigins[origin] && this.caching,
+ caching: this.caching,
+ }, { active: true })
+ }
+
+ /**
+ * Called by a tab to determine if MetaMask is currently locked or unlocked
+ */
+ _handleIsUnlocked () {
+ const isUnlocked = this.keyringController.memStore.getState().isUnlocked
+ this.platform && this.platform.sendMessage({ action: 'answer-is-unlocked', isUnlocked }, { active: true })
+ }
+
+ /**
+ * Called to check privacy mode; if privacy mode is off, this will automatically enable the provider (legacy behavior)
+ */
+ _handlePrivacyRequest () {
+ const privacyMode = this.preferencesController.getFeatureFlags().privacyMode
+ if (!privacyMode) {
+ this.platform && this.platform.sendMessage({ action: 'approve-provider-request' }, { active: true })
+ this.publicConfigStore.emit('update', this.publicConfigStore.getState())
+ }
+ }
+
+ /**
+ * Called when a user approves access to a full Ethereum provider API
+ *
+ * @param {string} origin - Origin of the target window to approve provider access
+ */
+ approveProviderRequest (origin) {
+ this.closePopup && this.closePopup()
+ const requests = this.store.getState().providerRequests || []
+ this.platform && this.platform.sendMessage({ action: 'approve-provider-request' }, { active: true })
+ this.publicConfigStore.emit('update', this.publicConfigStore.getState())
+ const providerRequests = requests.filter(request => request.origin !== origin)
+ this.store.updateState({ providerRequests })
+ this.approvedOrigins[origin] = true
+ }
+
+ /**
+ * Called when a tab rejects access to a full Ethereum provider API
+ *
+ * @param {string} origin - Origin of the target window to reject provider access
+ */
+ rejectProviderRequest (origin) {
+ this.closePopup && this.closePopup()
+ const requests = this.store.getState().providerRequests || []
+ this.platform && this.platform.sendMessage({ action: 'reject-provider-request' }, { active: true })
+ const providerRequests = requests.filter(request => request.origin !== origin)
+ this.store.updateState({ providerRequests })
+ delete this.approvedOrigins[origin]
+ }
+
+ /**
+ * Clears any cached approvals for user-approved origins
+ */
+ clearApprovedOrigins () {
+ this.approvedOrigins = {}
+ }
+
+ /**
+ * Determines if a given origin should have accounts exposed
+ *
+ * @param {string} origin - Domain origin to check for approval status
+ * @returns {boolean} - True if the origin has been approved
+ */
+ shouldExposeAccounts (origin) {
+ const privacyMode = this.preferencesController.getFeatureFlags().privacyMode
+ return !privacyMode || this.approvedOrigins[origin]
+ }
+
+ /**
+ * Tells all tabs that MetaMask is now locked. This is primarily used to set
+ * internal flags in the contentscript and inpage script.
+ */
+ setLocked () {
+ this.platform.sendMessage({ action: 'metamask-set-locked' })
+ }
+}
+
+module.exports = ProviderApprovalController
diff --git a/app/scripts/controllers/token-rates.js b/app/scripts/controllers/token-rates.js
index 87d716aa6..b6f084841 100644
--- a/app/scripts/controllers/token-rates.js
+++ b/app/scripts/controllers/token-rates.js
@@ -1,5 +1,5 @@
const ObservableStore = require('obs-store')
-const { warn } = require('loglevel')
+const log = require('loglevel')
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = 180 * 1000
@@ -26,8 +26,11 @@ class TokenRatesController {
async updateExchangeRates () {
if (!this.isActive) { return }
const contractExchangeRates = {}
- for (const i in this._tokens) {
- const address = this._tokens[i].address
+ // copy array to ensure its not modified during iteration
+ const tokens = this._tokens.slice()
+ for (const token of tokens) {
+ if (!token) return log.error(`TokenRatesController - invalid tokens state:\n${JSON.stringify(tokens, null, 2)}`)
+ const address = token.address
contractExchangeRates[address] = await this.fetchExchangeRate(address)
}
this.store.putState({ contractExchangeRates })
@@ -44,7 +47,7 @@ class TokenRatesController {
const json = await response.json()
return json && json.length ? json[0].averagePrice : 0
} catch (error) {
- warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error)
+ log.warn(`MetaMask - TokenRatesController exchange rate fetch failed for ${address}.`, error)
return 0
}
}
diff --git a/app/scripts/controllers/transactions/tx-gas-utils.js b/app/scripts/controllers/transactions/tx-gas-utils.js
index 3dd45507f..def67c2c3 100644
--- a/app/scripts/controllers/transactions/tx-gas-utils.js
+++ b/app/scripts/controllers/transactions/tx-gas-utils.js
@@ -7,6 +7,8 @@ const {
const { addHexPrefix } = require('ethereumjs-util')
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
+import { TRANSACTION_NO_CONTRACT_ERROR_KEY } from '../../../../ui/app/constants/error-keys'
+
/**
tx-gas-utils are gas utility methods for Transaction manager
its passed ethquery
@@ -32,6 +34,7 @@ class TxGasUtil {
} catch (err) {
txMeta.simulationFails = {
reason: err.message,
+ errorKey: err.errorKey,
}
return txMeta
}
@@ -56,24 +59,38 @@ class TxGasUtil {
return txParams.gas
}
- // if recipient has no code, gas is 21k max:
const recipient = txParams.to
const hasRecipient = Boolean(recipient)
- let code
- if (recipient) 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
+ // see if we can set the gas based on the recipient
+ if (hasRecipient) {
+ const code = await this.query.getCode(recipient)
+ // For an address with no code, geth will return '0x', and ganache-core v2.2.1 will return '0x0'
+ const codeIsEmpty = !code || code === '0x' || code === '0x0'
+
+ if (codeIsEmpty) {
+ // if there's data in the params, but there's no contract code, it's not a valid transaction
+ if (txParams.data) {
+ const err = new Error('TxGasUtil - Trying to call a function on a non-contract address')
+ // set error key so ui can display localized error message
+ err.errorKey = TRANSACTION_NO_CONTRACT_ERROR_KEY
+ throw err
+ }
+
+ // This is a standard ether simple send, gas requirement is exactly 21k
+ txParams.gas = SIMPLE_GAS_COST
+ // prevents buffer addition
+ txMeta.simpleSend = true
+ return SIMPLE_GAS_COST
+ }
}
- // if not, fall back to block gasLimit
+ // fallback to block gasLimit
const blockGasLimitBN = hexToBn(blockGasLimitHex)
const saferGasLimitBN = BnMultiplyByFraction(blockGasLimitBN, 19, 20)
txParams.gas = bnToHex(saferGasLimitBN)
- // run tx
+ // estimate tx gas requirements
return await this.query.estimateGas(txParams)
}
diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js
index b885a7e05..327e25042 100644
--- a/app/scripts/inpage.js
+++ b/app/scripts/inpage.js
@@ -6,14 +6,21 @@ const LocalMessageDuplexStream = require('post-message-stream')
const setupDappAutoReload = require('./lib/auto-reload.js')
const MetamaskInpageProvider = require('metamask-inpage-provider')
+let isEnabled = false
+let warned = false
+let providerHandle
+let isApprovedHandle
+let isUnlockedHandle
+
restoreContextAfterImports()
log.setDefaultLevel(process.env.METAMASK_DEBUG ? 'debug' : 'warn')
-console.warn('ATTENTION: In an effort to improve user privacy, MetaMask will ' +
-'stop exposing user accounts to dapps by default beginning November 2nd, 2018. ' +
-'Dapps should call provider.enable() in order to view and use accounts. Please see ' +
-'https://bit.ly/2QQHXvF for complete information and up-to-date example code.')
+console.warn('ATTENTION: In an effort to improve user privacy, MetaMask ' +
+'stopped exposing user accounts to dapps if "privacy mode" is enabled on ' +
+'November 2nd, 2018. Dapps should now call provider.enable() in order to view and use ' +
+'accounts. Please see https://bit.ly/2QQHXvF for complete information and up-to-date ' +
+'example code.')
//
// setup plugin communication
@@ -27,26 +34,125 @@ var metamaskStream = new LocalMessageDuplexStream({
// compose the inpage provider
var inpageProvider = new MetamaskInpageProvider(metamaskStream)
+
// set a high max listener count to avoid unnecesary warnings
inpageProvider.setMaxListeners(100)
-// Augment the provider with its enable method
-inpageProvider.enable = function (options = {}) {
+// set up a listener for when MetaMask is locked
+window.addEventListener('metamasksetlocked', () => {
+ isEnabled = false
+})
+
+// augment the provider with its enable method
+inpageProvider.enable = function ({ force } = {}) {
return new Promise((resolve, reject) => {
- if (options.mockRejection) {
- reject('User rejected account access')
- } else {
- inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
- if (error) {
- reject(error)
- } else {
- resolve(response.result)
- }
- })
+ window.removeEventListener('ethereumprovider', providerHandle)
+ providerHandle = ({ detail }) => {
+ if (typeof detail.error !== 'undefined') {
+ reject(detail.error)
+ } else {
+ // wait for the publicConfig store to populate with an account
+ const publicConfig = new Promise((resolve) => {
+ const { selectedAddress } = inpageProvider.publicConfigStore.getState()
+ inpageProvider._metamask.isUnlocked().then(unlocked => {
+ if (!unlocked || selectedAddress) {
+ resolve()
+ } else {
+ inpageProvider.publicConfigStore.on('update', ({ selectedAddress }) => {
+ selectedAddress && resolve()
+ })
+ }
+ })
+ })
+
+ // wait for the background to update with an account
+ const ethAccounts = new Promise((resolveAccounts, rejectAccounts) => {
+ inpageProvider.sendAsync({ method: 'eth_accounts', params: [] }, (error, response) => {
+ if (error) {
+ rejectAccounts(error)
+ } else {
+ resolveAccounts(response.result)
+ }
+ })
+ })
+
+ Promise.all([ethAccounts, publicConfig])
+ .then(([selectedAddress]) => {
+ isEnabled = true
+ resolve(selectedAddress)
+ })
+ .catch(reject)
+ }
}
+ window.addEventListener('ethereumprovider', providerHandle)
+ window.postMessage({ type: 'ETHEREUM_ENABLE_PROVIDER', force }, '*')
})
}
+// add metamask-specific convenience methods
+inpageProvider._metamask = new Proxy({
+ /**
+ * Determines if this domain is currently enabled
+ *
+ * @returns {boolean} - true if this domain is currently enabled
+ */
+ isEnabled: function () {
+ return isEnabled
+ },
+
+ /**
+ * Determines if this domain has been previously approved
+ *
+ * @returns {Promise<boolean>} - Promise resolving to true if this domain has been previously approved
+ */
+ isApproved: function() {
+ return new Promise((resolve, reject) => {
+ window.removeEventListener('ethereumisapproved', isApprovedHandle)
+ isApprovedHandle = ({ detail }) => {
+ if (typeof detail.error !== 'undefined') {
+ reject(detail.error)
+ } else {
+ if (detail.caching) {
+ resolve(!!detail.isApproved)
+ } else {
+ resolve(false)
+ }
+ }
+ }
+ window.addEventListener('ethereumisapproved', isApprovedHandle)
+ window.postMessage({ type: 'ETHEREUM_IS_APPROVED' }, '*')
+ })
+ },
+
+ /**
+ * Determines if MetaMask is unlocked by the user
+ *
+ * @returns {Promise<boolean>} - Promise resolving to true if MetaMask is currently unlocked
+ */
+ isUnlocked: function () {
+ return new Promise((resolve, reject) => {
+ window.removeEventListener('metamaskisunlocked', isUnlockedHandle)
+ isUnlockedHandle = ({ detail }) => {
+ if (typeof detail.error !== 'undefined') {
+ reject(detail.error)
+ } else {
+ resolve(!!detail.isUnlocked)
+ }
+ }
+ window.addEventListener('metamaskisunlocked', isUnlockedHandle)
+ window.postMessage({ type: 'METAMASK_IS_UNLOCKED' }, '*')
+ })
+ },
+}, {
+ get: function(obj, prop) {
+ !warned && console.warn('Heads up! ethereum._metamask exposes methods that have ' +
+ 'not been standardized yet. This means that these methods may not be implemented ' +
+ 'in other dapp browsers and may be removed from MetaMask in the future.')
+ warned = true
+ return obj[prop]
+ },
+})
+
// Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound
// `sendAsync` method on the prototype, causing `this` reference issues with drizzle
const proxiedInpageProvider = new Proxy(inpageProvider, {
@@ -57,6 +163,19 @@ const proxiedInpageProvider = new Proxy(inpageProvider, {
window.ethereum = proxiedInpageProvider
+// detect eth_requestAccounts and pipe to enable for now
+function detectAccountRequest(method) {
+ const originalMethod = inpageProvider[method]
+ inpageProvider[method] = function ({ method }) {
+ if (method === 'eth_requestAccounts') {
+ return window.ethereum.enable()
+ }
+ return originalMethod.apply(this, arguments)
+ }
+}
+detectAccountRequest('send')
+detectAccountRequest('sendAsync')
+
//
// setup web3
//
diff --git a/app/scripts/lib/contracts/registrar.js b/app/scripts/lib/ens-ipfs/contracts/registrar.js
index 99ca24458..99ca24458 100644
--- a/app/scripts/lib/contracts/registrar.js
+++ b/app/scripts/lib/ens-ipfs/contracts/registrar.js
diff --git a/app/scripts/lib/contracts/resolver.js b/app/scripts/lib/ens-ipfs/contracts/resolver.js
index 1bf3f90ce..1bf3f90ce 100644
--- a/app/scripts/lib/contracts/resolver.js
+++ b/app/scripts/lib/ens-ipfs/contracts/resolver.js
diff --git a/app/scripts/lib/ens-ipfs/resolver.js b/app/scripts/lib/ens-ipfs/resolver.js
new file mode 100644
index 000000000..fe2dc1134
--- /dev/null
+++ b/app/scripts/lib/ens-ipfs/resolver.js
@@ -0,0 +1,54 @@
+const namehash = require('eth-ens-namehash')
+const multihash = require('multihashes')
+const Eth = require('ethjs-query')
+const EthContract = require('ethjs-contract')
+const registrarAbi = require('./contracts/registrar')
+const resolverAbi = require('./contracts/resolver')
+
+module.exports = resolveEnsToIpfsContentId
+
+
+async function resolveEnsToIpfsContentId ({ provider, name }) {
+ const eth = new Eth(provider)
+ const hash = namehash.hash(name)
+ const contract = new EthContract(eth)
+ // lookup registrar
+ const chainId = Number.parseInt(await eth.net_version(), 10)
+ const registrarAddress = getRegistrarForChainId(chainId)
+ if (!registrarAddress) {
+ throw new Error(`EnsIpfsResolver - no known ens-ipfs registrar for chainId "${chainId}"`)
+ }
+ const Registrar = contract(registrarAbi).at(registrarAddress)
+ // lookup resolver
+ const resolverLookupResult = await Registrar.resolver(hash)
+ const resolverAddress = resolverLookupResult[0]
+ if (hexValueIsEmpty(resolverAddress)) {
+ throw new Error(`EnsIpfsResolver - no resolver found for name "${name}"`)
+ }
+ const Resolver = contract(resolverAbi).at(resolverAddress)
+ // lookup content id
+ const contentLookupResult = await Resolver.content(hash)
+ const contentHash = contentLookupResult[0]
+ if (hexValueIsEmpty(contentHash)) {
+ throw new Error(`EnsIpfsResolver - no content ID found for name "${name}"`)
+ }
+ const nonPrefixedHex = contentHash.slice(2)
+ const buffer = multihash.fromHexString(nonPrefixedHex)
+ const contentId = multihash.toB58String(multihash.encode(buffer, 'sha2-256'))
+ return contentId
+}
+
+function hexValueIsEmpty(value) {
+ return [undefined, null, '0x', '0x0', '0x0000000000000000000000000000000000000000000000000000000000000000'].includes(value)
+}
+
+function getRegistrarForChainId (chainId) {
+ switch (chainId) {
+ // mainnet
+ case 1:
+ return '0x314159265dd8dbb310642f98f50c066173c1259b'
+ // ropsten
+ case 3:
+ return '0x112234455c3a32fd11230c42e7bccd4a84e02010'
+ }
+}
diff --git a/app/scripts/lib/ens-ipfs/setup.js b/app/scripts/lib/ens-ipfs/setup.js
new file mode 100644
index 000000000..45eb1ce14
--- /dev/null
+++ b/app/scripts/lib/ens-ipfs/setup.js
@@ -0,0 +1,63 @@
+const urlUtil = require('url')
+const extension = require('extensionizer')
+const resolveEnsToIpfsContentId = require('./resolver.js')
+
+const supportedTopLevelDomains = ['eth']
+
+module.exports = setupEnsIpfsResolver
+
+function setupEnsIpfsResolver({ provider }) {
+
+ // install listener
+ const urlPatterns = supportedTopLevelDomains.map(tld => `*://*.${tld}/*`)
+ extension.webRequest.onErrorOccurred.addListener(webRequestDidFail, { urls: urlPatterns })
+
+ // return api object
+ return {
+ // uninstall listener
+ remove () {
+ extension.webRequest.onErrorOccurred.removeListener(webRequestDidFail)
+ },
+ }
+
+ async function webRequestDidFail (details) {
+ const { tabId, url } = details
+ // ignore requests that are not associated with tabs
+ if (tabId === -1) return
+ // parse ens name
+ const urlData = urlUtil.parse(url)
+ const { hostname: name, path, search } = urlData
+ const domainParts = name.split('.')
+ const topLevelDomain = domainParts[domainParts.length - 1]
+ // if unsupported TLD, abort
+ if (!supportedTopLevelDomains.includes(topLevelDomain)) return
+ // otherwise attempt resolve
+ attemptResolve({ tabId, name, path, search })
+ }
+
+ async function attemptResolve({ tabId, name, path, search }) {
+ extension.tabs.update(tabId, { url: `loading.html` })
+ try {
+ const ipfsContentId = await resolveEnsToIpfsContentId({ provider, name })
+ let url = `https://gateway.ipfs.io/ipfs/${ipfsContentId}${path}${search || ''}`
+ try {
+ // check if ipfs gateway has result
+ const response = await fetch(url, { method: 'HEAD' })
+ // if failure, redirect to 404 page
+ if (response.status !== 200) {
+ extension.tabs.update(tabId, { url: '404.html' })
+ return
+ }
+ // otherwise redirect to the correct page
+ extension.tabs.update(tabId, { url })
+ } catch (err) {
+ console.warn(err)
+ // if HEAD fetch failed, redirect so user can see relevant error page
+ extension.tabs.update(tabId, { url })
+ }
+ } catch (err) {
+ console.warn(err)
+ extension.tabs.update(tabId, { url: `error.html?name=${name}` })
+ }
+ }
+}
diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js
deleted file mode 100644
index 8b08453c4..000000000
--- a/app/scripts/lib/ipfsContent.js
+++ /dev/null
@@ -1,46 +0,0 @@
-const extension = require('extensionizer')
-const resolver = require('./resolver.js')
-
-module.exports = function (provider) {
- function ipfsContent (details) {
- const name = details.url.substring(7, details.url.length - 1)
- let clearTime = null
- if (/^.+\.eth$/.test(name) === false) return
-
- extension.tabs.query({active: true}, tab => {
- extension.tabs.update(tab.id, { url: 'loading.html' })
-
- clearTime = setTimeout(() => {
- return extension.tabs.update(tab.id, { url: '404.html' })
- }, 60000)
-
- resolver.resolve(name, provider).then(ipfsHash => {
- clearTimeout(clearTime)
- let url = 'https://ipfs.infura.io/ipfs/' + ipfsHash
- return fetch(url, { method: 'HEAD' }).then(response => response.status).then(statusCode => {
- if (statusCode !== 200) return extension.tabs.update(tab.id, { url: '404.html' })
- extension.tabs.update(tab.id, { url: url })
- })
- .catch(err => {
- url = 'https://ipfs.infura.io/ipfs/' + ipfsHash
- extension.tabs.update(tab.id, {url: url})
- return err
- })
- })
- .catch(err => {
- clearTimeout(clearTime)
- const url = err === 'unsupport' ? 'unsupport' : 'error'
- extension.tabs.update(tab.id, {url: `${url}.html?name=${name}`})
- })
- })
- return { cancel: true }
- }
-
- extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/'], types: ['main_frame']})
-
- return {
- remove () {
- extension.webRequest.onErrorOccurred.removeListener(ipfsContent)
- },
- }
-}
diff --git a/app/scripts/lib/reportFailedTxToSentry.js b/app/scripts/lib/reportFailedTxToSentry.js
index df5661e59..de4d57145 100644
--- a/app/scripts/lib/reportFailedTxToSentry.js
+++ b/app/scripts/lib/reportFailedTxToSentry.js
@@ -7,10 +7,10 @@ module.exports = reportFailedTxToSentry
// for sending to sentry
//
-function reportFailedTxToSentry ({ raven, txMeta }) {
+function reportFailedTxToSentry ({ sentry, txMeta }) {
const errorMessage = 'Transaction Failed: ' + extractEthjsErrorMessage(txMeta.err.message)
- raven.captureMessage(errorMessage, {
+ sentry.captureMessage(errorMessage, {
// "extra" key is required by Sentry
- extra: txMeta,
+ extra: { txMeta },
})
}
diff --git a/app/scripts/lib/resolver.js b/app/scripts/lib/resolver.js
deleted file mode 100644
index ff0fed161..000000000
--- a/app/scripts/lib/resolver.js
+++ /dev/null
@@ -1,71 +0,0 @@
-const namehash = require('eth-ens-namehash')
-const multihash = require('multihashes')
-const HttpProvider = require('ethjs-provider-http')
-const Eth = require('ethjs-query')
-const EthContract = require('ethjs-contract')
-const registrarAbi = require('./contracts/registrar')
-const resolverAbi = require('./contracts/resolver')
-
-function ens (name, provider) {
- const eth = new Eth(new HttpProvider(getProvider(provider.type)))
- const hash = namehash.hash(name)
- const contract = new EthContract(eth)
- const Registrar = contract(registrarAbi).at(getRegistrar(provider.type))
- return new Promise((resolve, reject) => {
- if (provider.type === 'mainnet' || provider.type === 'ropsten') {
- Registrar.resolver(hash).then((address) => {
- if (address === '0x0000000000000000000000000000000000000000') {
- reject(null)
- } else {
- const Resolver = contract(resolverAbi).at(address['0'])
- return Resolver.content(hash)
- }
- }).then((contentHash) => {
- if (contentHash['0'] === '0x0000000000000000000000000000000000000000000000000000000000000000') reject(null)
- if (contentHash.ret !== '0x') {
- const hex = contentHash['0'].substring(2)
- const buf = multihash.fromHexString(hex)
- resolve(multihash.toB58String(multihash.encode(buf, 'sha2-256')))
- } else {
- reject(null)
- }
- })
- } else {
- return reject('unsupport')
- }
- })
-}
-
-function getProvider (type) {
- switch (type) {
- case 'mainnet':
- return 'https://mainnet.infura.io/'
- case 'ropsten':
- return 'https://ropsten.infura.io/'
- default:
- return 'http://localhost:8545/'
- }
-}
-
-function getRegistrar (type) {
- switch (type) {
- case 'mainnet':
- return '0x314159265dd8dbb310642f98f50c066173c1259b'
- case 'ropsten':
- return '0x112234455c3a32fd11230c42e7bccd4a84e02010'
- default:
- return '0x0000000000000000000000000000000000000000'
- }
-}
-
-module.exports.resolve = function (name, provider) {
- const path = name.split('.')
- const topLevelDomain = path[path.length - 1]
- if (topLevelDomain === 'eth' || topLevelDomain === 'test') {
- return ens(name, provider)
- } else {
- return new Promise((resolve, reject) => {
- reject(null)
- })
- }
-}
diff --git a/app/scripts/lib/setupFetchDebugging.js b/app/scripts/lib/setupFetchDebugging.js
index dd87b65a6..c1ef22d21 100644
--- a/app/scripts/lib/setupFetchDebugging.js
+++ b/app/scripts/lib/setupFetchDebugging.js
@@ -2,7 +2,7 @@ module.exports = setupFetchDebugging
//
// This is a utility to help resolve cases where `window.fetch` throws a
-// `TypeError: Failed to Fetch` without any stack or context for the request
+// `TypeError: Failed to Fetch` without any stack or context for the request
// https://github.com/getsentry/sentry-javascript/pull/1293
//
@@ -17,9 +17,11 @@ function setupFetchDebugging() {
try {
return await originalFetch.call(window, ...args)
} catch (err) {
- console.warn('FetchDebugger - fetch encountered an Error', err)
- console.warn('FetchDebugger - overriding stack to point of original call')
- err.stack = initialStack
+ if (!err.stack) {
+ console.warn('FetchDebugger - fetch encountered an Error without a stack', err)
+ console.warn('FetchDebugger - overriding stack to point of original call')
+ err.stack = initialStack
+ }
throw err
}
}
diff --git a/app/scripts/lib/setupRaven.js b/app/scripts/lib/setupSentry.js
index e6e511640..69042bc19 100644
--- a/app/scripts/lib/setupRaven.js
+++ b/app/scripts/lib/setupSentry.js
@@ -1,58 +1,55 @@
-const Raven = require('raven-js')
+const Sentry = require('@sentry/browser')
const METAMASK_DEBUG = process.env.METAMASK_DEBUG
const extractEthjsErrorMessage = require('./extractEthjsErrorMessage')
-const PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
-const DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
+const SENTRY_DSN_PROD = 'https://3567c198f8a8412082d32655da2961d0@sentry.io/273505'
+const SENTRY_DSN_DEV = 'https://f59f3dd640d2429d9d0e2445a87ea8e1@sentry.io/273496'
-module.exports = setupRaven
+module.exports = setupSentry
-// Setup raven / sentry remote error reporting
-function setupRaven (opts) {
- const { release } = opts
- let ravenTarget
+// Setup sentry remote error reporting
+function setupSentry (opts) {
+ const { release, getState } = opts
+ let sentryTarget
// detect brave
const isBrave = Boolean(window.chrome.ipcRenderer)
if (METAMASK_DEBUG) {
- console.log('Setting up Sentry Remote Error Reporting: DEV')
- ravenTarget = DEV
+ console.log('Setting up Sentry Remote Error Reporting: SENTRY_DSN_DEV')
+ sentryTarget = SENTRY_DSN_DEV
} else {
- console.log('Setting up Sentry Remote Error Reporting: PROD')
- ravenTarget = PROD
+ console.log('Setting up Sentry Remote Error Reporting: SENTRY_DSN_PROD')
+ sentryTarget = SENTRY_DSN_PROD
}
- const client = Raven.config(ravenTarget, {
+ Sentry.init({
+ dsn: sentryTarget,
+ debug: METAMASK_DEBUG,
release,
- transport: function (opts) {
- opts.data.extra.isBrave = isBrave
- const report = opts.data
+ beforeSend: (report) => rewriteReport(report),
+ })
- try {
- // handle error-like non-error exceptions
- rewriteErrorLikeExceptions(report)
- // simplify certain complex error messages (e.g. Ethjs)
- simplifyErrorMessages(report)
- // modify report urls
- rewriteReportUrls(report)
- } catch (err) {
- console.warn(err)
- }
- // make request normally
- client._makeRequest(opts)
- },
+ Sentry.configureScope(scope => {
+ scope.setExtra('isBrave', isBrave)
})
- client.install()
- return Raven
-}
+ function rewriteReport(report) {
+ try {
+ // simplify certain complex error messages (e.g. Ethjs)
+ simplifyErrorMessages(report)
+ // modify report urls
+ rewriteReportUrls(report)
+ // append app state
+ if (getState) {
+ const appState = getState()
+ report.extra.appState = appState
+ }
+ } catch (err) {
+ console.warn(err)
+ }
+ return report
+ }
-function rewriteErrorLikeExceptions (report) {
- // handle errors that lost their error-ness in serialization (e.g. dnode)
- rewriteErrorMessages(report, (errorMessage) => {
- if (!errorMessage.includes('Non-Error exception captured with keys:')) return errorMessage
- if (!(report.extra && report.extra.__serialized__ && report.extra.__serialized__.message)) return errorMessage
- return `Non-Error Exception: ${report.extra.__serialized__.message}`
- })
+ return Sentry
}
function simplifyErrorMessages (report) {
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 7913662d4..5ae0f608d 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -37,6 +37,7 @@ const TransactionController = require('./controllers/transactions')
const BalancesController = require('./controllers/computed-balances')
const TokenRatesController = require('./controllers/token-rates')
const DetectTokensController = require('./controllers/detect-tokens')
+const ProviderApprovalController = require('./controllers/provider-approval')
const nodeify = require('./lib/nodeify')
const accountImporter = require('./account-import-strategies')
const getBuyEthUrl = require('./lib/buy-eth-url')
@@ -89,7 +90,7 @@ module.exports = class MetamaskController extends EventEmitter {
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
- showWatchAssetUi: opts.showWatchAssetUi,
+ openPopup: opts.openPopup,
network: this.networkController,
})
@@ -138,12 +139,12 @@ module.exports = class MetamaskController extends EventEmitter {
this.accountTracker.stop()
}
})
-
+
// ensure accountTracker updates balances after network change
this.networkController.on('networkDidChange', () => {
this.accountTracker._updateAccounts()
})
-
+
// key mgmt
const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring]
this.keyringController = new KeyringController({
@@ -197,6 +198,8 @@ module.exports = class MetamaskController extends EventEmitter {
})
this.networkController.on('networkDidChange', () => {
this.balancesController.updateAllBalances()
+ var currentCurrency = this.currencyController.getCurrentCurrency()
+ this.setCurrentCurrency(currentCurrency, function() {})
})
this.balancesController.updateAllBalances()
@@ -217,6 +220,15 @@ module.exports = class MetamaskController extends EventEmitter {
this.typedMessageManager = new TypedMessageManager({ networkController: this.networkController })
this.publicConfigStore = this.initPublicConfigStore()
+ this.providerApprovalController = new ProviderApprovalController({
+ closePopup: opts.closePopup,
+ keyringController: this.keyringController,
+ openPopup: opts.openPopup,
+ platform: opts.platform,
+ preferencesController: this.preferencesController,
+ publicConfigStore: this.publicConfigStore,
+ })
+
this.store.updateStructure({
TransactionController: this.txController.store,
KeyringController: this.keyringController.store,
@@ -246,6 +258,7 @@ module.exports = class MetamaskController extends EventEmitter {
NoticeController: this.noticeController.memStore,
ShapeshiftController: this.shapeshiftController.store,
InfuraController: this.infuraController.store,
+ ProviderApprovalController: this.providerApprovalController.store,
})
this.memStore.subscribe(this.sendUpdate.bind(this))
}
@@ -261,7 +274,11 @@ module.exports = class MetamaskController extends EventEmitter {
},
version,
// account mgmt
- getAccounts: async () => {
+ getAccounts: async ({ origin }) => {
+ // Expose no accounts if this origin has not been approved, preventing
+ // account-requring RPC methods from completing successfully
+ const exposeAccounts = this.providerApprovalController.shouldExposeAccounts(origin)
+ if (origin !== 'MetaMask' && !exposeAccounts) { return [] }
const isUnlocked = this.keyringController.memStore.getState().isUnlocked
const selectedAddress = this.preferencesController.getSelectedAddress()
// only show address if account is unlocked
@@ -275,6 +292,8 @@ module.exports = class MetamaskController extends EventEmitter {
processTransaction: this.newUnapprovedTransaction.bind(this),
// msg signing
processEthSignMessage: this.newUnsignedMessage.bind(this),
+ processTypedMessage: this.newUnsignedTypedMessage.bind(this),
+ processTypedMessageV3: this.newUnsignedTypedMessage.bind(this),
processPersonalMessage: this.newUnsignedPersonalMessage.bind(this),
getPendingNonce: this.getPendingNonce.bind(this),
}
@@ -345,6 +364,7 @@ module.exports = class MetamaskController extends EventEmitter {
const noticeController = this.noticeController
const addressBookController = this.addressBookController
const networkController = this.networkController
+ const providerApprovalController = this.providerApprovalController
return {
// etc
@@ -402,7 +422,7 @@ module.exports = class MetamaskController extends EventEmitter {
setAddressBook: nodeify(addressBookController.setAddressBook, addressBookController),
// KeyringController
- setLocked: nodeify(keyringController.setLocked, keyringController),
+ setLocked: nodeify(this.setLocked, this),
createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this),
createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this),
addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController),
@@ -433,6 +453,10 @@ module.exports = class MetamaskController extends EventEmitter {
// notices
checkNotices: noticeController.updateNoticesList.bind(noticeController),
markNoticeRead: noticeController.markNoticeRead.bind(noticeController),
+
+ approveProviderRequest: providerApprovalController.approveProviderRequest.bind(providerApprovalController),
+ clearApprovedOrigins: providerApprovalController.clearApprovedOrigins.bind(providerApprovalController),
+ rejectProviderRequest: providerApprovalController.rejectProviderRequest.bind(providerApprovalController),
}
}
@@ -978,8 +1002,8 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Object} msgParams - The params passed to eth_signTypedData.
* @param {Function} cb - The callback function, called with the signature.
*/
- newUnsignedTypedMessage (msgParams, req) {
- const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req)
+ newUnsignedTypedMessage (msgParams, req, version) {
+ const promise = this.typedMessageManager.addUnapprovedMessageAsync(msgParams, req, version)
this.sendUpdate()
this.opts.showUnconfirmedMessage()
return promise
@@ -1273,10 +1297,6 @@ module.exports = class MetamaskController extends EventEmitter {
engine.push(subscriptionManager.middleware)
// watch asset
engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController))
- // sign typed data middleware
- engine.push(this.createTypedDataMiddleware('eth_signTypedData', 'V1').bind(this))
- engine.push(this.createTypedDataMiddleware('eth_signTypedData_v1', 'V1').bind(this))
- engine.push(this.createTypedDataMiddleware('eth_signTypedData_v3', 'V3', true).bind(this))
// forward to metamask primary provider
engine.push(createProviderMiddleware({ provider }))
@@ -1412,10 +1432,13 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} cb - A callback function returning currency info.
*/
setCurrentCurrency (currencyCode, cb) {
+ const { ticker } = this.networkController.getNetworkConfig()
try {
+ this.currencyController.setNativeCurrency(ticker)
this.currencyController.setCurrentCurrency(currencyCode)
this.currencyController.updateConversionRate()
const data = {
+ nativeCurrency: ticker || 'ETH',
conversionRate: this.currencyController.getConversionRate(),
currentCurrency: this.currencyController.getCurrentCurrency(),
conversionDate: this.currencyController.getConversionDate(),
@@ -1454,11 +1477,14 @@ module.exports = class MetamaskController extends EventEmitter {
/**
* A method for selecting a custom URL for an ethereum RPC provider.
* @param {string} rpcTarget - A URL for a valid Ethereum RPC API.
+ * @param {number} chainId - The chainId of the selected network.
+ * @param {string} ticker - The ticker symbol of the selected network.
+ * @param {string} nickname - Optional nickname of the selected network.
* @returns {Promise<String>} - The RPC Target URL confirmed.
*/
- async setCustomRpc (rpcTarget) {
- this.networkController.setRpcTarget(rpcTarget)
- await this.preferencesController.addToFrequentRpcList(rpcTarget)
+ async setCustomRpc (rpcTarget, chainId, ticker = 'ETH', nickname = '') {
+ this.networkController.setRpcTarget(rpcTarget, chainId, ticker, nickname)
+ await this.preferencesController.addToFrequentRpcList(rpcTarget, chainId, ticker, nickname)
return rpcTarget
}
@@ -1542,27 +1568,6 @@ module.exports = class MetamaskController extends EventEmitter {
* @param {Function} - next
* @param {Function} - end
*/
- createTypedDataMiddleware (methodName, version, reverse) {
- return async (req, res, next, end) => {
- const { method, params } = req
- if (method === methodName) {
- const promise = this.typedMessageManager.addUnapprovedMessageAsync({
- data: reverse ? params[1] : params[0],
- from: reverse ? params[0] : params[1],
- }, req, version)
- this.sendUpdate()
- this.opts.showUnconfirmedMessage()
- try {
- res.result = await promise
- end()
- } catch (error) {
- end(error)
- }
- } else {
- next()
- }
- }
- }
/**
* Adds a domain to the {@link BlacklistController} whitelist
@@ -1571,4 +1576,12 @@ module.exports = class MetamaskController extends EventEmitter {
whitelistPhishingDomain (hostname) {
return this.blacklistController.whitelistDomain(hostname)
}
+
+ /**
+ * Locks MetaMask
+ */
+ setLocked() {
+ this.providerApprovalController.setLocked()
+ return this.keyringController.setLocked()
+ }
}
diff --git a/app/scripts/platforms/extension.js b/app/scripts/platforms/extension.js
index 71b162dd0..9ef0d22c9 100644
--- a/app/scripts/platforms/extension.js
+++ b/app/scripts/platforms/extension.js
@@ -57,6 +57,18 @@ class ExtensionPlatform {
}
}
+ addMessageListener (cb) {
+ extension.runtime.onMessage.addListener(cb)
+ }
+
+ sendMessage (message, query = {}) {
+ extension.tabs.query(query, tabs => {
+ tabs.forEach(tab => {
+ extension.tabs.sendMessage(tab.id, message)
+ })
+ })
+ }
+
_showConfirmedTransaction (txMeta) {
this._subscribeToNotificationClicked()
diff --git a/app/scripts/ui.js b/app/scripts/ui.js
index 98a036338..c4f6615db 100644
--- a/app/scripts/ui.js
+++ b/app/scripts/ui.js
@@ -9,7 +9,7 @@ const extension = require('extensionizer')
const ExtensionPlatform = require('./platforms/extension')
const NotificationManager = require('./lib/notification-manager')
const notificationManager = new NotificationManager()
-const setupRaven = require('./lib/setupRaven')
+const setupSentry = require('./lib/setupSentry')
const log = require('loglevel')
start().catch(log.error)
@@ -21,7 +21,17 @@ async function start () {
// setup sentry error reporting
const release = global.platform.getVersion()
- setupRaven({ release })
+ setupSentry({ release, getState })
+ // provide app state to append to error logs
+ function getState() {
+ // get app state
+ const state = window.getCleanAppState()
+ // remove unnecessary data
+ delete state.localeMessages
+ delete state.metamask.recentBlocks
+ // return state to be added to request
+ return state
+ }
// inject css
// const css = MetaMaskUiCss()