1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 19:26:13 +02:00
metamask-extension/app/scripts/controllers/preferences.js

671 lines
22 KiB
JavaScript
Raw Normal View History

const ObservableStore = require('obs-store')
const normalizeAddress = require('eth-sig-util').normalize
2018-08-28 03:10:14 +02:00
const { isValidAddress } = require('ethereumjs-util')
const extend = require('xtend')
class PreferencesController {
2018-04-18 20:41:39 +02:00
/**
*
* @typedef {Object} PreferencesController
2018-04-23 18:41:02 +02:00
* @param {object} opts Overrides the defaults for the initial state of this.store
* @property {object} store The stored object containing a users preferences, stored in local storage
* @property {array} store.frequentRpcList A list of custom rpcs to provide the user
* @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
2018-07-27 22:05:12 +02:00
* @property {object} store.accountTokens The tokens stored per account and then per network type
2018-08-21 17:59:42 +02:00
* @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
2019-02-25 20:10:13 +01:00
* user wishes to see that feature.
*
* Feature flags can be set by the global function `setPreference(feature, enabled)`, and so should not expose any sensitive behavior.
* @property {object} store.knownMethodData Contains all data methods known by the user
* @property {string} store.currentLocale The preferred language locale key
* @property {string} store.selectedAddress A hex string that matches the currently selected address in the app
*
2018-04-18 20:41:39 +02:00
*/
constructor (opts = {}) {
const initState = extend({
frequentRpcListDetail: [],
currentAccountTab: 'history',
2018-07-27 22:05:12 +02:00
accountTokens: {},
2018-08-21 17:59:42 +02:00
assetImages: {},
tokens: [],
2018-06-19 00:33:50 +02:00
suggestedTokens: {},
useBlockie: false,
2019-02-25 20:10:13 +01:00
// WARNING: Do not use feature flags for security-sensitive things.
// Feature flag toggling is available in the global namespace
// for convenient testing of pre-release features, and should never
// perform sensitive operations.
featureFlags: {},
knownMethodData: {},
currentLocale: opts.initLangCode,
identities: {},
lostIdentities: {},
seedWords: null,
forgottenPassword: false,
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
completedOnboarding: false,
completedUiMigration: true,
}, opts.initState)
2018-06-04 23:21:46 +02:00
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
2018-09-27 20:19:09 +02:00
this.openPopup = opts.openPopup
2018-07-27 22:05:12 +02:00
this._subscribeProviderType()
2019-02-25 20:10:13 +01:00
global.setPreference = (key, value) => {
return this.setFeatureFlag(key, value)
}
}
// PUBLIC METHODS
/**
* Sets the {@code forgottenPassword} state property
* @param {boolean} forgottenPassword whether or not the user has forgotten their password
*/
setPasswordForgotten (forgottenPassword) {
this.store.updateState({ forgottenPassword })
}
/**
* Sets the {@code seedWords} seed words
* @param {string|null} seedWords the seed words
*/
setSeedWords (seedWords) {
this.store.updateState({ seedWords })
}
2018-04-18 20:41:39 +02:00
/**
* Setter for the `useBlockie` property
*
* @param {boolean} val Whether or not the user prefers blockie indicators
*
*/
setUseBlockie (val) {
this.store.updateState({ useBlockie: val })
2017-11-24 02:33:44 +01:00
}
2018-06-19 00:05:41 +02:00
getSuggestedTokens () {
return this.store.getState().suggestedTokens
}
2018-08-21 17:59:42 +02:00
getAssetImages () {
return this.store.getState().assetImages
}
2018-08-21 17:59:42 +02:00
addSuggestedERC20Asset (tokenOpts) {
this._validateERC20AssetParams(tokenOpts)
2018-06-19 00:33:50 +02:00
const suggested = this.getSuggestedTokens()
2018-08-23 20:54:40 +02:00
const { rawAddress, symbol, decimals, image } = tokenOpts
2018-08-07 00:28:47 +02:00
const address = normalizeAddress(rawAddress)
2018-08-23 20:54:40 +02:00
const newEntry = { address, symbol, decimals, image }
2018-08-07 00:28:47 +02:00
suggested[address] = newEntry
2018-06-19 00:33:50 +02:00
this.store.updateState({ suggestedTokens: suggested })
}
/**
* Add new methodData to state, to avoid requesting this information again through Infura
*
* @param {string} fourBytePrefix Four-byte method signature
* @param {string} methodData Corresponding data method
*/
addKnownMethodData (fourBytePrefix, methodData) {
const knownMethodData = this.store.getState().knownMethodData
knownMethodData[fourBytePrefix] = methodData
this.store.updateState({ knownMethodData })
}
2018-06-19 00:05:41 +02:00
/**
* RPC engine middleware for requesting new asset added
2018-06-19 00:05:41 +02:00
*
* @param req
* @param res
* @param {Function} - next
* @param {Function} - end
*/
async requestWatchAsset (req, res, next, end) {
2018-10-19 18:57:11 +02:00
if (req.method === 'metamask_watchAsset' || req.method === 'wallet_watchAsset') {
const { type, options } = req.params
switch (type) {
case 'ERC20':
2018-08-21 18:12:45 +02:00
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`))
2018-06-19 00:33:50 +02:00
}
2018-06-19 00:05:41 +02:00
} else {
next()
2018-06-19 00:05:41 +02:00
}
}
2018-04-18 20:41:39 +02:00
/**
* Getter for the `useBlockie` property
*
* @returns {boolean} this.store.useBlockie
*
*/
2017-11-24 02:33:44 +01:00
getUseBlockie () {
return this.store.getState().useBlockie
}
2018-04-18 20:41:39 +02:00
/**
* Setter for the `currentLocale` property
*
* @param {string} key he preferred language locale key
2018-04-18 20:41:39 +02:00
*
*/
2018-03-16 01:29:45 +01:00
setCurrentLocale (key) {
this.store.updateState({ currentLocale: key })
}
/**
* Updates identities to only include specified addresses. Removes identities
* not included in addresses array
*
2018-06-03 20:30:11 +02:00
* @param {string[]} addresses An array of hex addresses
*
*/
setAddresses (addresses) {
const oldIdentities = this.store.getState().identities
const oldAccountTokens = this.store.getState().accountTokens
2018-07-27 22:05:12 +02:00
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids
}, {})
2018-07-31 01:09:17 +02:00
const accountTokens = addresses.reduce((tokens, address) => {
const oldTokens = oldAccountTokens[address] || {}
tokens[address] = oldTokens
return tokens
}, {})
2018-07-27 22:05:12 +02:00
this.store.updateState({ identities, accountTokens })
}
2018-07-11 06:20:40 +02:00
/**
* Removes an address from state
*
* @param {string} address A hex address
* @returns {string} the address that was removed
*/
removeAddress (address) {
const identities = this.store.getState().identities
2018-07-27 22:05:12 +02:00
const accountTokens = this.store.getState().accountTokens
2018-07-11 06:20:40 +02:00
if (!identities[address]) {
throw new Error(`${address} can't be deleted cause it was not found`)
}
delete identities[address]
2018-07-27 22:05:12 +02:00
delete accountTokens[address]
this.store.updateState({ identities, accountTokens })
2018-07-11 06:20:40 +02:00
// If the selected account is no longer valid,
// select an arbitrary other account:
if (address === this.getSelectedAddress()) {
const selected = Object.keys(identities)[0]
this.setSelectedAddress(selected)
}
return address
}
/**
* Adds addresses to the identities object without removing identities
*
2018-06-03 20:30:11 +02:00
* @param {string[]} addresses An array of hex addresses
*
*/
addAddresses (addresses) {
const identities = this.store.getState().identities
2018-07-27 22:05:12 +02:00
const accountTokens = this.store.getState().accountTokens
addresses.forEach((address) => {
// skip if already exists
if (identities[address]) return
// add missing identity
const identityCount = Object.keys(identities).length
accountTokens[address] = {}
identities[address] = { name: `Account ${identityCount + 1}`, address }
})
2018-07-27 22:05:12 +02:00
this.store.updateState({ identities, accountTokens })
}
/*
* Synchronizes identity entries with known accounts.
* Removes any unknown identities, and returns the resulting selected address.
*
* @param {Array<string>} addresses known to the vault.
* @returns {Promise<string>} selectedAddress the selected address.
*/
syncAddresses (addresses) {
2018-07-03 00:49:33 +02:00
const { identities, lostIdentities } = this.store.getState()
2018-07-03 00:49:33 +02:00
const newlyLost = {}
Object.keys(identities).forEach((identity) => {
if (!addresses.includes(identity)) {
newlyLost[identity] = identities[identity]
2018-06-05 00:34:38 +02:00
delete identities[identity]
}
})
2018-06-04 23:21:46 +02:00
// Identities are no longer present.
if (Object.keys(newlyLost).length > 0) {
2018-06-04 23:21:46 +02:00
// Notify our servers:
if (this.diagnostics) this.diagnostics.reportOrphans(newlyLost)
// store lost accounts
2018-07-03 00:49:33 +02:00
for (const key in newlyLost) {
lostIdentities[key] = newlyLost[key]
}
2018-06-04 23:21:46 +02:00
}
this.store.updateState({ identities, lostIdentities })
this.addAddresses(addresses)
2018-06-05 00:18:12 +02:00
// If the selected account is no longer valid,
// select an arbitrary other account:
let selected = this.getSelectedAddress()
if (!addresses.includes(selected)) {
selected = addresses[0]
this.setSelectedAddress(selected)
}
return selected
}
2018-08-04 01:24:12 +02:00
removeSuggestedTokens () {
return new Promise((resolve, reject) => {
this.store.updateState({ suggestedTokens: {} })
resolve({})
2018-08-04 01:24:12 +02:00
})
}
2018-04-18 20:41:39 +02:00
/**
* Setter for the `selectedAddress` property
*
* @param {string} _address A new hex address for an account
2018-07-31 21:59:19 +02:00
* @returns {Promise<void>} Promise resolves with tokens
2018-04-18 20:41:39 +02:00
*
*/
2017-02-21 21:32:13 +01:00
setSelectedAddress (_address) {
2018-07-27 01:28:12 +02:00
const address = normalizeAddress(_address)
2018-07-31 21:59:19 +02:00
this._updateTokens(address)
this.store.updateState({ selectedAddress: address })
const tokens = this.store.getState().tokens
2018-07-27 01:28:12 +02:00
return Promise.resolve(tokens)
}
/**
* Getter for the `selectedAddress` property
*
* @returns {string} The hex address for the currently selected account
*
*/
getSelectedAddress () {
return this.store.getState().selectedAddress
}
/**
* Contains data about tokens users add to their account.
* @typedef {Object} AddedToken
* @property {string} address - The hex address for the token contract. Will be all lower cased and hex-prefixed.
* @property {string} symbol - The symbol of the token, usually 3 or 4 capitalized letters
* {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#symbol}
* @property {boolean} decimals - The number of decimals the token uses.
* {@link https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md#decimals}
*/
/**
* Adds a new token to the token array, or updates the token if passed an address that already exists.
* Modifies the existing tokens array from the store. All objects in the tokens array array AddedToken objects.
* @see AddedToken {@link AddedToken}
*
* @param {string} rawAddress Hex address of the token contract. May or may not be a checksum address.
* @param {string} symbol The symbol of the token
* @param {number} decimals The number of decimals the token uses.
* @returns {Promise<array>} Promises the new array of AddedToken objects.
*
*/
2018-08-23 20:54:40 +02:00
async addToken (rawAddress, symbol, decimals, image) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const tokens = this.store.getState().tokens
2018-08-21 17:59:42 +02:00
const assetImages = this.getAssetImages()
const previousEntry = tokens.find((token, index) => {
return token.address === address
})
const previousIndex = tokens.indexOf(previousEntry)
if (previousEntry) {
tokens[previousIndex] = newEntry
} else {
tokens.push(newEntry)
}
2018-08-23 20:54:40 +02:00
assetImages[address] = image
2018-08-21 17:59:42 +02:00
this._updateAccountTokens(tokens, assetImages)
return Promise.resolve(tokens)
}
2018-04-18 20:41:39 +02:00
/**
* Removes a specified token from the tokens array.
*
* @param {string} rawAddress Hex address of the token contract to remove.
2018-04-18 20:47:06 +02:00
* @returns {Promise<array>} The new array of AddedToken objects
2018-04-18 20:41:39 +02:00
*
*/
removeToken (rawAddress) {
const tokens = this.store.getState().tokens
2018-08-21 17:59:42 +02:00
const assetImages = this.getAssetImages()
const updatedTokens = tokens.filter(token => token.address !== rawAddress)
2018-08-21 17:59:42 +02:00
delete assetImages[rawAddress]
this._updateAccountTokens(updatedTokens, assetImages)
return Promise.resolve(updatedTokens)
}
2018-04-18 20:41:39 +02:00
/**
* A getter for the `tokens` property
*
* @returns {array} The current array of AddedToken objects
*
*/
getTokens () {
return this.store.getState().tokens
}
/**
* Sets a custom label for an account
* @param {string} account the account to set a label for
* @param {string} label the custom label for the account
* @return {Promise<string>}
*/
setAccountLabel (account, label) {
if (!account) throw new Error('setAccountLabel requires a valid address, got ' + String(account))
const address = normalizeAddress(account)
const {identities} = this.store.getState()
identities[address] = identities[address] || {}
identities[address].name = label
this.store.updateState({ identities })
return Promise.resolve(label)
}
2018-04-18 20:41:39 +02:00
/**
* Setter for the `currentAccountTab` property
2018-04-18 20:41:39 +02:00
*
* @param {string} currentAccountTab Specifies the new tab to be marked as current
* @returns {Promise<void>} Promise resolves with undefined
*
*/
setCurrentAccountTab (currentAccountTab) {
return new Promise((resolve, reject) => {
this.store.updateState({ currentAccountTab })
resolve()
})
}
/**
* updates custom RPC details
*
* @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.
*
*/
updateRpc (newRpcDetails) {
const rpcList = this.getFrequentRpcListDetail()
const index = rpcList.findIndex((element) => { return element.rpcUrl === newRpcDetails.rpcUrl })
if (index > -1) {
const rpcDetail = rpcList[index]
const updatedRpc = extend(rpcDetail, newRpcDetails)
rpcList[index] = updatedRpc
this.store.updateState({ frequentRpcListDetail: rpcList })
} else {
const { rpcUrl, chainId, ticker, nickname } = newRpcDetails
return this.addToFrequentRpcList(rpcUrl, chainId, ticker, nickname)
}
return Promise.resolve(rpcList)
}
2018-04-18 20:41:39 +02:00
/**
* Adds custom RPC url to state.
2018-04-18 20:41:39 +02:00
*
* @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.
2018-04-18 20:41:39 +02:00
*
*/
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') {
let checkedChainId
if (!!chainId && !Number.isNaN(parseInt(chainId))) {
checkedChainId = chainId
}
rpcList.push({ rpcUrl: url, chainId: checkedChainId, ticker, nickname })
}
2018-10-29 23:56:29 +01:00
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)
2017-02-21 21:32:13 +01:00
}
2018-04-18 20:41:39 +02:00
/**
* Getter for the `frequentRpcListDetail` property.
2018-04-18 20:41:39 +02:00
*
* @returns {array<array>} An array of rpc urls.
2018-04-18 20:41:39 +02:00
*
*/
getFrequentRpcListDetail () {
return this.store.getState().frequentRpcListDetail
2017-02-21 21:32:13 +01:00
}
2017-11-14 17:04:55 +01:00
2018-04-18 20:41:39 +02:00
/**
* Updates the `featureFlags` property, which is an object. One property within that object will be set to a boolean.
*
* @param {string} feature A key that corresponds to a UI feature.
* @param {boolean} activated Indicates whether or not the UI feature should be displayed
2018-04-18 20:41:39 +02:00
* @returns {Promise<object>} Promises a new object; the updated featureFlags object.
*
*/
2017-11-14 17:04:55 +01:00
setFeatureFlag (feature, activated) {
const currentFeatureFlags = this.store.getState().featureFlags
const updatedFeatureFlags = {
...currentFeatureFlags,
[feature]: activated,
}
this.store.updateState({ featureFlags: updatedFeatureFlags })
2017-11-16 20:28:59 +01:00
2017-11-14 17:04:55 +01:00
return Promise.resolve(updatedFeatureFlags)
}
2018-04-18 20:41:39 +02:00
/**
* A getter for the `featureFlags` property
*
* @returns {object} A key-boolean map, where keys refer to features and booleans to whether the
* user wishes to see that feature
2018-04-18 20:41:39 +02:00
*
*/
2017-11-14 17:04:55 +01:00
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
}
/**
* Sets the completedOnboarding state to true, indicating that the user has completed the
* onboarding process.
*/
completeOnboarding () {
this.store.updateState({ completedOnboarding: true })
return Promise.resolve(true)
}
/**
* Sets the {@code completedUiMigration} state to {@code true}, indicating that the user has completed the UI switch.
*/
completeUiMigration () {
this.store.updateState({ completedUiMigration: true })
return Promise.resolve(true)
}
//
// PRIVATE METHODS
//
2018-08-07 00:28:47 +02:00
/**
* Subscription to network provider type.
*
*
*/
_subscribeProviderType () {
2018-07-31 21:59:19 +02:00
this.network.providerStore.subscribe(() => {
const { tokens } = this._getTokenRelatedStates()
this.store.updateState({ tokens })
})
}
/**
* Updates `accountTokens` and `tokens` of current account and network according to it.
*
2018-07-31 19:07:28 +02:00
* @param {array} tokens Array of tokens to be updated.
*
*/
2018-08-21 17:59:42 +02:00
_updateAccountTokens (tokens, assetImages) {
2018-07-31 21:59:19 +02:00
const { accountTokens, providerType, selectedAddress } = this._getTokenRelatedStates()
accountTokens[selectedAddress][providerType] = tokens
2018-08-21 17:59:42 +02:00
this.store.updateState({ accountTokens, tokens, assetImages })
2018-07-27 22:05:12 +02:00
}
2018-07-27 22:05:12 +02:00
/**
* Updates `tokens` of current account and network.
2018-07-27 22:05:12 +02:00
*
2018-07-31 21:59:19 +02:00
* @param {string} selectedAddress Account address to be updated with.
2018-07-27 22:05:12 +02:00
*
*/
_updateTokens (selectedAddress) {
2018-07-31 21:59:19 +02:00
const { tokens } = this._getTokenRelatedStates(selectedAddress)
this.store.updateState({ tokens })
}
/**
* A getter for `tokens` and `accountTokens` related states.
*
* @param {string} selectedAddress A new hex address for an account
2018-07-31 22:18:06 +02:00
* @returns {Object.<array, object, string, string>} States to interact with tokens in `accountTokens`
2018-07-31 21:59:19 +02:00
*
*/
_getTokenRelatedStates (selectedAddress) {
const accountTokens = this.store.getState().accountTokens
2018-07-31 21:59:19 +02:00
if (!selectedAddress) selectedAddress = this.store.getState().selectedAddress
const providerType = this.network.providerStore.getState().type
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
if (!(providerType in accountTokens[selectedAddress])) accountTokens[selectedAddress][providerType] = []
const tokens = accountTokens[selectedAddress][providerType]
2018-07-31 21:59:19 +02:00
return { tokens, accountTokens, providerType, selectedAddress }
2018-08-07 00:28:47 +02:00
}
/**
* Handle the suggestion of an ERC20 asset through `watchAsset`
* *
2018-08-21 17:59:42 +02:00
* @param {Promise} promise Promise according to addition of ERC20 token
*
*/
async _handleWatchAssetERC20 (options) {
2018-08-23 20:54:40 +02:00
const { address, symbol, decimals, image } = options
const rawAddress = address
2018-08-21 18:12:45 +02:00
try {
this._validateERC20AssetParams({ rawAddress, symbol, decimals })
} catch (err) {
return err
}
2018-08-23 20:54:40 +02:00
const tokenOpts = { rawAddress, decimals, symbol, image }
2018-08-21 17:59:42 +02:00
this.addSuggestedERC20Asset(tokenOpts)
2018-09-27 20:19:09 +02:00
return this.openPopup().then(() => {
const tokenAddresses = this.getTokens().filter(token => token.address === normalizeAddress(rawAddress))
return tokenAddresses.length > 0
})
}
2018-08-21 17:59:42 +02:00
/**
* 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 || 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`)
2018-08-21 17:59:42 +02:00
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