diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/_locales/en/messages.json | 3 | ||||
-rw-r--r-- | app/scripts/background.js | 2 | ||||
-rw-r--r-- | app/scripts/controllers/preferences.js | 107 | ||||
-rw-r--r-- | app/scripts/metamask-controller.js | 3 |
4 files changed, 109 insertions, 6 deletions
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index a25a2bd59..f61fb4335 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -29,6 +29,9 @@ "addTokens": { "message": "Add Tokens" }, + "addSuggestedTokens": { + "message": "Add Suggested Tokens" + }, "addAcquiredTokens": { "message": "Add the tokens you've acquired using MetaMask" }, diff --git a/app/scripts/background.js b/app/scripts/background.js index c7395c810..029ad139a 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, + showAddTokenUi: triggerUi, // initial state initState, // initial locale code @@ -449,3 +450,4 @@ extension.runtime.onInstalled.addListener(function (details) { 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..611d2d067 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').isValidAddress 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.imageObjects 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: {}, + imageObjects: {}, 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.showAddTokenUi = opts.showAddTokenUi this._subscribeProviderType() } // PUBLIC METHODS @@ -51,6 +56,50 @@ class PreferencesController { this.store.updateState({ useBlockie: val }) } + getSuggestedTokens () { + return this.store.getState().suggestedTokens + } + + getImageObjects () { + return this.store.getState().imageObjects + } + + addSuggestedToken (tokenOpts) { + this._validateSuggestedTokenParams(tokenOpts) + const suggested = this.getSuggestedTokens() + const { rawAddress, symbol, decimals, imageUrl } = tokenOpts + const address = normalizeAddress(rawAddress) + const newEntry = { address, symbol, decimals, imageUrl } + 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 + */ + requestAddToken (req, res, next, end) { + if (req.method === 'metamask_watchAsset') { + const { type, options } = req.params + switch (type) { + case 'ERC20': + this._handleWatchAssetERC20(options, res) + res.result = options.address + end() + break + default: + // TODO return promise for not handled assets + end(new Error(`Asset of type ${type} not supported`)) + } + } else { + next() + } + } + /** * Getter for the `useBlockie` property * @@ -186,6 +235,13 @@ class PreferencesController { return selected } + removeSuggestedTokens () { + return new Promise((resolve, reject) => { + this.store.updateState({ suggestedTokens: {} }) + resolve({}) + }) + } + /** * Setter for the `selectedAddress` property * @@ -232,11 +288,11 @@ class PreferencesController { * @returns {Promise<array>} Promises the new array of AddedToken objects. * */ - async addToken (rawAddress, symbol, decimals) { + async addToken (rawAddress, symbol, decimals, imageUrl) { const address = normalizeAddress(rawAddress) const newEntry = { address, symbol, decimals } - const tokens = this.store.getState().tokens + const imageObjects = this.getImageObjects() const previousEntry = tokens.find((token, index) => { return token.address === address }) @@ -247,7 +303,8 @@ class PreferencesController { } else { tokens.push(newEntry) } - this._updateAccountTokens(tokens) + imageObjects[address] = imageUrl + this._updateAccountTokens(tokens, imageObjects) return Promise.resolve(tokens) } @@ -260,8 +317,10 @@ class PreferencesController { */ removeToken (rawAddress) { const tokens = this.store.getState().tokens + const imageObjects = this.getImageObjects() const updatedTokens = tokens.filter(token => token.address !== rawAddress) - this._updateAccountTokens(updatedTokens) + delete imageObjects[rawAddress] + this._updateAccountTokens(updatedTokens, imageObjects) return Promise.resolve(updatedTokens) } @@ -387,6 +446,26 @@ class PreferencesController { // // PRIVATE METHODS // + + /** + * 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 + * + */ + _validateSuggestedTokenParams (opts) { + const { rawAddress, symbol, decimals } = opts + if (!rawAddress || !symbol || !decimals) throw new Error(`Cannot suggest token without address, symbol, and decimals`) + if (!(symbol.length < 5)) throw new Error(`Invalid symbol ${symbol} more than four 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}`) + } + /** * Subscription to network provider type. * @@ -405,10 +484,10 @@ class PreferencesController { * @param {array} tokens Array of tokens to be updated. * */ - _updateAccountTokens (tokens) { + _updateAccountTokens (tokens, imageObjects) { const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates() accountTokens[selectedAddress][providerType] = tokens - this.store.updateState({ accountTokens, tokens }) + this.store.updateState({ accountTokens, tokens, imageObjects }) } /** @@ -438,6 +517,22 @@ class PreferencesController { const tokens = accountTokens[selectedAddress][providerType] return { tokens, accountTokens, providerType, selectedAddress } } + + /** + * Handle the suggestion of an ERC20 asset through `watchAsset` + * * + * @param {Object} options Parameters according to addition of ERC20 token + * + */ + _handleWatchAssetERC20 (options) { + // TODO handle bad parameters + const { address, symbol, decimals, imageUrl } = options + const rawAddress = address + this._validateSuggestedTokenParams({ rawAddress, symbol, decimals }) + const tokenOpts = { rawAddress, decimals, symbol, imageUrl } + this.addSuggestedToken(tokenOpts) + this.showAddTokenUi() + } } module.exports = PreferencesController diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 2d7d2c671..57001fdff 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -88,6 +88,7 @@ module.exports = class MetamaskController extends EventEmitter { this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, + showAddTokenUi: opts.showAddTokenUi, network: this.networkController, }) @@ -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), @@ -1239,6 +1241,7 @@ module.exports = class MetamaskController extends EventEmitter { engine.push(createOriginMiddleware({ origin })) engine.push(createLoggerMiddleware({ origin })) engine.push(filterMiddleware) + engine.push(this.preferencesController.requestAddToken.bind(this.preferencesController)) engine.push(createProviderMiddleware({ provider: this.provider })) // setup connection |