diff options
author | brunobar79 <brunobar79@gmail.com> | 2018-09-06 04:26:09 +0800 |
---|---|---|
committer | brunobar79 <brunobar79@gmail.com> | 2018-09-06 04:26:09 +0800 |
commit | 8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52 (patch) | |
tree | 106d2ab08621f972b4b9140df16e129a84526703 /app/scripts | |
parent | b208ce723459a13f9b1fd6837af9d2858ba4cc17 (diff) | |
parent | dc2431fe62bc7e50ebbf864389e9590f29d2136f (diff) | |
download | tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.gz tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.bz2 tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.lz tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.xz tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.tar.zst tangerine-wallet-browser-8ee01f4e99d59dbe1bfa5703da9b5efda41a7a52.zip |
Merge branch 'develop' of github.com:MetaMask/metamask-extension into trezor-v5
Diffstat (limited to 'app/scripts')
-rw-r--r-- | app/scripts/background.js | 20 | ||||
-rw-r--r-- | app/scripts/controllers/preferences.js | 120 | ||||
-rw-r--r-- | app/scripts/lib/account-tracker.js | 18 | ||||
-rw-r--r-- | app/scripts/lib/ipfsContent.js | 2 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 66 |
5 files changed, 202 insertions, 24 deletions
diff --git a/app/scripts/background.js b/app/scripts/background.js index 1599b8f99..e13d2842a 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -256,6 +256,7 @@ function setupController (initState, initLangCode) { showUnconfirmedMessage: triggerUi, unlockAccountMessage: triggerUi, showUnapprovedTx: triggerUi, + showWatchAssetUi: showWatchAssetUi, // initial state initState, // initial locale code @@ -451,9 +452,28 @@ function triggerUi () { }) } +/** + * Opens the browser popup for user confirmation of watchAsset + * then it waits until user interact with the UI + */ +function showWatchAssetUi () { + triggerUi() + return new Promise( + (resolve) => { + var interval = setInterval(() => { + if (!notificationIsOpen) { + clearInterval(interval) + resolve() + } + }, 1000) + } + ) +} + // On first install, open a window to MetaMask website to how-it-works. extension.runtime.onInstalled.addListener(function (details) { if ((details.reason === 'install') && (!METAMASK_DEBUG)) { extension.tabs.create({url: 'https://metamask.io/#how-it-works'}) } }) + diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 707fd7de9..464a37017 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const normalizeAddress = require('eth-sig-util').normalize +const { isValidAddress } = require('ethereumjs-util') const extend = require('xtend') @@ -14,6 +15,7 @@ class PreferencesController { * @property {string} store.currentAccountTab Indicates the selected tab in the ui * @property {array} store.tokens The tokens the user wants display in their token lists * @property {object} store.accountTokens The tokens stored per account and then per network type + * @property {object} store.assetImages Contains assets objects related to assets added * @property {boolean} store.useBlockie The users preference for blockie identicons within the UI * @property {object} store.featureFlags A key-boolean map, where keys refer to features and booleans to whether the * user wishes to see that feature @@ -26,7 +28,9 @@ class PreferencesController { frequentRpcList: [], currentAccountTab: 'history', accountTokens: {}, + assetImages: {}, tokens: [], + suggestedTokens: {}, useBlockie: false, featureFlags: {}, currentLocale: opts.initLangCode, @@ -37,6 +41,7 @@ class PreferencesController { this.diagnostics = opts.diagnostics this.network = opts.network this.store = new ObservableStore(initState) + this.showWatchAssetUi = opts.showWatchAssetUi this._subscribeProviderType() } // PUBLIC METHODS @@ -51,6 +56,53 @@ class PreferencesController { this.store.updateState({ useBlockie: val }) } + getSuggestedTokens () { + return this.store.getState().suggestedTokens + } + + getAssetImages () { + return this.store.getState().assetImages + } + + addSuggestedERC20Asset (tokenOpts) { + this._validateERC20AssetParams(tokenOpts) + const suggested = this.getSuggestedTokens() + const { rawAddress, symbol, decimals, image } = tokenOpts + const address = normalizeAddress(rawAddress) + const newEntry = { address, symbol, decimals, image } + suggested[address] = newEntry + this.store.updateState({ suggestedTokens: suggested }) + } + + /** + * RPC engine middleware for requesting new asset added + * + * @param req + * @param res + * @param {Function} - next + * @param {Function} - end + */ + async requestWatchAsset (req, res, next, end) { + if (req.method === 'metamask_watchAsset') { + const { type, options } = req.params + switch (type) { + case 'ERC20': + const result = await this._handleWatchAssetERC20(options) + if (result instanceof Error) { + end(result) + } else { + res.result = result + end() + } + break + default: + end(new Error(`Asset of type ${type} not supported`)) + } + } else { + next() + } + } + /** * Getter for the `useBlockie` property * @@ -186,6 +238,13 @@ class PreferencesController { return selected } + removeSuggestedTokens () { + return new Promise((resolve, reject) => { + this.store.updateState({ suggestedTokens: {} }) + resolve({}) + }) + } + /** * Setter for the `selectedAddress` property * @@ -232,11 +291,11 @@ class PreferencesController { * @returns {Promise<array>} Promises the new array of AddedToken objects. * */ - async addToken (rawAddress, symbol, decimals) { + async addToken (rawAddress, symbol, decimals, image) { const address = normalizeAddress(rawAddress) const newEntry = { address, symbol, decimals } - const tokens = this.store.getState().tokens + const assetImages = this.getAssetImages() const previousEntry = tokens.find((token, index) => { return token.address === address }) @@ -247,7 +306,8 @@ class PreferencesController { } else { tokens.push(newEntry) } - this._updateAccountTokens(tokens) + assetImages[address] = image + this._updateAccountTokens(tokens, assetImages) return Promise.resolve(tokens) } @@ -260,8 +320,10 @@ class PreferencesController { */ removeToken (rawAddress) { const tokens = this.store.getState().tokens + const assetImages = this.getAssetImages() const updatedTokens = tokens.filter(token => token.address !== rawAddress) - this._updateAccountTokens(updatedTokens) + delete assetImages[rawAddress] + this._updateAccountTokens(updatedTokens, assetImages) return Promise.resolve(updatedTokens) } @@ -322,7 +384,7 @@ class PreferencesController { /** * Returns an updated rpcList based on the passed url and the current list. - * The returned list will have a max length of 2. If the _url currently exists it the list, it will be moved to the + * The returned list will have a max length of 3. If the _url currently exists it the list, it will be moved to the * end of the list. The current list is modified and returned as a promise. * * @param {string} _url The rpc url to add to the frequentRpcList. @@ -338,7 +400,7 @@ class PreferencesController { if (_url !== 'http://localhost:8545') { rpcList.push(_url) } - if (rpcList.length > 2) { + if (rpcList.length > 3) { rpcList.shift() } return Promise.resolve(rpcList) @@ -387,6 +449,7 @@ class PreferencesController { // // PRIVATE METHODS // + /** * Subscription to network provider type. * @@ -405,10 +468,10 @@ class PreferencesController { * @param {array} tokens Array of tokens to be updated. * */ - _updateAccountTokens (tokens) { + _updateAccountTokens (tokens, assetImages) { const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates() accountTokens[selectedAddress][providerType] = tokens - this.store.updateState({ accountTokens, tokens }) + this.store.updateState({ accountTokens, tokens, assetImages }) } /** @@ -438,6 +501,47 @@ class PreferencesController { const tokens = accountTokens[selectedAddress][providerType] return { tokens, accountTokens, providerType, selectedAddress } } + + /** + * Handle the suggestion of an ERC20 asset through `watchAsset` + * * + * @param {Promise} promise Promise according to addition of ERC20 token + * + */ + async _handleWatchAssetERC20 (options) { + const { address, symbol, decimals, image } = options + const rawAddress = address + try { + this._validateERC20AssetParams({ rawAddress, symbol, decimals }) + } catch (err) { + return err + } + const tokenOpts = { rawAddress, decimals, symbol, image } + this.addSuggestedERC20Asset(tokenOpts) + return this.showWatchAssetUi().then(() => { + const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress)) + return tokenAddresses.length > 0 + }) + } + + /** + * Validates that the passed options for suggested token have all required properties. + * + * @param {Object} opts The options object to validate + * @throws {string} Throw a custom error indicating that address, symbol and/or decimals + * doesn't fulfill requirements + * + */ + _validateERC20AssetParams (opts) { + const { rawAddress, symbol, decimals } = opts + if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`) + if (!(symbol.length < 6)) throw new Error(`Invalid symbol ${symbol} more than five characters`) + const numDecimals = parseInt(decimals, 10) + if (isNaN(numDecimals) || numDecimals > 36 || numDecimals < 0) { + throw new Error(`Invalid decimals ${decimals} must be at least 0, and not over 36`) + } + if (!isValidAddress(rawAddress)) throw new Error(`Invalid address ${rawAddress}`) + } } module.exports = PreferencesController diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js index b7e2c7cbe..3a52d5e8d 100644 --- a/app/scripts/lib/account-tracker.js +++ b/app/scripts/lib/account-tracker.js @@ -43,10 +43,24 @@ class AccountTracker { this._provider = opts.provider this._query = pify(new EthQuery(this._provider)) this._blockTracker = opts.blockTracker - // subscribe to latest block - this._blockTracker.on('latest', this._updateForBlock.bind(this)) // blockTracker.currentBlock may be null this._currentBlockNumber = this._blockTracker.getCurrentBlock() + // bind function for easier listener syntax + this._updateForBlock = this._updateForBlock.bind(this) + } + + start () { + // remove first to avoid double add + this._blockTracker.removeListener('latest', this._updateForBlock) + // add listener + this._blockTracker.addListener('latest', this._updateForBlock) + // fetch account balances + this._updateAccounts() + } + + stop () { + // remove listener + this._blockTracker.removeListener('latest', this._updateForBlock) } /** diff --git a/app/scripts/lib/ipfsContent.js b/app/scripts/lib/ipfsContent.js index 5db63f47d..38682b916 100644 --- a/app/scripts/lib/ipfsContent.js +++ b/app/scripts/lib/ipfsContent.js @@ -34,7 +34,7 @@ module.exports = function (provider) { return { cancel: true } } - extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/', '*://*.test/']}) + extension.webRequest.onErrorOccurred.addListener(ipfsContent, {urls: ['*://*.eth/']}) return { remove () { diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 29838ad2d..98cb62bfa 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -67,6 +67,10 @@ module.exports = class MetamaskController extends EventEmitter { const initState = opts.initState || {} this.recordFirstTimeInfo(initState) + // this keeps track of how many "controllerStream" connections are open + // the only thing that uses controller connections are open metamask UI instances + this.activeControllerConnections = 0 + // platform-specific api this.platform = opts.platform @@ -88,6 +92,7 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, + showWatchAssetUi: opts.showWatchAssetUi, network: this.networkController, }) @@ -127,6 +132,14 @@ module.exports = class MetamaskController extends EventEmitter { provider: this.provider, blockTracker: this.blockTracker, }) + // start and stop polling for balances based on activeControllerConnections + this.on('controllerConnectionChanged', (activeControllerConnections) => { + if (activeControllerConnections > 0) { + this.accountTracker.start() + } else { + this.accountTracker.stop() + } + }) // key mgmt const additionalKeyrings = [TrezorKeyring, LedgerBridgeKeyring] @@ -137,19 +150,7 @@ module.exports = class MetamaskController extends EventEmitter { encryptor: opts.encryptor || undefined, }) - // If only one account exists, make sure it is selected. - this.keyringController.memStore.subscribe((state) => { - const addresses = state.keyrings.reduce((res, keyring) => { - return res.concat(keyring.accounts) - }, []) - if (addresses.length === 1) { - const address = addresses[0] - this.preferencesController.setSelectedAddress(address) - } - // ensure preferences + identities controller know about all addresses - this.preferencesController.addAddresses(addresses) - this.accountTracker.syncWithAddresses(addresses) - }) + this.keyringController.memStore.subscribe((s) => this._onKeyringControllerUpdate(s)) // detect tokens controller this.detectTokensController = new DetectTokensController({ @@ -386,6 +387,7 @@ module.exports = class MetamaskController extends EventEmitter { setSelectedAddress: nodeify(preferencesController.setSelectedAddress, preferencesController), addToken: nodeify(preferencesController.addToken, preferencesController), removeToken: nodeify(preferencesController.removeToken, preferencesController), + removeSuggestedTokens: nodeify(preferencesController.removeSuggestedTokens, preferencesController), setCurrentAccountTab: nodeify(preferencesController.setCurrentAccountTab, preferencesController), setAccountLabel: nodeify(preferencesController.setAccountLabel, preferencesController), setFeatureFlag: nodeify(preferencesController.setFeatureFlag, preferencesController), @@ -1209,11 +1211,19 @@ module.exports = class MetamaskController extends EventEmitter { setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) + // report new active controller connection + this.activeControllerConnections++ + this.emit('controllerConnectionChanged', this.activeControllerConnections) + // connect dnode api to remote connection pump( outStream, dnode, outStream, (err) => { + // report new active controller connection + this.activeControllerConnections-- + this.emit('controllerConnectionChanged', this.activeControllerConnections) + // report any error if (err) log.error(err) } ) @@ -1242,6 +1252,7 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(createOriginMiddleware({ origin })) engine.push(createLoggerMiddleware({ origin })) engine.push(filterMiddleware) + engine.push(this.preferencesController.requestWatchAsset.bind(this.preferencesController)) engine.push(createProviderMiddleware({ provider: this.provider })) // setup connection @@ -1279,6 +1290,34 @@ module.exports = class MetamaskController extends EventEmitter { } /** + * Handle a KeyringController update + * @param {object} state the KC state + * @return {Promise<void>} + * @private + */ + async _onKeyringControllerUpdate (state) { + const {isUnlocked, keyrings} = state + const addresses = keyrings.reduce((acc, {accounts}) => acc.concat(accounts), []) + + if (!addresses.length) { + return + } + + // Ensure preferences + identities controller know about all addresses + this.preferencesController.addAddresses(addresses) + this.accountTracker.syncWithAddresses(addresses) + + const wasLocked = !isUnlocked + if (wasLocked) { + const oldSelectedAddress = this.preferencesController.getSelectedAddress() + if (!addresses.includes(oldSelectedAddress)) { + const address = addresses[0] + await this.preferencesController.setSelectedAddress(address) + } + } + } + + /** * A method for emitting the full MetaMask state to all registered listeners. * @private */ @@ -1424,6 +1463,7 @@ module.exports = class MetamaskController extends EventEmitter { } } + // TODO: Replace isClientOpen methods with `controllerConnectionChanged` events. /** * A method for recording whether the MetaMask user interface is open or not. * @private |