diff options
Diffstat (limited to 'app/scripts/controllers')
-rw-r--r-- | app/scripts/controllers/blacklist.js | 25 | ||||
-rw-r--r-- | app/scripts/controllers/currency.js | 80 | ||||
-rw-r--r-- | app/scripts/controllers/network/createInfuraClient.js | 33 | ||||
-rw-r--r-- | app/scripts/controllers/network/createMetamaskMiddleware.js | 2 | ||||
-rw-r--r-- | app/scripts/controllers/network/network.js | 60 | ||||
-rw-r--r-- | app/scripts/controllers/preferences.js | 106 | ||||
-rw-r--r-- | app/scripts/controllers/provider-approval.js | 158 | ||||
-rw-r--r-- | app/scripts/controllers/token-rates.js | 11 | ||||
-rw-r--r-- | app/scripts/controllers/transactions/index.js | 35 | ||||
-rw-r--r-- | app/scripts/controllers/transactions/tx-gas-utils.js | 35 | ||||
-rw-r--r-- | app/scripts/controllers/transactions/tx-state-manager.js | 5 |
11 files changed, 480 insertions, 70 deletions
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 fd6a4866d..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: {}, @@ -38,12 +38,15 @@ class PreferencesController { lostIdentities: {}, seedWords: null, forgottenPassword: false, + preferences: { + 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 @@ -101,7 +104,7 @@ class PreferencesController { * @param {Function} - end */ async requestWatchAsset (req, res, next, end) { - if (req.method === 'metamask_watchAsset') { + if (req.method === 'metamask_watchAsset' || req.method === 'wallet_watchAsset') { const { type, options } = req.params switch (type) { case 'ERC20': @@ -372,22 +375,6 @@ class PreferencesController { } /** - * Gets an updated rpc list from this.addToFrequentRpcList() and sets the `frequentRpcList` to this update list. - * - * @param {string} _url The the new rpc url to add to the updated list - * @param {bool} remove Remove selected url - * @returns {Promise<void>} Promise resolves with undefined - * - */ - updateFrequentRpcList (_url, remove = false) { - return this.addToFrequentRpcList(_url, remove) - .then((rpcList) => { - this.store.updateState({ frequentRpcList: rpcList }) - return Promise.resolve() - }) - } - - /** * Setter for the `currentAccountTab` property * * @param {string} currentAccountTab Specifies the new tab to be marked as current @@ -402,35 +389,53 @@ class PreferencesController { } /** - * Returns an updated rpcList based on the passed url and the current list. - * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the - * end of the list. The current list is modified and returned as a promise. + * Adds custom RPC url to state. * - * @param {string} _url The rpc url to add to the frequentRpcList. - * @param {bool} remove Remove selected url - * @returns {Promise<array>} The updated frequentRpcList. + * @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, remove = false) { - 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 (!remove && _url !== 'http://localhost:8545') { - rpcList.push(_url) + if (url !== 'http://localhost:8545') { + rpcList.push({ rpcUrl: url, chainId, ticker, nickname }) + } + this.store.updateState({ frequentRpcListDetail: rpcList }) + return Promise.resolve(rpcList) + } + + /** + * Removes custom RPC url from state. + * + * @param {string} url The RPC url to remove from frequentRpcList. + * @returns {Promise<array>} Promise resolving to updated frequentRpcList. + * + */ + removeFromFrequentRpcList (url) { + const rpcList = this.getFrequentRpcListDetail() + const index = rpcList.findIndex((element) => { return element.rpcUrl === url }) + if (index !== -1) { + rpcList.splice(index, 1) } + 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 } /** @@ -463,6 +468,33 @@ class PreferencesController { getFeatureFlags () { return this.store.getState().featureFlags } + + /** + * Updates the `preferences` property, which is an object. These are user-controlled features + * found in the settings page. + * @param {string} preference The preference to enable or disable. + * @param {boolean} value Indicates whether or not the preference should be enabled or disabled. + * @returns {Promise<object>} Promises a new object; the updated preferences object. + */ + setPreference (preference, value) { + const currentPreferences = this.getPreferences() + const updatedPreferences = { + ...currentPreferences, + [preference]: value, + } + + this.store.updateState({ preferences: updatedPreferences }) + return Promise.resolve(updatedPreferences) + } + + /** + * A getter for the `preferences` property + * @returns {object} A key-boolean map of user-selected preferences. + */ + getPreferences () { + return this.store.getState().preferences + } + // // PRIVATE METHODS // @@ -535,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 }) @@ -551,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..21d7fd22e --- /dev/null +++ b/app/scripts/controllers/provider-approval.js @@ -0,0 +1,158 @@ +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-legacy-provider-request', + selectedAddress: this.publicConfigStore.getState().selectedAddress, + }, { 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', + selectedAddress: this.publicConfigStore.getState().selectedAddress, + }, { 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/index.js b/app/scripts/controllers/transactions/index.js index a57c85f50..9f2290924 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -366,7 +366,40 @@ class TransactionController extends EventEmitter { this.txStateManager.setTxStatusSubmitted(txId) } - confirmTransaction (txId) { + /** + * Sets the status of the transaction to confirmed and sets the status of nonce duplicates as + * dropped if the txParams have data it will fetch the txReceipt + * @param {number} txId - The tx's ID + * @returns {Promise<void>} + */ + async confirmTransaction (txId) { + // get the txReceipt before marking the transaction confirmed + // to ensure the receipt is gotten before the ui revives the tx + const txMeta = this.txStateManager.getTx(txId) + + if (!txMeta) { + return + } + + try { + const txReceipt = await this.query.getTransactionReceipt(txMeta.hash) + + // It seems that sometimes the numerical values being returned from + // this.query.getTransactionReceipt are BN instances and not strings. + const gasUsed = typeof txReceipt.gasUsed !== 'string' + ? txReceipt.gasUsed.toString(16) + : txReceipt.gasUsed + + txMeta.txReceipt = { + ...txReceipt, + gasUsed, + } + + this.txStateManager.updateTx(txMeta, 'transactions#confirmTransaction - add txReceipt') + } catch (err) { + log.error(err) + } + this.txStateManager.setTxStatusConfirmed(txId) this._markNonceDuplicatesDropped(txId) } 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/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index daa6cc388..58c48e34e 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -400,6 +400,11 @@ class TransactionStateManager extends EventEmitter { */ _setTxStatus (txId, status) { const txMeta = this.getTx(txId) + + if (!txMeta) { + return + } + txMeta.status = status setTimeout(() => { try { |