1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00
metamask-extension/app/scripts/controllers/preferences.js

436 lines
14 KiB
JavaScript
Raw Normal View History

const ObservableStore = require('obs-store')
const normalizeAddress = require('eth-sig-util').normalize
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
* @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
* @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({
frequentRpcList: [],
currentAccountTab: 'history',
2018-07-27 22:05:12 +02:00
accountTokens: {},
tokens: [],
useBlockie: false,
2017-11-14 17:04:55 +01:00
featureFlags: {},
currentLocale: opts.initLangCode,
identities: {},
lostIdentities: {},
}, opts.initState)
2018-06-04 23:21:46 +02:00
this.diagnostics = opts.diagnostics
this.network = opts.network
this.store = new ObservableStore(initState)
2018-07-27 22:05:12 +02:00
this._defineTokens()
this._subscribeProviderType()
}
// PUBLIC METHODS
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-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
2018-07-27 22:05:12 +02:00
const accountTokens = this.store.getState().accountTokens
const identities = addresses.reduce((ids, address, index) => {
const oldId = oldIdentities[address] || {}
ids[address] = {name: `Account ${index + 1}`, address, ...oldId}
return ids
}, {})
for (const address in identities) {
2018-07-27 22:05:12 +02:00
if (!(address in accountTokens)) accountTokens[address] = {}
}
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
2018-07-27 22:05:12 +02:00
if (!(address in accountTokens)) 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-04-18 20:41:39 +02:00
/**
* Setter for the `selectedAddress` property
*
* @param {string} _address A new hex address for an account
* @returns {Promise<void>} Promise resolves with undefined
*
*/
2017-02-21 21:32:13 +01:00
setSelectedAddress (_address) {
2018-07-27 01:28:12 +02:00
const address = normalizeAddress(_address)
2018-07-27 22:05:12 +02:00
const accountTokens = this.store.getState().accountTokens
2018-07-27 01:28:12 +02:00
const providerType = this.network.providerStore.getState().type
2018-07-27 22:05:12 +02:00
if (!(address in accountTokens)) accountTokens[address] = {}
if (!(providerType in accountTokens[address])) accountTokens[address][providerType] = []
const tokens = accountTokens[address][providerType]
2018-07-27 01:28:12 +02:00
this.store.updateState({ selectedAddress: address, tokens })
2018-07-27 22:05:12 +02:00
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.
*
*/
async addToken (rawAddress, symbol, decimals) {
const address = normalizeAddress(rawAddress)
const newEntry = { address, symbol, decimals }
const tokens = this.store.getState().tokens
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-07-25 22:14:10 +02:00
const selectedAddress = this.store.getState().selectedAddress
2018-07-27 22:05:12 +02:00
const accountTokens = this.store.getState().accountTokens
const providerType = this.network.providerStore.getState().type
2018-07-27 22:05:12 +02:00
accountTokens[selectedAddress][providerType] = tokens
this.store.updateState({ accountTokens, tokens })
2017-12-20 23:46:12 +01:00
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) {
2018-07-27 22:05:12 +02:00
const accountTokens = this.store.getState().accountTokens
2018-07-25 22:14:10 +02:00
const selectedAddress = this.store.getState().selectedAddress
const providerType = this.network.providerStore.getState().type
2018-07-27 22:05:12 +02:00
const updatedTokens = accountTokens[selectedAddress][providerType].filter(token => token.address !== rawAddress)
accountTokens[selectedAddress][providerType] = updatedTokens
this.store.updateState({ accountTokens, tokens: updatedTokens })
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
/**
* 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
* @returns {Promise<void>} Promise resolves with undefined
*
*/
updateFrequentRpcList (_url) {
return this.addToFrequentRpcList(_url)
.then((rpcList) => {
this.store.updateState({ frequentRpcList: rpcList })
return Promise.resolve()
})
}
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()
})
}
2018-04-18 20:41:39 +02:00
/**
* 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
* end of the list. The current list is modified and returned as a promise.
2018-04-18 20:41:39 +02:00
*
* @param {string} _url The rpc url to add to the frequentRpcList.
* @returns {Promise<array>} The updated frequentRpcList.
2018-04-18 20:41:39 +02:00
*
*/
2017-02-21 21:51:46 +01:00
addToFrequentRpcList (_url) {
2017-04-27 06:05:45 +02:00
const rpcList = this.getFrequentRpcList()
const index = rpcList.findIndex((element) => { return element === _url })
if (index !== -1) {
rpcList.splice(index, 1)
}
if (_url !== 'http://localhost:8545') {
2017-02-21 21:32:13 +01:00
rpcList.push(_url)
}
if (rpcList.length > 2) {
rpcList.shift()
}
return Promise.resolve(rpcList)
2017-02-21 21:32:13 +01:00
}
2018-04-18 20:41:39 +02:00
/**
* Getter for the `frequentRpcList` property.
*
* @returns {array<string>} An array of one or two rpc urls.
*
*/
2017-02-21 21:51:46 +01:00
getFrequentRpcList () {
return this.store.getState().frequentRpcList
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
}
//
// PRIVATE METHODS
//
/**
2018-07-27 22:05:12 +02:00
* Getter definition for the `tokens` property of store when controller is initialized
*
*
*/
2018-07-27 22:05:12 +02:00
_defineTokens () {
const selectedAddress = this.store.getState().selectedAddress
const accountTokens = this.store.getState().accountTokens
const providerType = this.network.providerStore.getState().type
if (!(selectedAddress in accountTokens)) accountTokens[selectedAddress] = {}
if (!(providerType in accountTokens[selectedAddress])) return []
this.tokens = accountTokens[selectedAddress][providerType]
}
2018-07-27 22:05:12 +02:00
/**
* Subscription to network provider type
*
*
*/
_subscribeProviderType () {
this.network.providerStore.subscribe(({ type }) => {
const selectedAddress = this.store.getState().selectedAddress
const accountTokens = this.store.getState().accountTokens
if (!(type in accountTokens[selectedAddress])) accountTokens[selectedAddress][type] = []
const tokens = accountTokens[selectedAddress][type]
this.store.updateState({ tokens })
})
}
}
module.exports = PreferencesController