1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-27 12:56:01 +01:00
metamask-extension/ui/app/selectors/selectors.js
Erik Marks b75f812953
Improve LoginPerSite UX/devX and permissions logging (#7649)
Update accounts permission history on accountsChanged
Create PermissionsLogController
Fix permissions activity log pruning
Add selectors, background hooks for better UX
Make selected account the first account returned
Use enums for store keys in log controller
Add last selected address history to PreferencesController
2020-01-27 14:42:03 -08:00

564 lines
17 KiB
JavaScript

import { NETWORK_TYPES } from '../helpers/constants/common'
import { mapObjectValues } from '../../../app/scripts/lib/util'
import { stripHexPrefix, addHexPrefix } from 'ethereumjs-util'
import { createSelector } from 'reselect'
import abi from 'human-standard-token-abi'
import { multiplyCurrencies } from '../helpers/utils/conversion-util'
import {
addressSlicer,
checksumAddress,
formatDate,
getOriginFromUrl,
} from '../helpers/utils/util'
import { getPermittedAccounts } from './permissions'
export { getPermittedAccounts } from './permissions'
export function getNetworkIdentifier (state) {
const { metamask: { provider: { type, nickname, rpcTarget } } } = state
return nickname || rpcTarget || type
}
export function getCurrentKeyring (state) {
const identity = getSelectedIdentity(state)
if (!identity) {
return null
}
const simpleAddress = stripHexPrefix(identity.address).toLowerCase()
const keyring = state.metamask.keyrings.find((kr) => {
return kr.accounts.includes(simpleAddress) ||
kr.accounts.includes(identity.address)
})
return keyring
}
export function getAccountType (state) {
const currentKeyring = getCurrentKeyring(state)
const type = currentKeyring && currentKeyring.type
switch (type) {
case 'Trezor Hardware':
case 'Ledger Hardware':
return 'hardware'
case 'Simple Key Pair':
return 'imported'
default:
return 'default'
}
}
export function getSelectedAsset (state) {
const selectedToken = getSelectedToken(state)
return selectedToken && selectedToken.symbol || 'ETH'
}
export function getCurrentNetworkId (state) {
return state.metamask.network
}
export const getMetaMaskAccounts = createSelector(
getMetaMaskAccountsRaw,
getMetaMaskCachedBalances,
(currentAccounts, cachedBalances) => Object.entries(currentAccounts).reduce((selectedAccounts, [accountID, account]) => {
if (account.balance === null || account.balance === undefined) {
return {
...selectedAccounts,
[accountID]: {
...account,
balance: cachedBalances && cachedBalances[accountID],
},
}
} else {
return {
...selectedAccounts,
[accountID]: account,
}
}
}, {})
)
export function getSelectedAddress (state) {
const selectedAddress = state.metamask.selectedAddress || Object.keys(getMetaMaskAccounts(state))[0]
return selectedAddress
}
function lastSelectedAddressSelector (state, origin) {
return state.metamask.lastSelectedAddressByOrigin[origin] || null
}
// not using reselect here since the returns are contingent;
// we have no reasons to recompute the permitted accounts if there
// exists a lastSelectedAddress
export function getLastSelectedAddress (state, origin) {
return (
lastSelectedAddressSelector(state, origin) ||
getPermittedAccounts(state, origin)[0] || // always returns array
getSelectedAddress(state)
)
}
export function getSelectedIdentity (state) {
const selectedAddress = getSelectedAddress(state)
const identities = state.metamask.identities
return identities[selectedAddress]
}
export function getNumberOfAccounts (state) {
return Object.keys(state.metamask.accounts).length
}
export function getNumberOfTokens (state) {
const tokens = state.metamask.tokens
return tokens ? tokens.length : 0
}
export function getMetaMaskKeyrings (state) {
return state.metamask.keyrings
}
export function getMetaMaskIdentities (state) {
return state.metamask.identities
}
export function getMetaMaskAccountsRaw (state) {
return state.metamask.accounts
}
export function getMetaMaskCachedBalances (state) {
const network = getCurrentNetworkId(state)
return state.metamask.cachedBalances[network]
}
/**
* Get ordered (by keyrings) accounts with identity and balance
*/
export const getMetaMaskAccountsOrdered = createSelector(
getMetaMaskKeyrings,
getMetaMaskIdentities,
getMetaMaskAccounts,
(keyrings, identities, accounts) => keyrings
.reduce((list, keyring) => list.concat(keyring.accounts), [])
.filter(address => !!identities[address])
.map(address => ({ ...identities[address], ...accounts[address] }))
)
export function isBalanceCached (state) {
const selectedAccountBalance = state.metamask.accounts[getSelectedAddress(state)].balance
const cachedBalance = getSelectedAccountCachedBalance(state)
return Boolean(!selectedAccountBalance && cachedBalance)
}
export function getSelectedAccountCachedBalance (state) {
const cachedBalances = state.metamask.cachedBalances[state.metamask.network]
const selectedAddress = getSelectedAddress(state)
return cachedBalances && cachedBalances[selectedAddress]
}
export function getSelectedAccount (state) {
const accounts = getMetaMaskAccounts(state)
const selectedAddress = getSelectedAddress(state)
return accounts[selectedAddress]
}
export function getSelectedToken (state) {
const tokens = state.metamask.tokens || []
const selectedTokenAddress = state.metamask.selectedTokenAddress
const selectedToken = tokens.filter(({ address }) => address === selectedTokenAddress)[0]
const sendToken = state.metamask.send && state.metamask.send.token
return selectedToken || sendToken || null
}
export function getSelectedTokenExchangeRate (state) {
const contractExchangeRates = state.metamask.contractExchangeRates
const selectedToken = getSelectedToken(state) || {}
const { address } = selectedToken
return contractExchangeRates[address] || 0
}
export function getSelectedTokenAssetImage (state) {
const assetImages = state.metamask.assetImages || {}
const selectedToken = getSelectedToken(state) || {}
const { address } = selectedToken
return assetImages[address]
}
export function getAssetImages (state) {
const assetImages = state.metamask.assetImages || {}
return assetImages
}
export function getTokenExchangeRate (state, address) {
const contractExchangeRates = state.metamask.contractExchangeRates
return contractExchangeRates[address] || 0
}
export function conversionRateSelector (state) {
return state.metamask.conversionRate
}
export function getAddressBook (state) {
const network = state.metamask.network
if (!state.metamask.addressBook[network]) {
return []
}
return Object.values(state.metamask.addressBook[network])
}
export function getAddressBookEntry (state, address) {
const addressBook = getAddressBook(state)
const entry = addressBook.find(contact => contact.address === checksumAddress(address))
return entry
}
export function getAddressBookEntryName (state, address) {
const entry = getAddressBookEntry(state, address) || state.metamask.identities[address]
return entry && entry.name !== '' ? entry.name : addressSlicer(address)
}
export function getDaiV1Token (state) {
const OLD_DAI_CONTRACT_ADDRESS = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359'
const tokens = state.metamask.tokens || []
return tokens.find(({ address }) => checksumAddress(address) === OLD_DAI_CONTRACT_ADDRESS)
}
export function accountsWithSendEtherInfoSelector (state) {
const accounts = getMetaMaskAccounts(state)
const { identities } = state.metamask
const accountsWithSendEtherInfo = Object.entries(accounts).map(([key, account]) => {
return Object.assign({}, account, identities[key])
})
return accountsWithSendEtherInfo
}
export function getAccountsWithLabels (state) {
const accountsWithoutLabel = accountsWithSendEtherInfoSelector(state)
const accountsWithLabels = accountsWithoutLabel.map(account => {
const { address, name, balance } = account
return {
address,
truncatedAddress: `${address.slice(0, 6)}...${address.slice(-4)}`,
addressLabel: `${name} (...${address.slice(address.length - 4)})`,
label: name,
balance,
}
})
return accountsWithLabels
}
export function getCurrentAccountWithSendEtherInfo (state) {
const currentAddress = getSelectedAddress(state)
const accounts = accountsWithSendEtherInfoSelector(state)
return accounts.find(({ address }) => address === currentAddress)
}
export function getCurrentEthBalance (state) {
return getCurrentAccountWithSendEtherInfo(state).balance
}
export function getGasIsLoading (state) {
return state.appState.gasIsLoading
}
export function getForceGasMin (state) {
return state.metamask.send.forceGasMin
}
export function getSendFrom (state) {
return state.metamask.send.from
}
export function getSendAmount (state) {
return state.metamask.send.amount
}
export function getSendMaxModeState (state) {
return state.metamask.send.maxModeOn
}
export function getCurrentCurrency (state) {
return state.metamask.currentCurrency
}
export function getNativeCurrency (state) {
return state.metamask.nativeCurrency
}
export function getSelectedTokenToFiatRate (state) {
const selectedTokenExchangeRate = getSelectedTokenExchangeRate(state)
const conversionRate = conversionRateSelector(state)
const tokenToFiatRate = multiplyCurrencies(
conversionRate,
selectedTokenExchangeRate,
{ toNumericBase: 'dec' }
)
return tokenToFiatRate
}
export function getSelectedTokenContract (state) {
const selectedToken = getSelectedToken(state)
return selectedToken
? global.eth.contract(abi).at(selectedToken.address)
: null
}
export function getTotalUnapprovedCount ({ metamask }) {
const {
unapprovedTxs = {},
unapprovedMsgCount,
unapprovedPersonalMsgCount,
unapprovedTypedMessagesCount,
} = metamask
return Object.keys(unapprovedTxs).length + unapprovedMsgCount + unapprovedPersonalMsgCount +
unapprovedTypedMessagesCount
}
export function getIsMainnet (state) {
const networkType = getNetworkIdentifier(state)
return networkType === NETWORK_TYPES.MAINNET
}
export function isEthereumNetwork (state) {
const networkType = getNetworkIdentifier(state)
const {
KOVAN,
MAINNET,
RINKEBY,
ROPSTEN,
GOERLI,
} = NETWORK_TYPES
return [ KOVAN, MAINNET, RINKEBY, ROPSTEN, GOERLI].includes(networkType)
}
export function preferencesSelector ({ metamask }) {
return metamask.preferences
}
export function getAdvancedInlineGasShown (state) {
return Boolean(state.metamask.featureFlags.advancedInlineGas)
}
export function getUseNonceField (state) {
return Boolean(state.metamask.useNonceField)
}
export function getCustomNonceValue (state) {
return String(state.metamask.customNonceValue)
}
export function getPermissionsDescriptions (state) {
return state.metamask.permissionsDescriptions
}
export function getPermissionsRequests (state) {
return state.metamask.permissionsRequests
}
export function getDomainMetadata (state) {
return state.metamask.domainMetadata
}
export function getActiveTab (state) {
return state.activeTab
}
export function getMetaMetricState (state) {
return {
network: getCurrentNetworkId(state),
activeCurrency: getSelectedAsset(state),
accountType: getAccountType(state),
metaMetricsId: state.metamask.metaMetricsId,
numberOfTokens: getNumberOfTokens(state),
numberOfAccounts: getNumberOfAccounts(state),
participateInMetaMetrics: state.metamask.participateInMetaMetrics,
}
}
export function getRpcPrefsForCurrentProvider (state) {
const { frequentRpcListDetail, provider } = state.metamask
const selectRpcInfo = frequentRpcListDetail.find(rpcInfo => rpcInfo.rpcUrl === provider.rpcTarget)
const { rpcPrefs = {} } = selectRpcInfo || {}
return rpcPrefs
}
export function getKnownMethodData (state, data) {
if (!data) {
return null
}
const prefixedData = addHexPrefix(data)
const fourBytePrefix = prefixedData.slice(0, 10)
const { knownMethodData } = state.metamask
return knownMethodData && knownMethodData[fourBytePrefix]
}
export function getFeatureFlags (state) {
return state.metamask.featureFlags
}
export function getFirstPermissionRequest (state) {
const requests = getPermissionsRequests(state)
return requests && requests[0] ? requests[0] : null
}
export function hasPermissionRequests (state) {
return Boolean(getFirstPermissionRequest(state))
}
export function getPermissionsDomains (state) {
return state.metamask.domains
}
export function getAddressConnectedDomainMap (state) {
const {
domains,
domainMetadata,
} = state.metamask
const addressConnectedIconMap = {}
if (domains) {
Object.keys(domains).forEach(domainKey => {
const { permissions } = domains[domainKey]
const { icon, name } = domainMetadata[domainKey] || {}
permissions.forEach(perm => {
const caveats = perm.caveats || []
const exposedAccountCaveat = caveats.find(caveat => caveat.name === 'exposedAccounts')
if (exposedAccountCaveat && exposedAccountCaveat.value && exposedAccountCaveat.value.length) {
exposedAccountCaveat.value.forEach(address => {
const nameToRender = name || domainKey
addressConnectedIconMap[address] = addressConnectedIconMap[address]
? { ...addressConnectedIconMap[address], [domainKey]: { icon, name: nameToRender } }
: { [domainKey]: { icon, name: nameToRender } }
})
}
})
})
}
return addressConnectedIconMap
}
export function getDomainToConnectedAddressMap (state) {
const { domains = {} } = state.metamask
const domainToConnectedAddressMap = mapObjectValues(domains, (_, { permissions }) => {
const ethAccountsPermissions = permissions.filter(permission => permission.parentCapability === 'eth_accounts')
const ethAccountsPermissionsExposedAccountAddresses = ethAccountsPermissions.map(permission => {
const caveats = permission.caveats
const exposedAccountsCaveats = caveats.filter(caveat => caveat.name === 'exposedAccounts')
const exposedAccountsAddresses = exposedAccountsCaveats.map(caveat => caveat.value[0])
return exposedAccountsAddresses
})
const allAddressesConnectedToDomain = ethAccountsPermissionsExposedAccountAddresses.reduce((acc, arrayOfAddresses) => {
return [ ...acc, ...arrayOfAddresses ]
}, [])
return allAddressesConnectedToDomain
})
return domainToConnectedAddressMap
}
export function getAddressConnectedToCurrentTab (state) {
const domainToConnectedAddressMap = getDomainToConnectedAddressMap(state)
const originOfCurrentTab = getOriginOfCurrentTab(state)
const addressesConnectedToCurrentTab = domainToConnectedAddressMap[originOfCurrentTab]
const addressConnectedToCurrentTab = addressesConnectedToCurrentTab && addressesConnectedToCurrentTab[0]
return addressConnectedToCurrentTab
}
export function getRenderablePermissionsDomains (state) {
const {
domains = {},
domainMetadata,
permissionsHistory,
permissionsDescriptions,
selectedAddress,
} = state.metamask
const renderableDomains = Object.keys(domains).reduce((acc, domainKey) => {
const { permissions } = domains[domainKey]
const permissionsWithCaveatsForSelectedAddress = permissions.filter(perm => {
const caveats = perm.caveats || []
const exposedAccountCaveat = caveats.find(caveat => caveat.name === 'exposedAccounts')
const exposedAccountCaveatValue = exposedAccountCaveat && exposedAccountCaveat.value && exposedAccountCaveat.value.length
? exposedAccountCaveat.value[0]
: {}
return exposedAccountCaveatValue === selectedAddress
})
if (permissionsWithCaveatsForSelectedAddress.length) {
const permissionKeys = permissions.map(permission => permission.parentCapability)
const {
name,
icon,
extensionId,
} = domainMetadata[domainKey] || {}
const permissionsHistoryForDomain = permissionsHistory[domainKey] || {}
const ethAccountsPermissionsForDomain = permissionsHistoryForDomain['eth_accounts'] || {}
const accountsLastConnectedTime = ethAccountsPermissionsForDomain.accounts || {}
const selectedAddressLastConnectedTime = accountsLastConnectedTime[selectedAddress]
const lastConnectedTime = selectedAddressLastConnectedTime
? formatDate(selectedAddressLastConnectedTime, 'yyyy-M-d')
: ''
return [ ...acc, {
name: name || domainKey,
secondaryName: name ? domainKey : '',
icon,
key: domainKey,
lastConnectedTime,
permissionDescriptions: permissionKeys.map(permissionKey => permissionsDescriptions[permissionKey]),
extensionId,
}]
} else {
return acc
}
}, [])
return renderableDomains
}
export function getOriginOfCurrentTab (state) {
const { activeTab } = state
return activeTab && activeTab.url && getOriginFromUrl(activeTab.url)
}
export function getLastConnectedInfo (state) {
const { permissionsHistory = {} } = state.metamask
const lastConnectedInfoData = Object.keys(permissionsHistory).reduce((acc, origin) => {
const ethAccountsHistory = JSON.parse(JSON.stringify(permissionsHistory[origin]['eth_accounts']))
return {
...acc,
[origin]: ethAccountsHistory.accounts,
}
}, {})
return lastConnectedInfoData
}
export function getIpfsGateway (state) {
return state.metamask.ipfsGateway
}