import EventEmitter from 'events'; import pump from 'pump'; import { ObservableStore } from '@metamask/obs-store'; import { storeAsStream } from '@metamask/obs-store/dist/asStream'; import { JsonRpcEngine } from 'json-rpc-engine'; import { debounce } from 'lodash'; import createEngineStream from 'json-rpc-middleware-stream/engineStream'; import createFilterMiddleware from 'eth-json-rpc-filters'; import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager'; import { providerAsMiddleware } from 'eth-json-rpc-middleware'; import KeyringController from 'eth-keyring-controller'; import { errorCodes as rpcErrorCodes, ethErrors } from 'eth-rpc-errors'; import { Mutex } from 'await-semaphore'; import { stripHexPrefix } from 'ethereumjs-util'; import log from 'loglevel'; import TrezorKeyring from 'eth-trezor-keyring'; import LedgerBridgeKeyring from '@metamask/eth-ledger-bridge-keyring'; import LatticeKeyring from 'eth-lattice-keyring'; import { MetaMaskKeyring as QRHardwareKeyring } from '@keystonehq/metamask-airgapped-keyring'; import EthQuery from 'eth-query'; import nanoid from 'nanoid'; import { captureException } from '@sentry/browser'; import { AddressBookController, ApprovalController, ControllerMessenger, CurrencyRateController, PhishingController, NotificationController, GasFeeController, TokenListController, TokensController, TokenRatesController, CollectiblesController, AssetsContractController, CollectibleDetectionController, } from '@metamask/controllers'; import { PermissionController, SubjectMetadataController, } from '@metamask/snap-controllers'; import { TRANSACTION_STATUSES, TRANSACTION_TYPES, } from '../../shared/constants/transaction'; import { GAS_API_BASE_URL, GAS_DEV_API_BASE_URL, SWAPS_CLIENT_ID, } from '../../shared/constants/swaps'; import { MAINNET_CHAIN_ID } from '../../shared/constants/network'; import { DEVICE_NAMES, KEYRING_TYPES, } from '../../shared/constants/hardware-wallets'; import { CaveatTypes, RestrictedMethods, } from '../../shared/constants/permissions'; import { UI_NOTIFICATIONS } from '../../shared/notifications'; import { toChecksumHexAddress } from '../../shared/modules/hexstring-utils'; import { MILLISECOND } from '../../shared/constants/time'; import { POLLING_TOKEN_ENVIRONMENT_TYPES, SUBJECT_TYPES, } from '../../shared/constants/app'; import { hexToDecimal } from '../../ui/helpers/utils/conversions.util'; import { getTokenValueParam } from '../../ui/helpers/utils/token-util'; import { getTransactionData } from '../../ui/helpers/utils/transactions.util'; import { isEqualCaseInsensitive } from '../../ui/helpers/utils/util'; import ComposableObservableStore from './lib/ComposableObservableStore'; import AccountTracker from './lib/account-tracker'; import createLoggerMiddleware from './lib/createLoggerMiddleware'; import createMethodMiddleware from './lib/rpc-method-middleware'; import createOriginMiddleware from './lib/createOriginMiddleware'; import createTabIdMiddleware from './lib/createTabIdMiddleware'; import createOnboardingMiddleware from './lib/createOnboardingMiddleware'; import { setupMultiplex } from './lib/stream-utils'; import EnsController from './controllers/ens'; import NetworkController, { NETWORK_EVENTS } from './controllers/network'; import PreferencesController from './controllers/preferences'; import AppStateController from './controllers/app-state'; import CachedBalancesController from './controllers/cached-balances'; import AlertController from './controllers/alert'; import OnboardingController from './controllers/onboarding'; import ThreeBoxController from './controllers/threebox'; import IncomingTransactionsController from './controllers/incoming-transactions'; import MessageManager, { normalizeMsgData } from './lib/message-manager'; import DecryptMessageManager from './lib/decrypt-message-manager'; import EncryptionPublicKeyManager from './lib/encryption-public-key-manager'; import PersonalMessageManager from './lib/personal-message-manager'; import TypedMessageManager from './lib/typed-message-manager'; import TransactionController from './controllers/transactions'; import DetectTokensController from './controllers/detect-tokens'; import SwapsController from './controllers/swaps'; import accountImporter from './account-import-strategies'; import seedPhraseVerifier from './lib/seed-phrase-verifier'; import MetaMetricsController from './controllers/metametrics'; import { segment } from './lib/segment'; import createMetaRPCHandler from './lib/createMetaRPCHandler'; import { CaveatMutatorFactories, getCaveatSpecifications, getChangedAccounts, getPermissionBackgroundApiMethods, getPermissionSpecifications, getPermittedAccountsByOrigin, PermissionLogController, NOTIFICATION_NAMES, unrestrictedMethods, } from './controllers/permissions'; export const METAMASK_CONTROLLER_EVENTS = { // Fired after state changes that impact the extension badge (unapproved msg count) // The process of updating the badge happens in app/scripts/background.js. UPDATE_BADGE: 'updateBadge', // TODO: Add this and similar enums to @metamask/controllers and export them APPROVAL_STATE_CHANGE: 'ApprovalController:stateChange', }; export default class MetamaskController extends EventEmitter { /** * @param {Object} opts */ constructor(opts) { super(); this.defaultMaxListeners = 20; this.sendUpdate = debounce( this.privateSendUpdate.bind(this), MILLISECOND * 200, ); this.opts = opts; this.extension = opts.extension; this.platform = opts.platform; this.notificationManager = opts.notificationManager; const initState = opts.initState || {}; const version = this.platform.getVersion(); 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; this.getRequestAccountTabIds = opts.getRequestAccountTabIds; this.getOpenMetamaskTabsIds = opts.getOpenMetamaskTabsIds; this.controllerMessenger = new ControllerMessenger(); // observable state store this.store = new ComposableObservableStore({ state: initState, controllerMessenger: this.controllerMessenger, persist: true, }); // external connections by origin // Do not modify directly. Use the associated methods. this.connections = {}; // lock to ensure only one vault created at once this.createVaultMutex = new Mutex(); this.extension.runtime.onInstalled.addListener((details) => { if (details.reason === 'update' && version === '8.1.0') { this.platform.openExtensionInBrowser(); } }); // next, we will initialize the controllers // controller initialization order matters this.approvalController = new ApprovalController({ messenger: this.controllerMessenger.getRestricted({ name: 'ApprovalController', }), showApprovalRequest: opts.showUserConfirmation, }); this.networkController = new NetworkController(initState.NetworkController); this.networkController.setInfuraProjectId(opts.infuraProjectId); // now we can initialize the RPC provider, which other controllers require this.initializeProvider(); this.provider = this.networkController.getProviderAndBlockTracker().provider; this.blockTracker = this.networkController.getProviderAndBlockTracker().blockTracker; this.preferencesController = new PreferencesController({ initState: initState.PreferencesController, initLangCode: opts.initLangCode, openPopup: opts.openPopup, network: this.networkController, provider: this.provider, migrateAddressBookState: this.migrateAddressBookState.bind(this), }); this.tokensController = new TokensController({ onPreferencesStateChange: this.preferencesController.store.subscribe.bind( this.preferencesController.store, ), onNetworkStateChange: this.networkController.store.subscribe.bind( this.networkController.store, ), config: { provider: this.provider }, state: initState.TokensController, }); this.assetsContractController = new AssetsContractController({ provider: this.provider, }); this.collectiblesController = new CollectiblesController( { onPreferencesStateChange: this.preferencesController.store.subscribe.bind( this.preferencesController.store, ), onNetworkStateChange: this.networkController.store.subscribe.bind( this.networkController.store, ), getERC721AssetName: this.assetsContractController.getERC721AssetName.bind( this.assetsContractController, ), getERC721AssetSymbol: this.assetsContractController.getERC721AssetSymbol.bind( this.assetsContractController, ), getERC721TokenURI: this.assetsContractController.getERC721TokenURI.bind( this.assetsContractController, ), getERC721OwnerOf: this.assetsContractController.getERC721OwnerOf.bind( this.assetsContractController, ), getERC1155BalanceOf: this.assetsContractController.getERC1155BalanceOf.bind( this.assetsContractController, ), getERC1155TokenURI: this.assetsContractController.getERC1155TokenURI.bind( this.assetsContractController, ), }, {}, initState.CollectiblesController, ); process.env.COLLECTIBLES_V1 && (this.collectibleDetectionController = new CollectibleDetectionController( { onCollectiblesStateChange: (listener) => this.collectiblesController.subscribe(listener), onPreferencesStateChange: this.preferencesController.store.subscribe.bind( this.preferencesController.store, ), onNetworkStateChange: this.networkController.store.subscribe.bind( this.networkController.store, ), getOpenSeaApiKey: () => this.collectiblesController.openSeaApiKey, getBalancesInSingleCall: this.assetsContractController.getBalancesInSingleCall.bind( this.assetsContractController, ), addCollectible: this.collectiblesController.addCollectible.bind( this.collectiblesController, ), getCollectiblesState: () => this.collectiblesController.state, }, )); this.metaMetricsController = new MetaMetricsController({ segment, preferencesStore: this.preferencesController.store, onNetworkDidChange: this.networkController.on.bind( this.networkController, NETWORK_EVENTS.NETWORK_DID_CHANGE, ), getNetworkIdentifier: this.networkController.getNetworkIdentifier.bind( this.networkController, ), getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), version: this.platform.getVersion(), environment: process.env.METAMASK_ENVIRONMENT, initState: initState.MetaMetricsController, captureException, }); const gasFeeMessenger = this.controllerMessenger.getRestricted({ name: 'GasFeeController', }); const gasApiBaseUrl = process.env.SWAPS_USE_DEV_APIS ? GAS_DEV_API_BASE_URL : GAS_API_BASE_URL; this.gasFeeController = new GasFeeController({ interval: 10000, messenger: gasFeeMessenger, clientId: SWAPS_CLIENT_ID, getProvider: () => this.networkController.getProviderAndBlockTracker().provider, onNetworkStateChange: this.networkController.on.bind( this.networkController, NETWORK_EVENTS.NETWORK_DID_CHANGE, ), getCurrentNetworkEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( this.networkController, ), getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind( this, ), legacyAPIEndpoint: `${gasApiBaseUrl}/networks//gasPrices`, EIP1559APIEndpoint: `${gasApiBaseUrl}/networks//suggestedGasFees`, getCurrentNetworkLegacyGasAPICompatibility: () => { const chainId = this.networkController.getCurrentChainId(); return process.env.IN_TEST || chainId === MAINNET_CHAIN_ID; }, getChainId: () => { return process.env.IN_TEST ? MAINNET_CHAIN_ID : this.networkController.getCurrentChainId(); }, }); this.qrHardwareKeyring = new QRHardwareKeyring(); this.appStateController = new AppStateController({ addUnlockListener: this.on.bind(this, 'unlock'), isUnlocked: this.isUnlocked.bind(this), initState: initState.AppStateController, onInactiveTimeout: () => this.setLocked(), showUnlockRequest: opts.showUserConfirmation, preferencesStore: this.preferencesController.store, qrHardwareStore: this.qrHardwareKeyring.getMemStore(), }); const currencyRateMessenger = this.controllerMessenger.getRestricted({ name: 'CurrencyRateController', }); this.currencyRateController = new CurrencyRateController({ includeUSDRate: true, messenger: currencyRateMessenger, state: initState.CurrencyController, }); const tokenListMessenger = this.controllerMessenger.getRestricted({ name: 'TokenListController', }); this.tokenListController = new TokenListController({ chainId: hexToDecimal(this.networkController.getCurrentChainId()), useStaticTokenList: !this.preferencesController.store.getState() .useTokenDetection, onNetworkStateChange: (cb) => this.networkController.store.subscribe((networkState) => { const modifiedNetworkState = { ...networkState, provider: { ...networkState.provider, chainId: hexToDecimal(networkState.provider.chainId), }, }; return cb(modifiedNetworkState); }), onPreferencesStateChange: (cb) => this.preferencesController.store.subscribe((preferencesState) => { const modifiedPreferencesState = { ...preferencesState, useStaticTokenList: !this.preferencesController.store.getState() .useTokenDetection, }; return cb(modifiedPreferencesState); }), messenger: tokenListMessenger, state: initState.TokenListController, }); this.phishingController = new PhishingController(); this.notificationController = new NotificationController( { allNotifications: UI_NOTIFICATIONS }, initState.NotificationController, ); // token exchange rate tracker this.tokenRatesController = new TokenRatesController({ onTokensStateChange: (listener) => this.tokensController.subscribe(listener), onCurrencyRateStateChange: (listener) => this.controllerMessenger.subscribe( `${this.currencyRateController.name}:stateChange`, listener, ), onNetworkStateChange: (cb) => this.networkController.store.subscribe((networkState) => { const modifiedNetworkState = { ...networkState, provider: { ...networkState.provider, chainId: hexToDecimal(networkState.provider.chainId), }, }; return cb(modifiedNetworkState); }), }); this.ensController = new EnsController({ provider: this.provider, getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), onNetworkDidChange: this.networkController.on.bind( this.networkController, NETWORK_EVENTS.NETWORK_DID_CHANGE, ), }); this.incomingTransactionsController = new IncomingTransactionsController({ blockTracker: this.blockTracker, onNetworkDidChange: this.networkController.on.bind( this.networkController, NETWORK_EVENTS.NETWORK_DID_CHANGE, ), getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), preferencesController: this.preferencesController, initState: initState.IncomingTransactionsController, }); // account tracker watches balances, nonces, and any code at their address this.accountTracker = new AccountTracker({ provider: this.provider, blockTracker: this.blockTracker, getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), }); // start and stop polling for balances based on activeControllerConnections this.on('controllerConnectionChanged', (activeControllerConnections) => { if (activeControllerConnections > 0) { this.accountTracker.start(); this.incomingTransactionsController.start(); this.currencyRateController.start(); this.tokenListController.start(); } else { this.accountTracker.stop(); this.incomingTransactionsController.stop(); this.currencyRateController.stop(); this.tokenListController.stop(); } }); this.cachedBalancesController = new CachedBalancesController({ accountTracker: this.accountTracker, getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), initState: initState.CachedBalancesController, }); this.onboardingController = new OnboardingController({ initState: initState.OnboardingController, }); this.tokensController.hub.on('pendingSuggestedAsset', async () => { await opts.openPopup(); }); const additionalKeyrings = [ TrezorKeyring, LedgerBridgeKeyring, LatticeKeyring, QRHardwareKeyring, ]; this.keyringController = new KeyringController({ keyringTypes: additionalKeyrings, initState: initState.KeyringController, encryptor: opts.encryptor || undefined, }); this.keyringController.memStore.subscribe((state) => this._onKeyringControllerUpdate(state), ); this.keyringController.on('unlock', () => this._onUnlock()); this.keyringController.on('lock', () => this._onLock()); const getIdentities = () => this.preferencesController.store.getState().identities; this.permissionController = new PermissionController({ messenger: this.controllerMessenger.getRestricted({ name: 'PermissionController', allowedActions: [ `${this.approvalController.name}:addRequest`, `${this.approvalController.name}:hasRequest`, `${this.approvalController.name}:acceptRequest`, `${this.approvalController.name}:rejectRequest`, ], }), state: initState.PermissionController, caveatSpecifications: getCaveatSpecifications({ getIdentities }), permissionSpecifications: getPermissionSpecifications({ getIdentities, getAllAccounts: this.keyringController.getAccounts.bind( this.keyringController, ), }), unrestrictedMethods, }); this.permissionLogController = new PermissionLogController({ restrictedMethods: new Set(Object.keys(RestrictedMethods)), initState: initState.PermissionLogController, }); this.subjectMetadataController = new SubjectMetadataController({ messenger: this.controllerMessenger.getRestricted({ name: 'SubjectMetadataController', allowedActions: [`${this.permissionController.name}:hasPermissions`], }), state: initState.SubjectMetadataController, subjectCacheLimit: 100, }); this.detectTokensController = new DetectTokensController({ preferences: this.preferencesController, tokensController: this.tokensController, network: this.networkController, keyringMemStore: this.keyringController.memStore, tokenList: this.tokenListController, }); this.addressBookController = new AddressBookController( undefined, initState.AddressBookController, ); this.alertController = new AlertController({ initState: initState.AlertController, preferencesStore: this.preferencesController.store, }); this.threeBoxController = new ThreeBoxController({ preferencesController: this.preferencesController, addressBookController: this.addressBookController, keyringController: this.keyringController, initState: initState.ThreeBoxController, getKeyringControllerState: this.keyringController.memStore.getState.bind( this.keyringController.memStore, ), version, trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }); this.txController = new TransactionController({ initState: initState.TransactionController || initState.TransactionManager, getPermittedAccounts: this.getPermittedAccounts.bind(this), getProviderConfig: this.networkController.getProviderConfig.bind( this.networkController, ), getCurrentNetworkEIP1559Compatibility: this.networkController.getEIP1559Compatibility.bind( this.networkController, ), getCurrentAccountEIP1559Compatibility: this.getCurrentAccountEIP1559Compatibility.bind( this, ), networkStore: this.networkController.networkStore, getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), preferencesStore: this.preferencesController.store, txHistoryLimit: 40, signTransaction: this.keyringController.signTransaction.bind( this.keyringController, ), provider: this.provider, blockTracker: this.blockTracker, trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), getParticipateInMetrics: () => this.metaMetricsController.state.participateInMetaMetrics, getEIP1559GasFeeEstimates: this.gasFeeController.fetchGasFeeEstimates.bind( this.gasFeeController, ), }); this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation()); this.txController.on(`tx:status-update`, async (txId, status) => { if ( status === TRANSACTION_STATUSES.CONFIRMED || status === TRANSACTION_STATUSES.FAILED ) { const txMeta = this.txController.txStateManager.getTransaction(txId); const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail(); let rpcPrefs = {}; if (txMeta.chainId) { const rpcSettings = frequentRpcListDetail.find( (rpc) => txMeta.chainId === rpc.chainId, ); rpcPrefs = rpcSettings?.rpcPrefs ?? {}; } this.platform.showTransactionNotification(txMeta, rpcPrefs); const { txReceipt } = txMeta; // if this is a transferFrom method generated from within the app it may be a collectible transfer transaction // in which case we will want to check and update ownership status of the transferred collectible. if ( txMeta.type === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM && txMeta.txParams !== undefined ) { const { data, to: contractAddress, from: userAddress, } = txMeta.txParams; const { chainId } = txMeta; const transactionData = getTransactionData(data); const tokenAmountOrTokenId = getTokenValueParam(transactionData); const { allCollectibles } = this.collectiblesController.state; // check if its a known collectible const knownCollectible = allCollectibles?.[userAddress]?.[ chainId ].find( ({ address, tokenId }) => isEqualCaseInsensitive(address, contractAddress) && tokenId === tokenAmountOrTokenId, ); // if it is we check and update ownership status. if (knownCollectible) { this.collectiblesController.checkAndUpdateSingleCollectibleOwnershipStatus( knownCollectible, false, // TODO add this when checkAndUpdateSingleCollectibleOwnershipStatus is updated // { userAddress, chainId }, ); } } const metamaskState = await this.getState(); if (txReceipt && txReceipt.status === '0x0') { this.metaMetricsController.trackEvent( { event: 'Tx Status Update: On-Chain Failure', category: 'Background', properties: { action: 'Transactions', errorMessage: txMeta.simulationFails?.reason, numberOfTokens: metamaskState.tokens.length, numberOfAccounts: Object.keys(metamaskState.accounts).length, }, }, { matomoEvent: true, }, ); } } }); this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, async () => { const { ticker } = this.networkController.getProviderConfig(); try { await this.currencyRateController.setNativeCurrency(ticker); } catch (error) { // TODO: Handle failure to get conversion rate more gracefully console.error(error); } }); this.networkController.lookupNetwork(); this.messageManager = new MessageManager({ metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }); this.personalMessageManager = new PersonalMessageManager({ metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }); this.decryptMessageManager = new DecryptMessageManager({ metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }); this.encryptionPublicKeyManager = new EncryptionPublicKeyManager({ metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }); this.typedMessageManager = new TypedMessageManager({ getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), metricsEvent: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), }); this.swapsController = new SwapsController({ getBufferedGasLimit: this.txController.txGasUtil.getBufferedGasLimit.bind( this.txController.txGasUtil, ), networkController: this.networkController, provider: this.provider, getProviderConfig: this.networkController.getProviderConfig.bind( this.networkController, ), getTokenRatesState: () => this.tokenRatesController.state, getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), getEIP1559GasFeeEstimates: this.gasFeeController.fetchGasFeeEstimates.bind( this.gasFeeController, ), }); // ensure accountTracker updates balances after network change this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => { this.accountTracker._updateAccounts(); }); // clear unapproved transactions and messages when the network will change this.networkController.on(NETWORK_EVENTS.NETWORK_WILL_CHANGE, () => { this.txController.txStateManager.clearUnapprovedTxs(); this.encryptionPublicKeyManager.clearUnapproved(); this.personalMessageManager.clearUnapproved(); this.typedMessageManager.clearUnapproved(); this.decryptMessageManager.clearUnapproved(); this.messageManager.clearUnapproved(); }); // ensure isClientOpenAndUnlocked is updated when memState updates this.on('update', (memState) => this._onStateUpdate(memState)); this.store.updateStructure({ AppStateController: this.appStateController.store, TransactionController: this.txController.store, KeyringController: this.keyringController.store, PreferencesController: this.preferencesController.store, MetaMetricsController: this.metaMetricsController.store, AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, NetworkController: this.networkController.store, CachedBalancesController: this.cachedBalancesController.store, AlertController: this.alertController.store, OnboardingController: this.onboardingController.store, IncomingTransactionsController: this.incomingTransactionsController.store, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController.store, SubjectMetadataController: this.subjectMetadataController, ThreeBoxController: this.threeBoxController.store, NotificationController: this.notificationController, GasFeeController: this.gasFeeController, TokenListController: this.tokenListController, TokensController: this.tokensController, CollectiblesController: this.collectiblesController, }); this.memStore = new ComposableObservableStore({ config: { AppStateController: this.appStateController.store, NetworkController: this.networkController.store, AccountTracker: this.accountTracker.store, TxController: this.txController.memStore, CachedBalancesController: this.cachedBalancesController.store, TokenRatesController: this.tokenRatesController, MessageManager: this.messageManager.memStore, PersonalMessageManager: this.personalMessageManager.memStore, DecryptMessageManager: this.decryptMessageManager.memStore, EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore, TypesMessageManager: this.typedMessageManager.memStore, KeyringController: this.keyringController.memStore, PreferencesController: this.preferencesController.store, MetaMetricsController: this.metaMetricsController.store, AddressBookController: this.addressBookController, CurrencyController: this.currencyRateController, AlertController: this.alertController.store, OnboardingController: this.onboardingController.store, IncomingTransactionsController: this.incomingTransactionsController .store, PermissionController: this.permissionController, PermissionLogController: this.permissionLogController.store, SubjectMetadataController: this.subjectMetadataController, ThreeBoxController: this.threeBoxController.store, SwapsController: this.swapsController.store, EnsController: this.ensController.store, ApprovalController: this.approvalController, NotificationController: this.notificationController, GasFeeController: this.gasFeeController, TokenListController: this.tokenListController, TokensController: this.tokensController, CollectiblesController: this.collectiblesController, }, controllerMessenger: this.controllerMessenger, }); this.memStore.subscribe(this.sendUpdate.bind(this)); const password = process.env.CONF?.PASSWORD; if ( password && !this.isUnlocked() && this.onboardingController.store.getState().completedOnboarding ) { this.submitPassword(password); } // Lazily update the store with the current extension environment this.extension.runtime.getPlatformInfo(({ os }) => { this.appStateController.setBrowserEnvironment( os, // This method is presently only supported by Firefox this.extension.runtime.getBrowserInfo === undefined ? 'chrome' : 'firefox', ); }); this.setupControllerEventSubscriptions(); // TODO:LegacyProvider: Delete this.publicConfigStore = this.createPublicConfigStore(); } /** * Sets up BaseController V2 event subscriptions. Currently, this includes * the subscriptions necessary to notify permission subjects of account * changes. * * Some of the subscriptions in this method are ControllerMessenger selector * event subscriptions. See the relevant @metamask/controllers documentation * for more information. * * Note that account-related notifications emitted when the extension * becomes unlocked are handled in MetaMaskController._onUnlock. */ setupControllerEventSubscriptions() { const handleAccountsChange = async (origin, newAccounts) => { if (this.isUnlocked()) { this.notifyConnections(origin, { method: NOTIFICATION_NAMES.accountsChanged, // This should be the same as the return value of `eth_accounts`, // namely an array of the current / most recently selected Ethereum // account. params: newAccounts.length < 2 ? // If the length is 1 or 0, the accounts are sorted by definition. newAccounts : // If the length is 2 or greater, we have to execute // `eth_accounts` vi this method. await this.getPermittedAccounts(origin), }); } this.permissionLogController.updateAccountsHistory(origin, newAccounts); }; // This handles account changes whenever the selected address changes. let lastSelectedAddress; this.preferencesController.store.subscribe(async ({ selectedAddress }) => { if (selectedAddress && selectedAddress !== lastSelectedAddress) { lastSelectedAddress = selectedAddress; const permittedAccountsMap = getPermittedAccountsByOrigin( this.permissionController.state, ); for (const [origin, accounts] of permittedAccountsMap.entries()) { if (accounts.includes(selectedAddress)) { handleAccountsChange(origin, accounts); } } } }); // This handles account changes every time relevant permission state // changes, for any reason. this.controllerMessenger.subscribe( `${this.permissionController.name}:stateChange`, async (currentValue, previousValue) => { const changedAccounts = getChangedAccounts(currentValue, previousValue); for (const [origin, accounts] of changedAccounts.entries()) { handleAccountsChange(origin, accounts); } }, getPermittedAccountsByOrigin, ); } /** * Constructor helper: initialize a provider. */ initializeProvider() { const version = this.platform.getVersion(); const providerOpts = { static: { eth_syncing: false, web3_clientVersion: `MetaMask/v${version}`, }, version, // account mgmt getAccounts: async ({ origin }) => { if (origin === 'metamask') { const selectedAddress = this.preferencesController.getSelectedAddress(); return selectedAddress ? [selectedAddress] : []; } else if (this.isUnlocked()) { return await this.getPermittedAccounts(origin); } return []; // changing this is a breaking change }, // tx signing processTransaction: this.newUnapprovedTransaction.bind(this), // msg signing processEthSignMessage: this.newUnsignedMessage.bind(this), processTypedMessage: this.newUnsignedTypedMessage.bind(this), processTypedMessageV3: this.newUnsignedTypedMessage.bind(this), processTypedMessageV4: this.newUnsignedTypedMessage.bind(this), processPersonalMessage: this.newUnsignedPersonalMessage.bind(this), processDecryptMessage: this.newRequestDecryptMessage.bind(this), processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), getPendingNonce: this.getPendingNonce.bind(this), getPendingTransactionByHash: (hash) => this.txController.getTransactions({ searchCriteria: { hash, status: TRANSACTION_STATUSES.SUBMITTED, }, })[0], }; const providerProxy = this.networkController.initializeProvider( providerOpts, ); return providerProxy; } /** * TODO:LegacyProvider: Delete * Constructor helper: initialize a public config store. * This store is used to make some config info available to Dapps synchronously. */ createPublicConfigStore() { // subset of state for metamask inpage provider const publicConfigStore = new ObservableStore(); const { networkController } = this; // setup memStore subscription hooks this.on('update', updatePublicConfigStore); updatePublicConfigStore(this.getState()); function updatePublicConfigStore(memState) { const chainId = networkController.getCurrentChainId(); if (memState.network !== 'loading') { publicConfigStore.putState(selectPublicState(chainId, memState)); } } function selectPublicState(chainId, { isUnlocked, network }) { return { isUnlocked, chainId, networkVersion: network, }; } return publicConfigStore; } /** * Gets relevant state for the provider of an external origin. * * @param {string} origin - The origin to get the provider state for. * @returns {Promise<{ * isUnlocked: boolean, * networkVersion: string, * chainId: string, * accounts: string[], * }>} An object with relevant state properties. */ async getProviderState(origin) { return { isUnlocked: this.isUnlocked(), ...this.getProviderNetworkState(), accounts: await this.getPermittedAccounts(origin), }; } /** * Gets network state relevant for external providers. * * @param {Object} [memState] - The MetaMask memState. If not provided, * this function will retrieve the most recent state. * @returns {Object} An object with relevant network state properties. */ getProviderNetworkState(memState) { const { network } = memState || this.getState(); return { chainId: this.networkController.getCurrentChainId(), networkVersion: network, }; } //============================================================================= // EXPOSED TO THE UI SUBSYSTEM //============================================================================= /** * The metamask-state of the various controllers, made available to the UI * * @returns {Object} status */ getState() { const { vault } = this.keyringController.store.getState(); const isInitialized = Boolean(vault); return { isInitialized, ...this.memStore.getFlatState(), }; } /** * Returns an Object containing API Callback Functions. * These functions are the interface for the UI. * The API object can be transmitted over a stream via JSON-RPC. * * @returns {Object} Object containing API functions. */ getApi() { const { addressBookController, alertController, approvalController, appStateController, collectiblesController, collectibleDetectionController, assetsContractController, currencyRateController, detectTokensController, ensController, gasFeeController, keyringController, metaMetricsController, networkController, notificationController, onboardingController, permissionController, preferencesController, qrHardwareKeyring, swapsController, threeBoxController, tokensController, txController, } = this; return { // etc getState: this.getState.bind(this), setCurrentCurrency: currencyRateController.setCurrentCurrency.bind( currencyRateController, ), setUseBlockie: preferencesController.setUseBlockie.bind( preferencesController, ), setUseNonceField: preferencesController.setUseNonceField.bind( preferencesController, ), setUsePhishDetect: preferencesController.setUsePhishDetect.bind( preferencesController, ), setUseTokenDetection: preferencesController.setUseTokenDetection.bind( preferencesController, ), setUseCollectibleDetection: preferencesController.setUseCollectibleDetection.bind( preferencesController, ), setOpenSeaEnabled: preferencesController.setOpenSeaEnabled.bind( preferencesController, ), setIpfsGateway: preferencesController.setIpfsGateway.bind( preferencesController, ), setParticipateInMetaMetrics: metaMetricsController.setParticipateInMetaMetrics.bind( metaMetricsController, ), setCurrentLocale: preferencesController.setCurrentLocale.bind( preferencesController, ), markPasswordForgotten: this.markPasswordForgotten.bind(this), unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this), safelistPhishingDomain: this.safelistPhishingDomain.bind(this), getRequestAccountTabIds: this.getRequestAccountTabIds, getOpenMetamaskTabsIds: this.getOpenMetamaskTabsIds, markNotificationPopupAsAutomaticallyClosed: () => this.notificationManager.markAsAutomaticallyClosed(), // primary HD keyring management addNewAccount: this.addNewAccount.bind(this), verifySeedPhrase: this.verifySeedPhrase.bind(this), resetAccount: this.resetAccount.bind(this), removeAccount: this.removeAccount.bind(this), importAccountWithStrategy: this.importAccountWithStrategy.bind(this), // hardware wallets connectHardware: this.connectHardware.bind(this), forgetDevice: this.forgetDevice.bind(this), checkHardwareStatus: this.checkHardwareStatus.bind(this), unlockHardwareWalletAccount: this.unlockHardwareWalletAccount.bind(this), setLedgerTransportPreference: this.setLedgerTransportPreference.bind( this, ), attemptLedgerTransportCreation: this.attemptLedgerTransportCreation.bind( this, ), establishLedgerTransportPreference: this.establishLedgerTransportPreference.bind( this, ), // qr hardware devices submitQRHardwareCryptoHDKey: qrHardwareKeyring.submitCryptoHDKey.bind( qrHardwareKeyring, ), submitQRHardwareCryptoAccount: qrHardwareKeyring.submitCryptoAccount.bind( qrHardwareKeyring, ), cancelSyncQRHardware: qrHardwareKeyring.cancelSync.bind( qrHardwareKeyring, ), submitQRHardwareSignature: qrHardwareKeyring.submitSignature.bind( qrHardwareKeyring, ), cancelQRHardwareSignRequest: qrHardwareKeyring.cancelSignRequest.bind( qrHardwareKeyring, ), // mobile fetchInfoToSync: this.fetchInfoToSync.bind(this), // vault management submitPassword: this.submitPassword.bind(this), verifyPassword: this.verifyPassword.bind(this), // network management setProviderType: networkController.setProviderType.bind( networkController, ), rollbackToPreviousProvider: networkController.rollbackToPreviousProvider.bind( networkController, ), setCustomRpc: this.setCustomRpc.bind(this), updateAndSetCustomRpc: this.updateAndSetCustomRpc.bind(this), delCustomRpc: this.delCustomRpc.bind(this), // PreferencesController setSelectedAddress: preferencesController.setSelectedAddress.bind( preferencesController, ), addToken: tokensController.addToken.bind(tokensController), rejectWatchAsset: tokensController.rejectWatchAsset.bind( tokensController, ), acceptWatchAsset: tokensController.acceptWatchAsset.bind( tokensController, ), updateTokenType: tokensController.updateTokenType.bind(tokensController), removeToken: tokensController.removeAndIgnoreToken.bind(tokensController), setAccountLabel: preferencesController.setAccountLabel.bind( preferencesController, ), setFeatureFlag: preferencesController.setFeatureFlag.bind( preferencesController, ), setPreference: preferencesController.setPreference.bind( preferencesController, ), addKnownMethodData: preferencesController.addKnownMethodData.bind( preferencesController, ), setDismissSeedBackUpReminder: preferencesController.setDismissSeedBackUpReminder.bind( preferencesController, ), setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind( preferencesController, ), setEIP1559V2Enabled: preferencesController.setEIP1559V2Enabled.bind( preferencesController, ), // AssetsContractController getTokenStandardAndDetails: assetsContractController.getTokenStandardAndDetails.bind( assetsContractController, ), // CollectiblesController addCollectible: collectiblesController.addCollectible.bind( collectiblesController, ), addCollectibleVerifyOwnership: collectiblesController.addCollectibleVerifyOwnership.bind( collectiblesController, ), removeAndIgnoreCollectible: collectiblesController.removeAndIgnoreCollectible.bind( collectiblesController, ), removeCollectible: collectiblesController.removeCollectible.bind( collectiblesController, ), checkAndUpdateAllCollectiblesOwnershipStatus: collectiblesController.checkAndUpdateAllCollectiblesOwnershipStatus.bind( collectiblesController, ), checkAndUpdateSingleCollectibleOwnershipStatus: collectiblesController.checkAndUpdateSingleCollectibleOwnershipStatus.bind( collectiblesController, ), isCollectibleOwner: collectiblesController.isCollectibleOwner.bind( collectiblesController, ), // AddressController setAddressBook: addressBookController.set.bind(addressBookController), removeFromAddressBook: addressBookController.delete.bind( addressBookController, ), // AppStateController setLastActiveTime: appStateController.setLastActiveTime.bind( appStateController, ), setDefaultHomeActiveTabName: appStateController.setDefaultHomeActiveTabName.bind( appStateController, ), setConnectedStatusPopoverHasBeenShown: appStateController.setConnectedStatusPopoverHasBeenShown.bind( appStateController, ), setRecoveryPhraseReminderHasBeenShown: appStateController.setRecoveryPhraseReminderHasBeenShown.bind( appStateController, ), setRecoveryPhraseReminderLastShown: appStateController.setRecoveryPhraseReminderLastShown.bind( appStateController, ), setShowTestnetMessageInDropdown: appStateController.setShowTestnetMessageInDropdown.bind( appStateController, ), setCollectiblesDetectionNoticeDismissed: appStateController.setCollectiblesDetectionNoticeDismissed.bind( appStateController, ), setEnableEIP1559V2NoticeDismissed: appStateController.setEnableEIP1559V2NoticeDismissed.bind( appStateController, ), // EnsController tryReverseResolveAddress: ensController.reverseResolveAddress.bind( ensController, ), // KeyringController setLocked: this.setLocked.bind(this), createNewVaultAndKeychain: this.createNewVaultAndKeychain.bind(this), createNewVaultAndRestore: this.createNewVaultAndRestore.bind(this), exportAccount: keyringController.exportAccount.bind(keyringController), // txController cancelTransaction: txController.cancelTransaction.bind(txController), updateTransaction: txController.updateTransaction.bind(txController), updateAndApproveTransaction: txController.updateAndApproveTransaction.bind( txController, ), createCancelTransaction: this.createCancelTransaction.bind(this), createSpeedUpTransaction: this.createSpeedUpTransaction.bind(this), estimateGas: this.estimateGas.bind(this), getNextNonce: this.getNextNonce.bind(this), addUnapprovedTransaction: txController.addUnapprovedTransaction.bind( txController, ), // messageManager signMessage: this.signMessage.bind(this), cancelMessage: this.cancelMessage.bind(this), // personalMessageManager signPersonalMessage: this.signPersonalMessage.bind(this), cancelPersonalMessage: this.cancelPersonalMessage.bind(this), // typedMessageManager signTypedMessage: this.signTypedMessage.bind(this), cancelTypedMessage: this.cancelTypedMessage.bind(this), // decryptMessageManager decryptMessage: this.decryptMessage.bind(this), decryptMessageInline: this.decryptMessageInline.bind(this), cancelDecryptMessage: this.cancelDecryptMessage.bind(this), // EncryptionPublicKeyManager encryptionPublicKey: this.encryptionPublicKey.bind(this), cancelEncryptionPublicKey: this.cancelEncryptionPublicKey.bind(this), // onboarding controller setSeedPhraseBackedUp: onboardingController.setSeedPhraseBackedUp.bind( onboardingController, ), completeOnboarding: onboardingController.completeOnboarding.bind( onboardingController, ), setFirstTimeFlowType: onboardingController.setFirstTimeFlowType.bind( onboardingController, ), // alert controller setAlertEnabledness: alertController.setAlertEnabledness.bind( alertController, ), setUnconnectedAccountAlertShown: alertController.setUnconnectedAccountAlertShown.bind( alertController, ), setWeb3ShimUsageAlertDismissed: alertController.setWeb3ShimUsageAlertDismissed.bind( alertController, ), // 3Box setThreeBoxSyncingPermission: threeBoxController.setThreeBoxSyncingPermission.bind( threeBoxController, ), restoreFromThreeBox: threeBoxController.restoreFromThreeBox.bind( threeBoxController, ), setShowRestorePromptToFalse: threeBoxController.setShowRestorePromptToFalse.bind( threeBoxController, ), getThreeBoxLastUpdated: threeBoxController.getLastUpdated.bind( threeBoxController, ), turnThreeBoxSyncingOn: threeBoxController.turnThreeBoxSyncingOn.bind( threeBoxController, ), initializeThreeBox: this.initializeThreeBox.bind(this), // permissions removePermissionsFor: permissionController.revokePermissions.bind( permissionController, ), approvePermissionsRequest: permissionController.acceptPermissionsRequest.bind( permissionController, ), rejectPermissionsRequest: permissionController.rejectPermissionsRequest.bind( permissionController, ), ...getPermissionBackgroundApiMethods(permissionController), // swaps fetchAndSetQuotes: swapsController.fetchAndSetQuotes.bind( swapsController, ), setSelectedQuoteAggId: swapsController.setSelectedQuoteAggId.bind( swapsController, ), resetSwapsState: swapsController.resetSwapsState.bind(swapsController), setSwapsTokens: swapsController.setSwapsTokens.bind(swapsController), clearSwapsQuotes: swapsController.clearSwapsQuotes.bind(swapsController), setApproveTxId: swapsController.setApproveTxId.bind(swapsController), setTradeTxId: swapsController.setTradeTxId.bind(swapsController), setSwapsTxGasPrice: swapsController.setSwapsTxGasPrice.bind( swapsController, ), setSwapsTxGasLimit: swapsController.setSwapsTxGasLimit.bind( swapsController, ), setSwapsTxMaxFeePerGas: swapsController.setSwapsTxMaxFeePerGas.bind( swapsController, ), setSwapsTxMaxFeePriorityPerGas: swapsController.setSwapsTxMaxFeePriorityPerGas.bind( swapsController, ), safeRefetchQuotes: swapsController.safeRefetchQuotes.bind( swapsController, ), stopPollingForQuotes: swapsController.stopPollingForQuotes.bind( swapsController, ), setBackgroundSwapRouteState: swapsController.setBackgroundSwapRouteState.bind( swapsController, ), resetPostFetchState: swapsController.resetPostFetchState.bind( swapsController, ), setSwapsErrorKey: swapsController.setSwapsErrorKey.bind(swapsController), setInitialGasEstimate: swapsController.setInitialGasEstimate.bind( swapsController, ), setCustomApproveTxData: swapsController.setCustomApproveTxData.bind( swapsController, ), setSwapsLiveness: swapsController.setSwapsLiveness.bind(swapsController), setSwapsUserFeeLevel: swapsController.setSwapsUserFeeLevel.bind( swapsController, ), setSwapsQuotesPollingLimitEnabled: swapsController.setSwapsQuotesPollingLimitEnabled.bind( swapsController, ), // MetaMetrics trackMetaMetricsEvent: metaMetricsController.trackEvent.bind( metaMetricsController, ), trackMetaMetricsPage: metaMetricsController.trackPage.bind( metaMetricsController, ), createEventFragment: metaMetricsController.createEventFragment.bind( metaMetricsController, ), updateEventFragment: metaMetricsController.updateEventFragment.bind( metaMetricsController, ), finalizeEventFragment: metaMetricsController.finalizeEventFragment.bind( metaMetricsController, ), // approval controller resolvePendingApproval: approvalController.accept.bind( approvalController, ), rejectPendingApproval: approvalController.reject.bind(approvalController), // Notifications updateViewedNotifications: notificationController.updateViewed.bind( notificationController, ), // GasFeeController getGasFeeEstimatesAndStartPolling: gasFeeController.getGasFeeEstimatesAndStartPolling.bind( gasFeeController, ), disconnectGasFeeEstimatePoller: gasFeeController.disconnectPoller.bind( gasFeeController, ), getGasFeeTimeEstimate: gasFeeController.getTimeEstimate.bind( gasFeeController, ), addPollingTokenToAppState: appStateController.addPollingToken.bind( appStateController, ), removePollingTokenFromAppState: appStateController.removePollingToken.bind( appStateController, ), // DetectTokenController detectNewTokens: detectTokensController.detectNewTokens.bind( detectTokensController, ), // DetectCollectibleController detectCollectibles: process.env.COLLECTIBLES_V1 ? collectibleDetectionController.detectCollectibles.bind( collectibleDetectionController, ) : null, }; } //============================================================================= // VAULT / KEYRING RELATED METHODS //============================================================================= /** * Creates a new Vault and create a new keychain. * * A vault, or KeyringController, is a controller that contains * many different account strategies, currently called Keyrings. * Creating it new means wiping all previous keyrings. * * A keychain, or keyring, controls many accounts with a single backup and signing strategy. * For example, a mnemonic phrase can generate many accounts, and is a keyring. * * @param {string} password * @returns {Object} vault */ async createNewVaultAndKeychain(password) { const releaseLock = await this.createVaultMutex.acquire(); try { let vault; const accounts = await this.keyringController.getAccounts(); if (accounts.length > 0) { vault = await this.keyringController.fullUpdate(); } else { vault = await this.keyringController.createNewVaultAndKeychain( password, ); const addresses = await this.keyringController.getAccounts(); this.preferencesController.setAddresses(addresses); this.selectFirstIdentity(); } return vault; } finally { releaseLock(); } } /** * Create a new Vault and restore an existent keyring. * * @param {string} password * @param {string} seed */ async createNewVaultAndRestore(password, seed) { const releaseLock = await this.createVaultMutex.acquire(); try { let accounts, lastBalance; const { keyringController } = this; // clear known identities this.preferencesController.setAddresses([]); // clear permissions this.permissionController.clearState(); // clear accounts in accountTracker this.accountTracker.clearAccounts(); // clear cachedBalances this.cachedBalancesController.clearCachedBalances(); // clear unapproved transactions this.txController.txStateManager.clearUnapprovedTxs(); // create new vault const vault = await keyringController.createNewVaultAndRestore( password, seed, ); const ethQuery = new EthQuery(this.provider); accounts = await keyringController.getAccounts(); lastBalance = await this.getBalance( accounts[accounts.length - 1], ethQuery, ); const primaryKeyring = keyringController.getKeyringsByType( 'HD Key Tree', )[0]; if (!primaryKeyring) { throw new Error('MetamaskController - No HD Key Tree found'); } // seek out the first zero balance while (lastBalance !== '0x0') { await keyringController.addNewAccount(primaryKeyring); accounts = await keyringController.getAccounts(); lastBalance = await this.getBalance( accounts[accounts.length - 1], ethQuery, ); } // remove extra zero balance account potentially created from seeking ahead if (accounts.length > 1 && lastBalance === '0x0') { await this.removeAccount(accounts[accounts.length - 1]); accounts = await keyringController.getAccounts(); } // This must be set as soon as possible to communicate to the // keyring's iframe and have the setting initialized properly // Optimistically called to not block Metamask login due to // Ledger Keyring GitHub downtime const transportPreference = this.preferencesController.getLedgerTransportPreference(); this.setLedgerTransportPreference(transportPreference); // set new identities this.preferencesController.setAddresses(accounts); this.selectFirstIdentity(); return vault; } finally { releaseLock(); } } /** * Get an account balance from the AccountTracker or request it directly from the network. * * @param {string} address - The account address * @param {EthQuery} ethQuery - The EthQuery instance to use when asking the network */ getBalance(address, ethQuery) { return new Promise((resolve, reject) => { const cached = this.accountTracker.store.getState().accounts[address]; if (cached && cached.balance) { resolve(cached.balance); } else { ethQuery.getBalance(address, (error, balance) => { if (error) { reject(error); log.error(error); } else { resolve(balance || '0x0'); } }); } }); } /** * Collects all the information that we want to share * with the mobile client for syncing purposes * * @returns {Promise} Parts of the state that we want to syncx */ async fetchInfoToSync() { // Preferences const { currentLocale, frequentRpcList, identities, selectedAddress, useTokenDetection, } = this.preferencesController.store.getState(); const { tokenList } = this.tokenListController.state; const preferences = { currentLocale, frequentRpcList, identities, selectedAddress, }; // Tokens const { allTokens, allIgnoredTokens } = this.tokensController.state; // Filter ERC20 tokens const allERC20Tokens = {}; Object.keys(allTokens).forEach((chainId) => { allERC20Tokens[chainId] = {}; Object.keys(allTokens[chainId]).forEach((accountAddress) => { const checksummedAccountAddress = toChecksumHexAddress(accountAddress); allERC20Tokens[chainId][checksummedAccountAddress] = allTokens[chainId][ checksummedAccountAddress ].filter((asset) => { if (asset.isERC721 === undefined) { // since the token.address from allTokens is checksumaddress // asset.address have to be changed to lowercase when we are using dynamic list const address = useTokenDetection ? asset.address.toLowerCase() : asset.address; // the tokenList will be holding only erc20 tokens if (tokenList[address] !== undefined) { return true; } } else if (asset.isERC721 === false) { return true; } return false; }); }); }); // Accounts const hdKeyring = this.keyringController.getKeyringsByType( 'HD Key Tree', )[0]; const simpleKeyPairKeyrings = this.keyringController.getKeyringsByType( 'Simple Key Pair', ); const hdAccounts = await hdKeyring.getAccounts(); const simpleKeyPairKeyringAccounts = await Promise.all( simpleKeyPairKeyrings.map((keyring) => keyring.getAccounts()), ); const simpleKeyPairAccounts = simpleKeyPairKeyringAccounts.reduce( (acc, accounts) => [...acc, ...accounts], [], ); const accounts = { hd: hdAccounts .filter((item, pos) => hdAccounts.indexOf(item) === pos) .map((address) => toChecksumHexAddress(address)), simpleKeyPair: simpleKeyPairAccounts .filter((item, pos) => simpleKeyPairAccounts.indexOf(item) === pos) .map((address) => toChecksumHexAddress(address)), ledger: [], trezor: [], lattice: [], }; // transactions let { transactions } = this.txController.store.getState(); // delete tx for other accounts that we're not importing transactions = Object.values(transactions).filter((tx) => { const checksummedTxFrom = toChecksumHexAddress(tx.txParams.from); return accounts.hd.includes(checksummedTxFrom); }); return { accounts, preferences, transactions, tokens: { allTokens: allERC20Tokens, allIgnoredTokens }, network: this.networkController.store.getState(), }; } /** * Submits the user's password and attempts to unlock the vault. * Also synchronizes the preferencesController, to ensure its schema * is up to date with known accounts once the vault is decrypted. * * @param {string} password - The user's password * @returns {Promise} The keyringController update. */ async submitPassword(password) { await this.keyringController.submitPassword(password); try { await this.blockTracker.checkForLatestBlock(); } catch (error) { log.error('Error while unlocking extension.', error); } try { const threeBoxSyncingAllowed = this.threeBoxController.getThreeBoxSyncingState(); if (threeBoxSyncingAllowed && !this.threeBoxController.box) { // 'await' intentionally omitted to avoid waiting for initialization this.threeBoxController.init(); this.threeBoxController.turnThreeBoxSyncingOn(); } else if (threeBoxSyncingAllowed && this.threeBoxController.box) { this.threeBoxController.turnThreeBoxSyncingOn(); } } catch (error) { log.error('Error while unlocking extension.', error); } // This must be set as soon as possible to communicate to the // keyring's iframe and have the setting initialized properly // Optimistically called to not block Metamask login due to // Ledger Keyring GitHub downtime const transportPreference = this.preferencesController.getLedgerTransportPreference(); this.setLedgerTransportPreference(transportPreference); return this.keyringController.fullUpdate(); } /** * Submits a user's password to check its validity. * * @param {string} password - The user's password */ async verifyPassword(password) { await this.keyringController.verifyPassword(password); } /** * @type Identity * @property {string} name - The account nickname. * @property {string} address - The account's ethereum address, in lower case. * @property {boolean} mayBeFauceting - Whether this account is currently * receiving funds from our automatic Ropsten faucet. */ /** * Sets the first address in the state to the selected address */ selectFirstIdentity() { const { identities } = this.preferencesController.store.getState(); const address = Object.keys(identities)[0]; this.preferencesController.setSelectedAddress(address); } // // Hardware // async getKeyringForDevice(deviceName, hdPath = null) { let keyringName = null; switch (deviceName) { case DEVICE_NAMES.TREZOR: keyringName = TrezorKeyring.type; break; case DEVICE_NAMES.LEDGER: keyringName = LedgerBridgeKeyring.type; break; case DEVICE_NAMES.QR: keyringName = QRHardwareKeyring.type; break; case DEVICE_NAMES.LATTICE: keyringName = LatticeKeyring.type; break; default: throw new Error( 'MetamaskController:getKeyringForDevice - Unknown device', ); } let keyring = await this.keyringController.getKeyringsByType( keyringName, )[0]; if (!keyring) { keyring = await this.keyringController.addNewKeyring(keyringName); } if (hdPath && keyring.setHdPath) { keyring.setHdPath(hdPath); } if (deviceName === DEVICE_NAMES.LATTICE) { keyring.appName = 'MetaMask'; } if (deviceName === 'trezor') { const model = keyring.getModel(); this.appStateController.setTrezorModel(model); } keyring.network = this.networkController.getProviderConfig().type; return keyring; } async attemptLedgerTransportCreation() { const keyring = await this.getKeyringForDevice('ledger'); return await keyring.attemptMakeApp(); } async establishLedgerTransportPreference() { const transportPreference = this.preferencesController.getLedgerTransportPreference(); return await this.setLedgerTransportPreference(transportPreference); } /** * Fetch account list from a trezor device. * * @param deviceName * @param page * @param hdPath * @returns [] accounts */ async connectHardware(deviceName, page, hdPath) { const keyring = await this.getKeyringForDevice(deviceName, hdPath); let accounts = []; switch (page) { case -1: accounts = await keyring.getPreviousPage(); break; case 1: accounts = await keyring.getNextPage(); break; default: accounts = await keyring.getFirstPage(); } // Merge with existing accounts // and make sure addresses are not repeated const oldAccounts = await this.keyringController.getAccounts(); const accountsToTrack = [ ...new Set( oldAccounts.concat(accounts.map((a) => a.address.toLowerCase())), ), ]; this.accountTracker.syncWithAddresses(accountsToTrack); return accounts; } /** * Check if the device is unlocked * * @param deviceName * @param hdPath * @returns {Promise} */ async checkHardwareStatus(deviceName, hdPath) { const keyring = await this.getKeyringForDevice(deviceName, hdPath); return keyring.isUnlocked(); } /** * Clear * * @param deviceName * @returns {Promise} */ async forgetDevice(deviceName) { const keyring = await this.getKeyringForDevice(deviceName); keyring.forgetDevice(); return true; } /** * get hardware account label * * @returns string label */ getAccountLabel(name, index, hdPathDescription) { return `${name[0].toUpperCase()}${name.slice(1)} ${ parseInt(index, 10) + 1 } ${hdPathDescription || ''}`.trim(); } /** * Imports an account from a Trezor or Ledger device. * * @param index * @param deviceName * @param hdPath * @param hdPathDescription * @returns {} keyState */ async unlockHardwareWalletAccount( index, deviceName, hdPath, hdPathDescription, ) { const keyring = await this.getKeyringForDevice(deviceName, hdPath); keyring.setAccountToUnlock(index); const oldAccounts = await this.keyringController.getAccounts(); const keyState = await this.keyringController.addNewAccount(keyring); const newAccounts = await this.keyringController.getAccounts(); this.preferencesController.setAddresses(newAccounts); newAccounts.forEach((address) => { if (!oldAccounts.includes(address)) { const label = this.getAccountLabel( deviceName === DEVICE_NAMES.QR ? keyring.getName() : deviceName, index, hdPathDescription, ); // Set the account label to Trezor 1 / Ledger 1 / QR Hardware 1, etc this.preferencesController.setAccountLabel(address, label); // Select the account this.preferencesController.setSelectedAddress(address); } }); const { identities } = this.preferencesController.store.getState(); return { ...keyState, identities }; } // // Account Management // /** * Adds a new account to the default (first) HD seed phrase Keyring. * * @returns {} keyState */ async addNewAccount() { const primaryKeyring = this.keyringController.getKeyringsByType( 'HD Key Tree', )[0]; if (!primaryKeyring) { throw new Error('MetamaskController - No HD Key Tree found'); } const { keyringController } = this; const oldAccounts = await keyringController.getAccounts(); const keyState = await keyringController.addNewAccount(primaryKeyring); const newAccounts = await keyringController.getAccounts(); await this.verifySeedPhrase(); this.preferencesController.setAddresses(newAccounts); newAccounts.forEach((address) => { if (!oldAccounts.includes(address)) { this.preferencesController.setSelectedAddress(address); } }); const { identities } = this.preferencesController.store.getState(); return { ...keyState, identities }; } /** * Verifies the validity of the current vault's seed phrase. * * Validity: seed phrase restores the accounts belonging to the current vault. * * Called when the first account is created and on unlocking the vault. * * @returns {Promise} Seed phrase to be confirmed by the user. */ async verifySeedPhrase() { const primaryKeyring = this.keyringController.getKeyringsByType( 'HD Key Tree', )[0]; if (!primaryKeyring) { throw new Error('MetamaskController - No HD Key Tree found'); } const serialized = await primaryKeyring.serialize(); const seedWords = serialized.mnemonic; const accounts = await primaryKeyring.getAccounts(); if (accounts.length < 1) { throw new Error('MetamaskController - No accounts found'); } try { await seedPhraseVerifier.verifyAccounts(accounts, seedWords); return seedWords; } catch (err) { log.error(err.message); throw err; } } /** * Clears the transaction history, to allow users to force-reset their nonces. * Mostly used in development environments, when networks are restarted with * the same network ID. * * @returns {Promise} The current selected address. */ async resetAccount() { const selectedAddress = this.preferencesController.getSelectedAddress(); this.txController.wipeTransactions(selectedAddress); this.networkController.resetConnection(); return selectedAddress; } /** * Gets the permitted accounts for the specified origin. Returns an empty * array if no accounts are permitted. * * @param {string} origin - The origin whose exposed accounts to retrieve. * @returns {Promise} The origin's permitted accounts, or an empty * array. */ async getPermittedAccounts(origin) { try { return await this.permissionController.executeRestrictedMethod( origin, RestrictedMethods.eth_accounts, ); } catch (error) { if (error.code === rpcErrorCodes.provider.unauthorized) { return []; } throw error; } } /** * Stops exposing the account with the specified address to all third parties. * Exposed accounts are stored in caveats of the eth_accounts permission. This * method uses `PermissionController.updatePermissionsByCaveat` to * remove the specified address from every eth_accounts permission. If a * permission only included this address, the permission is revoked entirely. * * @param {string} targetAccount - The address of the account to stop exposing * to third parties. */ removeAllAccountPermissions(targetAccount) { this.permissionController.updatePermissionsByCaveat( CaveatTypes.restrictReturnedAccounts, (existingAccounts) => CaveatMutatorFactories[ CaveatTypes.restrictReturnedAccounts ].removeAccount(targetAccount, existingAccounts), ); } /** * Removes an account from state / storage. * * @param {string[]} address - A hex address */ async removeAccount(address) { // Remove all associated permissions this.removeAllAccountPermissions(address); // Remove account from the preferences controller this.preferencesController.removeAddress(address); // Remove account from the account tracker controller this.accountTracker.removeAccount([address]); // Remove account from the keyring await this.keyringController.removeAccount(address); return address; } /** * Imports an account with the specified import strategy. * These are defined in app/scripts/account-import-strategies * Each strategy represents a different way of serializing an Ethereum key pair. * * @param {string} strategy - A unique identifier for an account import strategy. * @param {any} args - The data required by that strategy to import an account. */ async importAccountWithStrategy(strategy, args) { const privateKey = await accountImporter.importAccount(strategy, args); const keyring = await this.keyringController.addNewKeyring( 'Simple Key Pair', [privateKey], ); const accounts = await keyring.getAccounts(); // update accounts in preferences controller const allAccounts = await this.keyringController.getAccounts(); this.preferencesController.setAddresses(allAccounts); // set new account as selected await this.preferencesController.setSelectedAddress(accounts[0]); } // --------------------------------------------------------------------------- // Identity Management (signature operations) /** * Called when a Dapp suggests a new tx to be signed. * this wrapper needs to exist so we can provide a reference to * "newUnapprovedTransaction" before "txController" is instantiated * * @param {Object} txParams - The transaction parameters. * @param {Object} [req] - The original request, containing the origin. */ async newUnapprovedTransaction(txParams, req) { return await this.txController.newUnapprovedTransaction(txParams, req); } // eth_sign methods: /** * Called when a Dapp uses the eth_sign method, to request user approval. * eth_sign is a pure signature of arbitrary data. It is on a deprecation * path, since this data can be a transaction, or can leak private key * information. * * @param {Object} msgParams - The params passed to eth_sign. * @param {Object} [req] - The original request, containing the origin. */ async newUnsignedMessage(msgParams, req) { const data = normalizeMsgData(msgParams.data); let promise; // 64 hex + "0x" at the beginning // This is needed because Ethereum's EcSign works only on 32 byte numbers // For 67 length see: https://github.com/MetaMask/metamask-extension/pull/12679/files#r749479607 if (data.length === 66 || data.length === 67) { promise = this.messageManager.addUnapprovedMessageAsync(msgParams, req); this.sendUpdate(); this.opts.showUserConfirmation(); } else { throw ethErrors.rpc.invalidParams( 'eth_sign requires 32 byte message hash', ); } return await promise; } /** * Signifies user intent to complete an eth_sign method. * * @param {Object} msgParams - The params passed to eth_call. * @returns {Promise} Full state update. */ async signMessage(msgParams) { log.info('MetaMaskController - signMessage'); const msgId = msgParams.metamaskId; try { // sets the status op the message to 'approved' // and removes the metamaskId for signing const cleanMsgParams = await this.messageManager.approveMessage( msgParams, ); const rawSig = await this.keyringController.signMessage(cleanMsgParams); this.messageManager.setMsgStatusSigned(msgId, rawSig); return this.getState(); } catch (error) { log.info('MetaMaskController - eth_sign failed', error); this.messageManager.errorMessage(msgId, error); throw error; } } /** * Used to cancel a message submitted via eth_sign. * * @param {string} msgId - The id of the message to cancel. */ cancelMessage(msgId) { const { messageManager } = this; messageManager.rejectMsg(msgId); return this.getState(); } // personal_sign methods: /** * Called when a dapp uses the personal_sign method. * This is identical to the Geth eth_sign method, and may eventually replace * eth_sign. * * We currently define our eth_sign and personal_sign mostly for legacy Dapps. * * @param {Object} msgParams - The params of the message to sign & return to the Dapp. * @param {Object} [req] - The original request, containing the origin. */ async newUnsignedPersonalMessage(msgParams, req) { const promise = this.personalMessageManager.addUnapprovedMessageAsync( msgParams, req, ); this.sendUpdate(); this.opts.showUserConfirmation(); return promise; } /** * Signifies a user's approval to sign a personal_sign message in queue. * Triggers signing, and the callback function from newUnsignedPersonalMessage. * * @param {Object} msgParams - The params of the message to sign & return to the Dapp. * @returns {Promise} A full state update. */ async signPersonalMessage(msgParams) { log.info('MetaMaskController - signPersonalMessage'); const msgId = msgParams.metamaskId; // sets the status op the message to 'approved' // and removes the metamaskId for signing try { const cleanMsgParams = await this.personalMessageManager.approveMessage( msgParams, ); const rawSig = await this.keyringController.signPersonalMessage( cleanMsgParams, ); // tells the listener that the message has been signed // and can be returned to the dapp this.personalMessageManager.setMsgStatusSigned(msgId, rawSig); return this.getState(); } catch (error) { log.info('MetaMaskController - eth_personalSign failed', error); this.personalMessageManager.errorMessage(msgId, error); throw error; } } /** * Used to cancel a personal_sign type message. * * @param {string} msgId - The ID of the message to cancel. */ cancelPersonalMessage(msgId) { const messageManager = this.personalMessageManager; messageManager.rejectMsg(msgId); return this.getState(); } // eth_decrypt methods /** * Called when a dapp uses the eth_decrypt method. * * @param {Object} msgParams - The params of the message to sign & return to the Dapp. * @param {Object} req - (optional) the original request, containing the origin * Passed back to the requesting Dapp. */ async newRequestDecryptMessage(msgParams, req) { const promise = this.decryptMessageManager.addUnapprovedMessageAsync( msgParams, req, ); this.sendUpdate(); this.opts.showUserConfirmation(); return promise; } /** * Only decrypt message and don't touch transaction state * * @param {Object} msgParams - The params of the message to decrypt. * @returns {Promise} A full state update. */ async decryptMessageInline(msgParams) { log.info('MetaMaskController - decryptMessageInline'); // decrypt the message inline const msgId = msgParams.metamaskId; const msg = this.decryptMessageManager.getMsg(msgId); try { const stripped = stripHexPrefix(msgParams.data); const buff = Buffer.from(stripped, 'hex'); msgParams.data = JSON.parse(buff.toString('utf8')); msg.rawData = await this.keyringController.decryptMessage(msgParams); } catch (e) { msg.error = e.message; } this.decryptMessageManager._updateMsg(msg); return this.getState(); } /** * Signifies a user's approval to decrypt a message in queue. * Triggers decrypt, and the callback function from newUnsignedDecryptMessage. * * @param {Object} msgParams - The params of the message to decrypt & return to the Dapp. * @returns {Promise} A full state update. */ async decryptMessage(msgParams) { log.info('MetaMaskController - decryptMessage'); const msgId = msgParams.metamaskId; // sets the status op the message to 'approved' // and removes the metamaskId for decryption try { const cleanMsgParams = await this.decryptMessageManager.approveMessage( msgParams, ); const stripped = stripHexPrefix(cleanMsgParams.data); const buff = Buffer.from(stripped, 'hex'); cleanMsgParams.data = JSON.parse(buff.toString('utf8')); // decrypt the message const rawMess = await this.keyringController.decryptMessage( cleanMsgParams, ); // tells the listener that the message has been decrypted and can be returned to the dapp this.decryptMessageManager.setMsgStatusDecrypted(msgId, rawMess); } catch (error) { log.info('MetaMaskController - eth_decrypt failed.', error); this.decryptMessageManager.errorMessage(msgId, error); } return this.getState(); } /** * Used to cancel a eth_decrypt type message. * * @param {string} msgId - The ID of the message to cancel. */ cancelDecryptMessage(msgId) { const messageManager = this.decryptMessageManager; messageManager.rejectMsg(msgId); return this.getState(); } // eth_getEncryptionPublicKey methods /** * Called when a dapp uses the eth_getEncryptionPublicKey method. * * @param {Object} msgParams - The params of the message to sign & return to the Dapp. * @param {Object} req - (optional) the original request, containing the origin * Passed back to the requesting Dapp. */ async newRequestEncryptionPublicKey(msgParams, req) { const address = msgParams; const keyring = await this.keyringController.getKeyringForAccount(address); switch (keyring.type) { case KEYRING_TYPES.LEDGER: { return new Promise((_, reject) => { reject( new Error('Ledger does not support eth_getEncryptionPublicKey.'), ); }); } case KEYRING_TYPES.TREZOR: { return new Promise((_, reject) => { reject( new Error('Trezor does not support eth_getEncryptionPublicKey.'), ); }); } case KEYRING_TYPES.LATTICE: { return new Promise((_, reject) => { reject( new Error('Lattice does not support eth_getEncryptionPublicKey.'), ); }); } case KEYRING_TYPES.QR: { return Promise.reject( new Error('QR hardware does not support eth_getEncryptionPublicKey.'), ); } default: { const promise = this.encryptionPublicKeyManager.addUnapprovedMessageAsync( msgParams, req, ); this.sendUpdate(); this.opts.showUserConfirmation(); return promise; } } } /** * Signifies a user's approval to receiving encryption public key in queue. * Triggers receiving, and the callback function from newUnsignedEncryptionPublicKey. * * @param {Object} msgParams - The params of the message to receive & return to the Dapp. * @returns {Promise} A full state update. */ async encryptionPublicKey(msgParams) { log.info('MetaMaskController - encryptionPublicKey'); const msgId = msgParams.metamaskId; // sets the status op the message to 'approved' // and removes the metamaskId for decryption try { const params = await this.encryptionPublicKeyManager.approveMessage( msgParams, ); // EncryptionPublicKey message const publicKey = await this.keyringController.getEncryptionPublicKey( params.data, ); // tells the listener that the message has been processed // and can be returned to the dapp this.encryptionPublicKeyManager.setMsgStatusReceived(msgId, publicKey); } catch (error) { log.info( 'MetaMaskController - eth_getEncryptionPublicKey failed.', error, ); this.encryptionPublicKeyManager.errorMessage(msgId, error); } return this.getState(); } /** * Used to cancel a eth_getEncryptionPublicKey type message. * * @param {string} msgId - The ID of the message to cancel. */ cancelEncryptionPublicKey(msgId) { const messageManager = this.encryptionPublicKeyManager; messageManager.rejectMsg(msgId); return this.getState(); } // eth_signTypedData methods /** * Called when a dapp uses the eth_signTypedData method, per EIP 712. * * @param {Object} msgParams - The params passed to eth_signTypedData. * @param {Object} [req] - The original request, containing the origin. * @param version */ newUnsignedTypedMessage(msgParams, req, version) { const promise = this.typedMessageManager.addUnapprovedMessageAsync( msgParams, req, version, ); this.sendUpdate(); this.opts.showUserConfirmation(); return promise; } /** * The method for a user approving a call to eth_signTypedData, per EIP 712. * Triggers the callback in newUnsignedTypedMessage. * * @param {Object} msgParams - The params passed to eth_signTypedData. * @returns {Object} Full state update. */ async signTypedMessage(msgParams) { log.info('MetaMaskController - eth_signTypedData'); const msgId = msgParams.metamaskId; const { version } = msgParams; try { const cleanMsgParams = await this.typedMessageManager.approveMessage( msgParams, ); // For some reason every version after V1 used stringified params. if (version !== 'V1') { // But we don't have to require that. We can stop suggesting it now: if (typeof cleanMsgParams.data === 'string') { cleanMsgParams.data = JSON.parse(cleanMsgParams.data); } } const signature = await this.keyringController.signTypedMessage( cleanMsgParams, { version }, ); this.typedMessageManager.setMsgStatusSigned(msgId, signature); return this.getState(); } catch (error) { log.info('MetaMaskController - eth_signTypedData failed.', error); this.typedMessageManager.errorMessage(msgId, error); throw error; } } /** * Used to cancel a eth_signTypedData type message. * * @param {string} msgId - The ID of the message to cancel. */ cancelTypedMessage(msgId) { const messageManager = this.typedMessageManager; messageManager.rejectMsg(msgId); return this.getState(); } /** * @returns {boolean} true if the keyring type supports EIP-1559 */ async getCurrentAccountEIP1559Compatibility() { return true; } //============================================================================= // END (VAULT / KEYRING RELATED METHODS) //============================================================================= /** * Allows a user to attempt to cancel a previously submitted transaction * by creating a new transaction. * * @param {number} originalTxId - the id of the txMeta that you want to * attempt to cancel * @param {import( * './controllers/transactions' * ).CustomGasSettings} [customGasSettings] - overrides to use for gas params * instead of allowing this method to generate them * @param newTxMetaProps * @returns {Object} MetaMask state */ async createCancelTransaction( originalTxId, customGasSettings, newTxMetaProps, ) { await this.txController.createCancelTransaction( originalTxId, customGasSettings, newTxMetaProps, ); const state = await this.getState(); return state; } /** * Allows a user to attempt to speed up a previously submitted transaction * by creating a new transaction. * * @param {number} originalTxId - the id of the txMeta that you want to * attempt to speed up * @param {import( * './controllers/transactions' * ).CustomGasSettings} [customGasSettings] - overrides to use for gas params * instead of allowing this method to generate them * @param newTxMetaProps * @returns {Object} MetaMask state */ async createSpeedUpTransaction( originalTxId, customGasSettings, newTxMetaProps, ) { await this.txController.createSpeedUpTransaction( originalTxId, customGasSettings, newTxMetaProps, ); const state = await this.getState(); return state; } estimateGas(estimateGasParams) { return new Promise((resolve, reject) => { return this.txController.txGasUtil.query.estimateGas( estimateGasParams, (err, res) => { if (err) { return reject(err); } return resolve(res.toString(16)); }, ); }); } //============================================================================= // PASSWORD MANAGEMENT //============================================================================= /** * Allows a user to begin the seed phrase recovery process. */ markPasswordForgotten() { this.preferencesController.setPasswordForgotten(true); this.sendUpdate(); } /** * Allows a user to end the seed phrase recovery process. */ unMarkPasswordForgotten() { this.preferencesController.setPasswordForgotten(false); this.sendUpdate(); } //============================================================================= // SETUP //============================================================================= /** * A runtime.MessageSender object, as provided by the browser: * * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender * @typedef {Object} MessageSender * @property {string} - The URL of the page or frame hosting the script that sent the message. */ /** * Used to create a multiplexed stream for connecting to an untrusted context * like a Dapp or other extension. * * @param {*} connectionStream - The Duplex stream to connect to. * @param {MessageSender} sender - The sender of the messages on this stream */ setupUntrustedCommunication(connectionStream, sender) { const { usePhishDetect } = this.preferencesController.store.getState(); const { hostname } = new URL(sender.url); // Check if new connection is blocked if phishing detection is on if (usePhishDetect && this.phishingController.test(hostname)) { log.debug('MetaMask - sending phishing warning for', hostname); this.sendPhishingWarning(connectionStream, hostname); return; } // setup multiplexing const mux = setupMultiplex(connectionStream); // messages between inpage and background this.setupProviderConnection(mux.createStream('metamask-provider'), sender); // TODO:LegacyProvider: Delete // legacy streams this.setupPublicConfig(mux.createStream('publicConfig')); } /** * Used to create a multiplexed stream for connecting to a trusted context, * like our own user interfaces, which have the provider APIs, but also * receive the exported API from this controller, which includes trusted * functions, like the ability to approve transactions or sign messages. * * @param {*} connectionStream - The duplex stream to connect to. * @param {MessageSender} sender - The sender of the messages on this stream */ setupTrustedCommunication(connectionStream, sender) { // setup multiplexing const mux = setupMultiplex(connectionStream); // connect features this.setupControllerConnection(mux.createStream('controller')); this.setupProviderConnection(mux.createStream('provider'), sender, true); } /** * Called when we detect a suspicious domain. Requests the browser redirects * to our anti-phishing page. * * @private * @param {*} connectionStream - The duplex stream to the per-page script, * for sending the reload attempt to. * @param {string} hostname - The hostname that triggered the suspicion. */ sendPhishingWarning(connectionStream, hostname) { const mux = setupMultiplex(connectionStream); const phishingStream = mux.createStream('phishing'); phishingStream.write({ hostname }); } /** * A method for providing our API over a stream using JSON-RPC. * * @param {*} outStream - The stream to provide our API over. */ setupControllerConnection(outStream) { const api = this.getApi(); // report new active controller connection this.activeControllerConnections += 1; this.emit('controllerConnectionChanged', this.activeControllerConnections); // set up postStream transport outStream.on('data', createMetaRPCHandler(api, outStream)); const handleUpdate = (update) => { if (outStream._writableState.ended) { return; } // send notification to client-side outStream.write({ jsonrpc: '2.0', method: 'sendUpdate', params: [update], }); }; this.on('update', handleUpdate); outStream.on('end', () => { this.activeControllerConnections -= 1; this.emit( 'controllerConnectionChanged', this.activeControllerConnections, ); this.removeListener('update', handleUpdate); }); } /** * A method for serving our ethereum provider over a given stream. * * @param {*} outStream - The stream to provide over. * @param {MessageSender} sender - The sender of the messages on this stream * @param {boolean} isInternal - True if this is a connection with an internal process */ setupProviderConnection(outStream, sender, isInternal) { const origin = isInternal ? 'metamask' : new URL(sender.url).origin; let subjectType = isInternal ? SUBJECT_TYPES.INTERNAL : SUBJECT_TYPES.WEBSITE; if (sender.id !== this.extension.runtime.id) { subjectType = SUBJECT_TYPES.EXTENSION; this.subjectMetadataController.addSubjectMetadata(origin, { extensionId: sender.id, subjectType: SUBJECT_TYPES.EXTENSION, }); } let tabId; if (sender.tab && sender.tab.id) { tabId = sender.tab.id; } const engine = this.setupProviderEngine({ origin, location: sender.url, tabId, subjectType, }); // setup connection const providerStream = createEngineStream({ engine }); const connectionId = this.addConnection(origin, { engine }); pump(outStream, providerStream, outStream, (err) => { // handle any middleware cleanup engine._middleware.forEach((mid) => { if (mid.destroy && typeof mid.destroy === 'function') { mid.destroy(); } }); connectionId && this.removeConnection(origin, connectionId); if (err) { log.error(err); } }); } /** * A method for creating a provider that is safely restricted for the requesting subject. * * @param {Object} options - Provider engine options * @param {string} options.origin - The origin of the sender * @param {string} options.location - The full URL of the sender * @param {string} options.subjectType - The type of the sender subject. * @param {tabId} [options.tabId] - The tab ID of the sender - if the sender is within a tab */ setupProviderEngine({ origin, location, subjectType, tabId }) { // setup json rpc engine stack const engine = new JsonRpcEngine(); const { provider, blockTracker } = this; // create filter polyfill middleware const filterMiddleware = createFilterMiddleware({ provider, blockTracker }); // create subscription polyfill middleware const subscriptionManager = createSubscriptionManager({ provider, blockTracker, }); subscriptionManager.events.on('notification', (message) => engine.emit('notification', message), ); // append origin to each request engine.push(createOriginMiddleware({ origin })); // append tabId to each request if it exists if (tabId) { engine.push(createTabIdMiddleware({ tabId })); } // logging engine.push(createLoggerMiddleware({ origin })); engine.push( createOnboardingMiddleware({ location, registerOnboarding: this.onboardingController.registerOnboarding, }), ); engine.push(this.permissionLogController.createMiddleware()); engine.push( createMethodMiddleware({ origin, subjectType, // Miscellaneous addSubjectMetadata: this.subjectMetadataController.addSubjectMetadata.bind( this.subjectMetadataController, ), getProviderState: this.getProviderState.bind(this), getUnlockPromise: this.appStateController.getUnlockPromise.bind( this.appStateController, ), handleWatchAssetRequest: this.tokensController.watchAsset.bind( this.tokensController, ), requestUserApproval: this.approvalController.addAndShowApprovalRequest.bind( this.approvalController, ), sendMetrics: this.metaMetricsController.trackEvent.bind( this.metaMetricsController, ), // Permission-related getAccounts: this.getPermittedAccounts.bind(this, origin), getPermissionsForOrigin: this.permissionController.getPermissions.bind( this.permissionController, origin, ), hasPermission: this.permissionController.hasPermission.bind( this.permissionController, origin, ), requestAccountsPermission: this.permissionController.requestPermissions.bind( this.permissionController, { origin }, { eth_accounts: {} }, ), requestPermissionsForOrigin: this.permissionController.requestPermissions.bind( this.permissionController, { origin }, ), // Custom RPC-related addCustomRpc: async ({ chainId, blockExplorerUrl, ticker, chainName, rpcUrl, } = {}) => { await this.preferencesController.addToFrequentRpcList( rpcUrl, chainId, ticker, chainName, { blockExplorerUrl, }, ); }, findCustomRpcBy: this.findCustomRpcBy.bind(this), getCurrentChainId: this.networkController.getCurrentChainId.bind( this.networkController, ), setProviderType: this.networkController.setProviderType.bind( this.networkController, ), updateRpcTarget: ({ rpcUrl, chainId, ticker, nickname }) => { this.networkController.setRpcTarget( rpcUrl, chainId, ticker, nickname, ); }, // Web3 shim-related getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind( this.alertController, ), setWeb3ShimUsageRecorded: this.alertController.setWeb3ShimUsageRecorded.bind( this.alertController, ), }), ); // filter and subscription polyfills engine.push(filterMiddleware); engine.push(subscriptionManager.middleware); if (subjectType !== SUBJECT_TYPES.INTERNAL) { // permissions engine.push( this.permissionController.createPermissionMiddleware({ origin, }), ); } // forward to metamask primary provider engine.push(providerAsMiddleware(provider)); return engine; } /** * TODO:LegacyProvider: Delete * A method for providing our public config info over a stream. * This includes info we like to be synchronous if possible, like * the current selected account, and network ID. * * Since synchronous methods have been deprecated in web3, * this is a good candidate for deprecation. * * @param {*} outStream - The stream to provide public config over. */ setupPublicConfig(outStream) { const configStream = storeAsStream(this.publicConfigStore); pump(configStream, outStream, (err) => { configStream.destroy(); if (err) { log.error(err); } }); } /** * Adds a reference to a connection by origin. Ignores the 'metamask' origin. * Caller must ensure that the returned id is stored such that the reference * can be deleted later. * * @param {string} origin - The connection's origin string. * @param {Object} options - Data associated with the connection * @param {Object} options.engine - The connection's JSON Rpc Engine * @returns {string} The connection's id (so that it can be deleted later) */ addConnection(origin, { engine }) { if (origin === 'metamask') { return null; } if (!this.connections[origin]) { this.connections[origin] = {}; } const id = nanoid(); this.connections[origin][id] = { engine, }; return id; } /** * Deletes a reference to a connection, by origin and id. * Ignores unknown origins. * * @param {string} origin - The connection's origin string. * @param {string} id - The connection's id, as returned from addConnection. */ removeConnection(origin, id) { const connections = this.connections[origin]; if (!connections) { return; } delete connections[id]; if (Object.keys(connections).length === 0) { delete this.connections[origin]; } } /** * Causes the RPC engines associated with the connections to the given origin * to emit a notification event with the given payload. * * The caller is responsible for ensuring that only permitted notifications * are sent. * * Ignores unknown origins. * * @param {string} origin - The connection's origin string. * @param {unknown} payload - The event payload. */ notifyConnections(origin, payload) { const connections = this.connections[origin]; if (connections) { Object.values(connections).forEach((conn) => { if (conn.engine) { conn.engine.emit('notification', payload); } }); } } /** * Causes the RPC engines associated with all connections to emit a * notification event with the given payload. * * If the "payload" parameter is a function, the payload for each connection * will be the return value of that function called with the connection's * origin. * * The caller is responsible for ensuring that only permitted notifications * are sent. * * @param {unknown} payload - The event payload, or payload getter function. */ notifyAllConnections(payload) { const getPayload = typeof payload === 'function' ? (origin) => payload(origin) : () => payload; Object.keys(this.connections).forEach((origin) => { Object.values(this.connections[origin]).forEach(async (conn) => { if (conn.engine) { conn.engine.emit('notification', await getPayload(origin)); } }); }); } // handlers /** * Handle a KeyringController update * * @param {Object} state - the KC state * @returns {Promise} * @private */ async _onKeyringControllerUpdate(state) { const { 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.syncAddresses(addresses); this.accountTracker.syncWithAddresses(addresses); } /** * Handle global application unlock. * Notifies all connections that the extension is unlocked, and which * account(s) are currently accessible, if any. */ _onUnlock() { this.notifyAllConnections(async (origin) => { return { method: NOTIFICATION_NAMES.unlockStateChanged, params: { isUnlocked: true, accounts: await this.getPermittedAccounts(origin), }, }; }); // In the current implementation, this handler is triggered by a // KeyringController event. Other controllers subscribe to the 'unlock' // event of the MetaMaskController itself. this.emit('unlock'); } /** * Handle global application lock. * Notifies all connections that the extension is locked. */ _onLock() { this.notifyAllConnections({ method: NOTIFICATION_NAMES.unlockStateChanged, params: { isUnlocked: false, }, }); // In the current implementation, this handler is triggered by a // KeyringController event. Other controllers subscribe to the 'lock' // event of the MetaMaskController itself. this.emit('lock'); } /** * Handle memory state updates. * - Ensure isClientOpenAndUnlocked is updated * - Notifies all connections with the new provider network state * - The external providers handle diffing the state * * @param newState */ _onStateUpdate(newState) { this.isClientOpenAndUnlocked = newState.isUnlocked && this._isClientOpen; this.notifyAllConnections({ method: NOTIFICATION_NAMES.chainChanged, params: this.getProviderNetworkState(newState), }); } // misc /** * A method for emitting the full MetaMask state to all registered listeners. * * @private */ privateSendUpdate() { this.emit('update', this.getState()); } /** * @returns {boolean} Whether the extension is unlocked. */ isUnlocked() { return this.keyringController.memStore.getState().isUnlocked; } //============================================================================= // MISCELLANEOUS //============================================================================= /** * Returns the nonce that will be associated with a transaction once approved * * @param {string} address - The hex string address for the transaction * @returns {Promise} */ async getPendingNonce(address) { const { nonceDetails, releaseLock, } = await this.txController.nonceTracker.getNonceLock(address); const pendingNonce = nonceDetails.params.highestSuggested; releaseLock(); return pendingNonce; } /** * Returns the next nonce according to the nonce-tracker * * @param {string} address - The hex string address for the transaction * @returns {Promise} */ async getNextNonce(address) { const nonceLock = await this.txController.nonceTracker.getNonceLock( address, ); nonceLock.releaseLock(); return nonceLock.nextNonce; } /** * Migrate address book state from old to new chainId. * * Address book state is keyed by the `networkStore` state from the network controller. This value is set to the * `networkId` for our built-in Infura networks, but it's set to the `chainId` for custom networks. * When this `chainId` value is changed for custom RPC endpoints, we need to migrate any contacts stored under the * old key to the new key. * * The `duplicate` parameter is used to specify that the contacts under the old key should not be removed. This is * useful in the case where two RPC endpoints shared the same set of contacts, and we're not sure which one each * contact belongs under. Duplicating the contacts under both keys is the only way to ensure they are not lost. * * @param {string} oldChainId - The old chainId * @param {string} newChainId - The new chainId * @param {boolean} [duplicate] - Whether to duplicate the addresses on both chainIds (default: false) */ async migrateAddressBookState(oldChainId, newChainId, duplicate = false) { const { addressBook } = this.addressBookController.state; if (!addressBook[oldChainId]) { return; } for (const address of Object.keys(addressBook[oldChainId])) { const entry = addressBook[oldChainId][address]; this.addressBookController.set( address, entry.name, newChainId, entry.memo, ); if (!duplicate) { this.addressBookController.delete(oldChainId, address); } } } //============================================================================= // CONFIG //============================================================================= // Log blocks /** * A method for selecting a custom URL for an ethereum RPC provider and updating it * * @param {string} rpcUrl - A URL for a valid Ethereum RPC API. * @param {string} chainId - The chainId of the selected network. * @param {string} ticker - The ticker symbol of the selected network. * @param {string} [nickname] - Nickname of the selected network. * @param {Object} [rpcPrefs] - RPC preferences. * @param {string} [rpcPrefs.blockExplorerUrl] - URL of block explorer for the chain. * @returns {Promise} The RPC Target URL confirmed. */ async updateAndSetCustomRpc( rpcUrl, chainId, ticker = 'ETH', nickname, rpcPrefs, ) { this.networkController.setRpcTarget( rpcUrl, chainId, ticker, nickname, rpcPrefs, ); await this.preferencesController.updateRpc({ rpcUrl, chainId, ticker, nickname, rpcPrefs, }); return rpcUrl; } /** * A method for selecting a custom URL for an ethereum RPC provider. * * @param {string} rpcUrl - A URL for a valid Ethereum RPC API. * @param {string} chainId - The chainId of the selected network. * @param {string} ticker - The ticker symbol of the selected network. * @param {string} nickname - Optional nickname of the selected network. * @param rpcPrefs * @returns {Promise} The RPC Target URL confirmed. */ async setCustomRpc( rpcUrl, chainId, ticker = 'ETH', nickname = '', rpcPrefs = {}, ) { const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail(); const rpcSettings = frequentRpcListDetail.find( (rpc) => rpcUrl === rpc.rpcUrl, ); if (rpcSettings) { this.networkController.setRpcTarget( rpcSettings.rpcUrl, rpcSettings.chainId, rpcSettings.ticker, rpcSettings.nickname, rpcPrefs, ); } else { this.networkController.setRpcTarget( rpcUrl, chainId, ticker, nickname, rpcPrefs, ); await this.preferencesController.addToFrequentRpcList( rpcUrl, chainId, ticker, nickname, rpcPrefs, ); } return rpcUrl; } /** * A method for deleting a selected custom URL. * * @param {string} rpcUrl - A RPC URL to delete. */ async delCustomRpc(rpcUrl) { await this.preferencesController.removeFromFrequentRpcList(rpcUrl); } /** * Returns the first RPC info object that matches at least one field of the * provided search criteria. Returns null if no match is found * * @param {Object} rpcInfo - The RPC endpoint properties and values to check. * @returns {Object} rpcInfo found in the frequentRpcList */ findCustomRpcBy(rpcInfo) { const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail(); for (const existingRpcInfo of frequentRpcListDetail) { for (const key of Object.keys(rpcInfo)) { if (existingRpcInfo[key] === rpcInfo[key]) { return existingRpcInfo; } } } return null; } async initializeThreeBox() { await this.threeBoxController.init(); } /** * Sets the Ledger Live preference to use for Ledger hardware wallet support * * @param {string} transportType - The Ledger transport type. */ async setLedgerTransportPreference(transportType) { const currentValue = this.preferencesController.getLedgerTransportPreference(); const newValue = this.preferencesController.setLedgerTransportPreference( transportType, ); const keyring = await this.getKeyringForDevice('ledger'); if (keyring?.updateTransportMethod) { return keyring.updateTransportMethod(newValue).catch((e) => { // If there was an error updating the transport, we should // fall back to the original value this.preferencesController.setLedgerTransportPreference(currentValue); throw e; }); } return undefined; } /** * A method for initializing storage the first time. * * @param {Object} initState - The default state to initialize with. * @private */ recordFirstTimeInfo(initState) { if (!('firstTimeInfo' in initState)) { const version = this.platform.getVersion(); initState.firstTimeInfo = { version, date: Date.now(), }; } } // TODO: Replace isClientOpen methods with `controllerConnectionChanged` events. /* eslint-disable accessor-pairs */ /** * A method for recording whether the MetaMask user interface is open or not. * * @param {boolean} open */ set isClientOpen(open) { this._isClientOpen = open; this.detectTokensController.isOpen = open; } /* eslint-enable accessor-pairs */ /** * A method that is called by the background when all instances of metamask are closed. * Currently used to stop polling in the gasFeeController. */ onClientClosed() { try { this.gasFeeController.stopPolling(); this.appStateController.clearPollingTokens(); } catch (error) { console.error(error); } } /** * A method that is called by the background when a particular environment type is closed (fullscreen, popup, notification). * Currently used to stop polling in the gasFeeController for only that environement type * * @param environmentType */ onEnvironmentTypeClosed(environmentType) { const appStatePollingTokenType = POLLING_TOKEN_ENVIRONMENT_TYPES[environmentType]; const pollingTokensToDisconnect = this.appStateController.store.getState()[ appStatePollingTokenType ]; pollingTokensToDisconnect.forEach((pollingToken) => { this.gasFeeController.disconnectPoller(pollingToken); this.appStateController.removePollingToken( pollingToken, appStatePollingTokenType, ); }); } /** * Adds a domain to the PhishingController safelist * * @param {string} hostname - the domain to safelist */ safelistPhishingDomain(hostname) { return this.phishingController.bypass(hostname); } /** * Locks MetaMask */ setLocked() { const [trezorKeyring] = this.keyringController.getKeyringsByType( KEYRING_TYPES.TREZOR, ); if (trezorKeyring) { trezorKeyring.dispose(); } return this.keyringController.setLocked(); } }