1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-13 13:17:13 +01:00
metamask-extension/app/scripts/metamask-controller.js
Filip Sekulic d8889ac83a OpenSea security provider metrics (#17688)
* Added metrics for the OpenSea security provider

* Fixed tests

* Fixed a test

* Fixed metrics

* Code refactor

* Lint fixed

* Removed unnecessary code

* Fix build

* Fix e2e

* Cleanup

* Fix e2e

* Code refactor

* Removed unnecessary code

* rpc middleware: catch securityProviderCheck errors
to not block dapp rpc requests

* Fixed an issue

* Added aditional test

* Applied some changes

* Fixed a test

* Fixed a test

* Code refactor

* Covered more code with tests

* Updated a test

* Fixed an issue

---------

Co-authored-by: Jyoti Puri <jyotipuri@gmail.com>
Co-authored-by: digiwand <20778143+digiwand@users.noreply.github.com>
Co-authored-by: Brad Decker <bhdecker84@gmail.com>
2023-03-30 15:50:10 +01:00

4618 lines
154 KiB
JavaScript

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 { createEngineStream } from 'json-rpc-middleware-stream';
import { providerAsMiddleware } from '@metamask/eth-json-rpc-middleware';
import { debounce } from 'lodash';
import {
KeyringController,
keyringBuilderFactory,
} from '@metamask/eth-keyring-controller';
import createFilterMiddleware from 'eth-json-rpc-filters';
import createSubscriptionManager from 'eth-json-rpc-filters/subscriptionManager';
import {
errorCodes as rpcErrorCodes,
EthereumRpcError,
ethErrors,
} from 'eth-rpc-errors';
import { Mutex } from 'await-semaphore';
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 } from '@metamask/address-book-controller';
import {
ApprovalController,
ApprovalRequestNotFoundError,
} from '@metamask/approval-controller';
import { ControllerMessenger } from '@metamask/base-controller';
import {
CurrencyRateController,
TokenListController,
TokensController,
TokenRatesController,
NftController,
AssetsContractController,
NftDetectionController,
} from '@metamask/assets-controllers';
import { PhishingController } from '@metamask/phishing-controller';
import { AnnouncementController } from '@metamask/announcement-controller';
import { GasFeeController } from '@metamask/gas-fee-controller';
import {
PermissionController,
PermissionsRequestNotFoundError,
} from '@metamask/permission-controller';
import {
SubjectMetadataController,
SubjectType,
} from '@metamask/subject-metadata-controller';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { RateLimitController } from '@metamask/rate-limit-controller';
import { NotificationController } from '@metamask/notification-controller';
///: END:ONLY_INCLUDE_IN
import SmartTransactionsController from '@metamask/smart-transactions-controller';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import {
CronjobController,
SnapController,
IframeExecutionService,
} from '@metamask/snaps-controllers';
///: END:ONLY_INCLUDE_IN
import browser from 'webextension-polyfill';
import {
AssetType,
TransactionStatus,
TransactionType,
TokenStandard,
} from '../../shared/constants/transaction';
import {
GAS_API_BASE_URL,
GAS_DEV_API_BASE_URL,
SWAPS_CLIENT_ID,
} from '../../shared/constants/swaps';
import { CHAIN_IDS, NETWORK_TYPES } from '../../shared/constants/network';
import {
HardwareDeviceNames,
HardwareKeyringTypes,
} from '../../shared/constants/hardware-wallets';
import {
CaveatTypes,
RestrictedMethods,
///: BEGIN:ONLY_INCLUDE_IN(flask)
EndowmentPermissions,
ExcludedSnapPermissions,
ExcludedSnapEndowments,
///: END:ONLY_INCLUDE_IN
} from '../../shared/constants/permissions';
import { UI_NOTIFICATIONS } from '../../shared/notifications';
import {
toChecksumHexAddress,
stripHexPrefix,
} from '../../shared/modules/hexstring-utils';
import { MILLISECOND, SECOND } from '../../shared/constants/time';
import {
ORIGIN_METAMASK,
///: BEGIN:ONLY_INCLUDE_IN(flask)
MESSAGE_TYPE,
SNAP_DIALOG_TYPES,
///: END:ONLY_INCLUDE_IN
POLLING_TOKEN_ENVIRONMENT_TYPES,
} from '../../shared/constants/app';
import { EVENT, EVENT_NAMES } from '../../shared/constants/metametrics';
import {
getTokenIdParam,
fetchTokenBalance,
} from '../../shared/lib/token-util.ts';
import { isEqualCaseInsensitive } from '../../shared/modules/string-utils';
import { parseStandardTokenTransactionData } from '../../shared/modules/transaction.utils';
import { STATIC_MAINNET_TOKEN_LIST } from '../../shared/constants/tokens';
import { getTokenValueParam } from '../../shared/lib/metamask-controller-utils';
import { isManifestV3 } from '../../shared/modules/mv3.utils';
import { hexToDecimal } from '../../shared/modules/conversion.utils';
///: BEGIN:ONLY_INCLUDE_IN(flask)
import { isMain, isFlask } from '../../shared/constants/environment';
// eslint-disable-next-line import/order
import { DesktopController } from '@metamask/desktop/dist/controllers/desktop';
///: END:ONLY_INCLUDE_IN
import {
onMessageReceived,
checkForMultipleVersionsRunning,
} from './detect-multiple-instances';
import ComposableObservableStore from './lib/ComposableObservableStore';
import AccountTracker from './lib/account-tracker';
import createDupeReqFilterMiddleware from './lib/createDupeReqFilterMiddleware';
import createLoggerMiddleware from './lib/createLoggerMiddleware';
import {
createMethodMiddleware,
///: BEGIN:ONLY_INCLUDE_IN(flask)
createSnapMethodMiddleware,
///: END:ONLY_INCLUDE_IN
} 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 BackupController from './controllers/backup';
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 { previousValueComparator } from './lib/util';
import createMetamaskMiddleware from './lib/createMetamaskMiddleware';
import {
CaveatMutatorFactories,
getCaveatSpecifications,
getChangedAccounts,
getPermissionBackgroundApiMethods,
getPermissionSpecifications,
getPermittedAccountsByOrigin,
NOTIFICATION_NAMES,
PermissionLogController,
unrestrictedMethods,
///: BEGIN:ONLY_INCLUDE_IN(flask)
buildSnapEndowmentSpecifications,
buildSnapRestrictedMethodSpecifications,
///: END:ONLY_INCLUDE_IN
} from './controllers/permissions';
import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware';
import { securityProviderCheck } from './lib/security-provider-helpers';
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 the `controllers` repo and export them
APPROVAL_STATE_CHANGE: 'ApprovalController:stateChange',
};
// stream channels
const PHISHING_SAFELIST = 'metamask-phishing-safelist';
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.browser;
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();
// instance of a class that wraps the extension's storage local API.
this.localStoreApiWrapper = opts.localStore;
// 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({
state: initState.NetworkController,
infuraProjectId: opts.infuraProjectId,
trackMetaMetricsEvent: (...args) =>
this.metaMetricsController.trackEvent(...args),
});
this.networkController.initializeProvider();
this.provider =
this.networkController.getProviderAndBlockTracker().provider;
this.blockTracker =
this.networkController.getProviderAndBlockTracker().blockTracker;
const tokenListMessenger = this.controllerMessenger.getRestricted({
name: 'TokenListController',
});
this.tokenListController = new TokenListController({
chainId: hexToDecimal(
this.networkController.store.getState().provider.chainId,
),
preventPollingOnNetworkRestart: initState.TokenListController
? initState.TokenListController.preventPollingOnNetworkRestart
: true,
onNetworkStateChange: (cb) => {
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
});
},
messenger: tokenListMessenger,
state: initState.TokenListController,
});
this.preferencesController = new PreferencesController({
initState: initState.PreferencesController,
initLangCode: opts.initLangCode,
openPopup: opts.openPopup,
network: this.networkController,
tokenListController: this.tokenListController,
provider: this.provider,
});
this.tokensController = new TokensController({
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(
this.preferencesController.store,
),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
},
};
return cb(modifiedNetworkState);
}),
config: { provider: this.provider },
state: initState.TokensController,
});
this.assetsContractController = new AssetsContractController(
{
onPreferencesStateChange: (listener) =>
this.preferencesController.store.subscribe(listener),
onNetworkStateChange: (cb) =>
this.networkController.on(NETWORK_EVENTS.NETWORK_DID_CHANGE, () => {
const networkState = this.networkController.store.getState();
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
},
{
provider: this.provider,
},
initState.AssetsContractController,
);
this.nftController = new NftController(
{
onPreferencesStateChange:
this.preferencesController.store.subscribe.bind(
this.preferencesController.store,
),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
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,
),
onNftAdded: ({ address, symbol, tokenId, standard, source }) =>
this.metaMetricsController.trackEvent({
event: EVENT_NAMES.NFT_ADDED,
category: EVENT.CATEGORIES.WALLET,
properties: {
token_contract_address: address,
token_symbol: symbol,
asset_type: AssetType.NFT,
token_standard: standard,
source,
},
sensitiveProperties: {
tokenId,
},
}),
},
{},
initState.NftController,
);
this.nftController.setApiKey(process.env.OPENSEA_KEY);
this.nftDetectionController = new NftDetectionController({
onNftsStateChange: (listener) => this.nftController.subscribe(listener),
onPreferencesStateChange: this.preferencesController.store.subscribe.bind(
this.preferencesController.store,
),
onNetworkStateChange: (cb) =>
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
getOpenSeaApiKey: () => this.nftController.openSeaApiKey,
getBalancesInSingleCall:
this.assetsContractController.getBalancesInSingleCall.bind(
this.assetsContractController,
),
addNft: this.nftController.addNft.bind(this.nftController),
getNftState: () => this.nftController.state,
});
this.metaMetricsController = new MetaMetricsController({
segment,
preferencesStore: this.preferencesController.store,
onNetworkDidChange: this.networkController.on.bind(
this.networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
),
getNetworkIdentifier: () => {
const { type, rpcUrl } =
this.networkController.store.getState().provider;
return type === NETWORK_TYPES.RPC ? rpcUrl : type;
},
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
version: this.platform.getVersion(),
environment: process.env.METAMASK_ENVIRONMENT,
extension: this.extension,
initState: initState.MetaMetricsController,
captureException,
});
this.on('update', (update) => {
this.metaMetricsController.handleMetaMaskStateUpdate(update);
});
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({
state: initState.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/<chain_id>/gasPrices`,
EIP1559APIEndpoint: `${gasApiBaseUrl}/networks/<chain_id>/suggestedGasFees`,
getCurrentNetworkLegacyGasAPICompatibility: () => {
const { chainId } = this.networkController.store.getState().provider;
return process.env.IN_TEST || chainId === CHAIN_IDS.MAINNET;
},
getChainId: () => {
return process.env.IN_TEST
? CHAIN_IDS.MAINNET
: this.networkController.store.getState().provider.chainId;
},
});
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,
nativeCurrency: this.networkController.providerStore.getState().ticker,
},
});
this.phishingController = new PhishingController(
{},
initState.PhishingController,
);
this.phishingController.maybeUpdateState();
if (process.env.IN_TEST) {
this.phishingController.setHotlistRefreshInterval(5 * SECOND);
this.phishingController.setStalelistRefreshInterval(30 * SECOND);
}
this.announcementController = new AnnouncementController(
{ allAnnouncements: UI_NOTIFICATIONS },
initState.AnnouncementController,
);
// 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,
providerConfig: {
...networkState.provider,
chainId: hexToDecimal(networkState.provider.chainId),
},
};
return cb(modifiedNetworkState);
}),
},
{
disabled:
!this.preferencesController.store.getState().useCurrencyRateCheck,
},
initState.TokenRatesController,
);
this.preferencesController.store.subscribe(
previousValueComparator((prevState, currState) => {
const { useCurrencyRateCheck: prevUseCurrencyRateCheck } = prevState;
const { useCurrencyRateCheck: currUseCurrencyRateCheck } = currState;
if (currUseCurrencyRateCheck && !prevUseCurrencyRateCheck) {
this.currencyRateController.start();
this.tokenRatesController.configure(
{ disabled: false },
false,
false,
);
} else if (!currUseCurrencyRateCheck && prevUseCurrencyRateCheck) {
this.currencyRateController.stop();
this.tokenRatesController.configure({ disabled: true }, false, false);
}
}, this.preferencesController.store.getState()),
);
this.ensController = new EnsController({
provider: this.provider,
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
onNetworkDidChange: this.networkController.on.bind(
this.networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
),
});
this.onboardingController = new OnboardingController({
initState: initState.OnboardingController,
});
this.incomingTransactionsController = new IncomingTransactionsController({
blockTracker: this.blockTracker,
onNetworkDidChange: this.networkController.on.bind(
this.networkController,
NETWORK_EVENTS.NETWORK_DID_CHANGE,
),
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
preferencesController: this.preferencesController,
onboardingController: this.onboardingController,
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.store.getState().provider.chainId,
getNetworkIdentifier: () => {
const { type, rpcUrl } =
this.networkController.store.getState().provider;
return type === NETWORK_TYPES.RPC ? rpcUrl : type;
},
preferencesController: this.preferencesController,
onboardingController: this.onboardingController,
});
// start and stop polling for balances based on activeControllerConnections
this.on('controllerConnectionChanged', (activeControllerConnections) => {
const { completedOnboarding } =
this.onboardingController.store.getState();
if (activeControllerConnections > 0 && completedOnboarding) {
this.triggerNetworkrequests();
} else {
this.stopNetworkRequests();
}
});
this.onboardingController.store.subscribe(
previousValueComparator(async (prevState, currState) => {
const { completedOnboarding: prevCompletedOnboarding } = prevState;
const { completedOnboarding: currCompletedOnboarding } = currState;
if (!prevCompletedOnboarding && currCompletedOnboarding) {
this.triggerNetworkrequests();
}
}, this.onboardingController.store.getState()),
);
this.cachedBalancesController = new CachedBalancesController({
accountTracker: this.accountTracker,
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
initState: initState.CachedBalancesController,
});
this.tokensController.hub.on('pendingSuggestedAsset', async () => {
await opts.openPopup();
});
let additionalKeyrings = [keyringBuilderFactory(QRHardwareKeyring)];
if (this.canUseHardwareWallets()) {
const keyringOverrides = this.opts.overrides?.keyrings;
const additionalKeyringTypes = [
keyringOverrides?.trezor || TrezorKeyring,
keyringOverrides?.ledger || LedgerBridgeKeyring,
keyringOverrides?.lattice || LatticeKeyring,
QRHardwareKeyring,
];
additionalKeyrings = additionalKeyringTypes.map((keyringType) =>
keyringBuilderFactory(keyringType),
);
}
this.keyringController = new KeyringController({
keyringBuilders: additionalKeyrings,
initState: initState.KeyringController,
encryptor: opts.encryptor || undefined,
cacheEncryptionKey: isManifestV3,
});
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,
),
captureKeyringTypesWithMissingIdentities: (
identities = {},
accounts = [],
) => {
const accountsMissingIdentities = accounts.filter(
(address) => !identities[address],
);
const keyringTypesWithMissingIdentities =
accountsMissingIdentities.map(
(address) =>
this.keyringController.getKeyringForAccount(address)?.type,
);
const identitiesCount = Object.keys(identities || {}).length;
const accountTrackerCount = Object.keys(
this.accountTracker.store.getState().accounts || {},
).length;
captureException(
new Error(
`Attempt to get permission specifications failed because their were ${accounts.length} accounts, but ${identitiesCount} identities, and the ${keyringTypesWithMissingIdentities} keyrings included accounts with missing identities. Meanwhile, there are ${accountTrackerCount} accounts in the account tracker.`,
),
);
},
}),
///: BEGIN:ONLY_INCLUDE_IN(flask)
...this.getSnapPermissionSpecifications(),
///: END:ONLY_INCLUDE_IN
},
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,
});
///: BEGIN:ONLY_INCLUDE_IN(flask)
const snapExecutionServiceArgs = {
iframeUrl: new URL(
'https://metamask.github.io/iframe-execution-environment/0.13.0',
),
messenger: this.controllerMessenger.getRestricted({
name: 'ExecutionService',
}),
setupSnapProvider: this.setupSnapProvider.bind(this),
};
this.snapExecutionService =
this.opts.overrides?.createSnapExecutionService?.(
snapExecutionServiceArgs,
) || new IframeExecutionService(snapExecutionServiceArgs);
const snapControllerMessenger = this.controllerMessenger.getRestricted({
name: 'SnapController',
allowedEvents: [
'ExecutionService:unhandledError',
'ExecutionService:outboundRequest',
'ExecutionService:outboundResponse',
],
allowedActions: [
`${this.permissionController.name}:getEndowments`,
`${this.permissionController.name}:getPermissions`,
`${this.permissionController.name}:hasPermission`,
`${this.permissionController.name}:hasPermissions`,
`${this.permissionController.name}:requestPermissions`,
`${this.permissionController.name}:revokeAllPermissions`,
`${this.permissionController.name}:revokePermissions`,
`${this.permissionController.name}:revokePermissionForAllSubjects`,
`${this.permissionController.name}:getSubjectNames`,
`${this.permissionController.name}:updateCaveat`,
`${this.approvalController.name}:addRequest`,
`${this.permissionController.name}:grantPermissions`,
`${this.subjectMetadataController.name}:getSubjectMetadata`,
'ExecutionService:executeSnap',
'ExecutionService:getRpcRequestHandler',
'ExecutionService:terminateSnap',
'ExecutionService:terminateAllSnaps',
'ExecutionService:handleRpcRequest',
],
});
this.snapController = new SnapController({
environmentEndowmentPermissions: Object.values(EndowmentPermissions),
excludedPermissions: {
...ExcludedSnapPermissions,
...ExcludedSnapEndowments,
},
closeAllConnections: this.removeAllConnections.bind(this),
state: initState.SnapController,
messenger: snapControllerMessenger,
featureFlags: {
dappsCanUpdateSnaps: true,
allowLocalSnaps: isFlask,
requireAllowlist: isMain,
},
});
this.notificationController = new NotificationController({
messenger: this.controllerMessenger.getRestricted({
name: 'NotificationController',
}),
state: initState.NotificationController,
});
this.rateLimitController = new RateLimitController({
state: initState.RateLimitController,
messenger: this.controllerMessenger.getRestricted({
name: 'RateLimitController',
}),
implementations: {
showNativeNotification: (origin, message) => {
const subjectMetadataState = this.controllerMessenger.call(
'SubjectMetadataController:getState',
);
const originMetadata = subjectMetadataState.subjectMetadata[origin];
this.platform._showNotification(
originMetadata?.name ?? origin,
message,
);
return null;
},
showInAppNotification: (origin, message) => {
this.controllerMessenger.call(
'NotificationController:show',
origin,
message,
);
return null;
},
},
});
// --- Snaps Cronjob Controller configuration
const cronjobControllerMessenger = this.controllerMessenger.getRestricted({
name: 'CronjobController',
allowedEvents: [
'SnapController:snapInstalled',
'SnapController:snapUpdated',
'SnapController:snapRemoved',
],
allowedActions: [
`${this.permissionController.name}:getPermissions`,
'SnapController:handleRequest',
'SnapController:getAll',
],
});
this.cronjobController = new CronjobController({
state: initState.CronjobController,
messenger: cronjobControllerMessenger,
});
this.desktopController = new DesktopController({
initState: initState.DesktopController,
});
///: END:ONLY_INCLUDE_IN
this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,
tokensController: this.tokensController,
assetsContractController: this.assetsContractController,
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.addressBookController = new AddressBookController(
undefined,
initState.AddressBookController,
);
this.alertController = new AlertController({
initState: initState.AlertController,
preferencesStore: this.preferencesController.store,
});
this.backupController = new BackupController({
preferencesController: this.preferencesController,
addressBookController: this.addressBookController,
networkController: this.networkController,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
});
this.txController = new TransactionController({
initState:
initState.TransactionController || initState.TransactionManager,
getPermittedAccounts: this.getPermittedAccounts.bind(this),
getProviderConfig: () => this.networkController.store.getState().provider,
getCurrentNetworkEIP1559Compatibility:
this.networkController.getEIP1559Compatibility.bind(
this.networkController,
),
getCurrentAccountEIP1559Compatibility:
this.getCurrentAccountEIP1559Compatibility.bind(this),
getNetworkState: () => this.networkController.store.getState().network,
onNetworkStateChange: (listener) =>
this.networkController.networkStore.subscribe(listener),
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
preferencesStore: this.preferencesController.store,
txHistoryLimit: 60,
signTransaction: this.keyringController.signTransaction.bind(
this.keyringController,
),
provider: this.provider,
blockTracker: this.blockTracker,
createEventFragment: this.metaMetricsController.createEventFragment.bind(
this.metaMetricsController,
),
updateEventFragment: this.metaMetricsController.updateEventFragment.bind(
this.metaMetricsController,
),
finalizeEventFragment:
this.metaMetricsController.finalizeEventFragment.bind(
this.metaMetricsController,
),
getEventFragmentById:
this.metaMetricsController.getEventFragmentById.bind(
this.metaMetricsController,
),
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
getParticipateInMetrics: () =>
this.metaMetricsController.state.participateInMetaMetrics,
getEIP1559GasFeeEstimates:
this.gasFeeController.fetchGasFeeEstimates.bind(this.gasFeeController),
getExternalPendingTransactions:
this.getExternalPendingTransactions.bind(this),
getAccountType: this.getAccountType.bind(this),
getDeviceModel: this.getDeviceModel.bind(this),
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
securityProviderRequest: this.securityProviderRequest.bind(this),
});
this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation());
this.txController.on(`tx:status-update`, async (txId, status) => {
if (
status === TransactionStatus.confirmed ||
status === TransactionStatus.failed
) {
const txMeta = this.txController.txStateManager.getTransaction(txId);
let rpcPrefs = {};
if (txMeta.chainId) {
const { networkConfigurations } =
this.networkController.store.getState();
const matchingNetworkConfig = Object.values(
networkConfigurations,
).find(
(networkConfiguration) =>
networkConfiguration.chainId === txMeta.chainId,
);
rpcPrefs = matchingNetworkConfig?.rpcPrefs ?? {};
}
this.platform.showTransactionNotification(txMeta, rpcPrefs);
const { txReceipt } = txMeta;
// if this is a transferFrom method generated from within the app it may be an NFT transfer transaction
// in which case we will want to check and update ownership status of the transferred NFT.
if (
txMeta.type === TransactionType.tokenMethodTransferFrom &&
txMeta.txParams !== undefined
) {
const {
data,
to: contractAddress,
from: userAddress,
} = txMeta.txParams;
const { chainId } = txMeta;
const transactionData = parseStandardTokenTransactionData(data);
// Sometimes the tokenId value is parsed as "_value" param. Not seeing this often any more, but still occasionally:
// i.e. call approve() on BAYC contract - https://etherscan.io/token/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d#writeContract, and tokenId shows up as _value,
// not sure why since it doesn't match the ERC721 ABI spec we use to parse these transactions - https://github.com/MetaMask/metamask-eth-abis/blob/d0474308a288f9252597b7c93a3a8deaad19e1b2/src/abis/abiERC721.ts#L62.
const transactionDataTokenId =
getTokenIdParam(transactionData) ??
getTokenValueParam(transactionData);
const { allNfts } = this.nftController.state;
const chainIdAsDecimal = hexToDecimal(chainId);
// check if its a known NFT
const knownNft = allNfts?.[userAddress]?.[chainIdAsDecimal]?.find(
({ address, tokenId }) =>
isEqualCaseInsensitive(address, contractAddress) &&
tokenId === transactionDataTokenId,
);
// if it is we check and update ownership status.
if (knownNft) {
this.nftController.checkAndUpdateSingleNftOwnershipStatus(
knownNft,
false,
{ userAddress, chainId: chainIdAsDecimal },
);
}
}
const metamaskState = this.getState();
if (txReceipt && txReceipt.status === '0x0') {
this.metaMetricsController.trackEvent(
{
event: 'Tx Status Update: On-Chain Failure',
category: EVENT.CATEGORIES.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.store.getState().provider;
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,
),
securityProviderRequest: this.securityProviderRequest.bind(this),
});
this.personalMessageManager = new PersonalMessageManager({
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
securityProviderRequest: this.securityProviderRequest.bind(this),
});
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.store.getState().provider.chainId,
metricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
securityProviderRequest: this.securityProviderRequest.bind(this),
});
this.swapsController = new SwapsController({
getBufferedGasLimit: this.txController.txGasUtil.getBufferedGasLimit.bind(
this.txController.txGasUtil,
),
networkController: this.networkController,
provider: this.provider,
getProviderConfig: () => this.networkController.store.getState().provider,
getTokenRatesState: () => this.tokenRatesController.state,
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
getEIP1559GasFeeEstimates:
this.gasFeeController.fetchGasFeeEstimates.bind(this.gasFeeController),
});
this.smartTransactionsController = new SmartTransactionsController(
{
onNetworkStateChange: (cb) => {
this.networkController.store.subscribe((networkState) => {
const modifiedNetworkState = {
...networkState,
providerConfig: {
...networkState.provider,
},
};
return cb(modifiedNetworkState);
});
},
getNetwork: () => this.networkController.store.getState().network,
getNonceLock: this.txController.nonceTracker.getNonceLock.bind(
this.txController.nonceTracker,
),
confirmExternalTransaction:
this.txController.confirmExternalTransaction.bind(this.txController),
provider: this.provider,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
},
{
supportedChainIds: [CHAIN_IDS.MAINNET, CHAIN_IDS.GOERLI],
},
initState.SmartTransactionsController,
);
// 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();
});
this.metamaskMiddleware = createMetamaskMiddleware({
static: {
eth_syncing: false,
web3_clientVersion: `MetaMask/v${version}`,
},
version,
// account mgmt
getAccounts: async (
{ origin: innerOrigin },
{ suppressUnauthorizedError = true } = {},
) => {
if (innerOrigin === ORIGIN_METAMASK) {
const selectedAddress =
this.preferencesController.getSelectedAddress();
return selectedAddress ? [selectedAddress] : [];
} else if (this.isUnlocked()) {
return await this.getPermittedAccounts(innerOrigin, {
suppressUnauthorizedError,
});
}
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: TransactionStatus.submitted,
},
})[0],
});
// ensure isClientOpenAndUnlocked is updated when memState updates
this.on('update', (memState) => this._onStateUpdate(memState));
/**
* All controllers in Memstore but not in store. They are not persisted.
* On chrome profile re-start, they will be re-initialized.
*/
const resetOnRestartStore = {
AccountTracker: this.accountTracker.store,
TxController: this.txController.memStore,
TokenRatesController: this.tokenRatesController,
MessageManager: this.messageManager.memStore,
PersonalMessageManager: this.personalMessageManager.memStore,
DecryptMessageManager: this.decryptMessageManager.memStore,
EncryptionPublicKeyManager: this.encryptionPublicKeyManager.memStore,
TypesMessageManager: this.typedMessageManager.memStore,
SwapsController: this.swapsController.store,
EnsController: this.ensController.store,
ApprovalController: this.approvalController,
};
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,
BackupController: this.backupController,
AnnouncementController: this.announcementController,
GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController,
TokensController: this.tokensController,
SmartTransactionsController: this.smartTransactionsController,
NftController: this.nftController,
PhishingController: this.phishingController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
NotificationController: this.notificationController,
DesktopController: this.desktopController.store,
///: END:ONLY_INCLUDE_IN
...resetOnRestartStore,
});
this.memStore = new ComposableObservableStore({
config: {
AppStateController: this.appStateController.store,
NetworkController: this.networkController.store,
CachedBalancesController: this.cachedBalancesController.store,
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,
BackupController: this.backupController,
AnnouncementController: this.announcementController,
GasFeeController: this.gasFeeController,
TokenListController: this.tokenListController,
TokensController: this.tokensController,
SmartTransactionsController: this.smartTransactionsController,
NftController: this.nftController,
///: BEGIN:ONLY_INCLUDE_IN(flask)
SnapController: this.snapController,
CronjobController: this.cronjobController,
NotificationController: this.notificationController,
DesktopController: this.desktopController.store,
///: END:ONLY_INCLUDE_IN
...resetOnRestartStore,
},
controllerMessenger: this.controllerMessenger,
});
// if this is the first time, clear the state of by calling these methods
const resetMethods = [
this.accountTracker.resetState,
this.txController.resetState,
this.messageManager.resetState,
this.personalMessageManager.resetState,
this.decryptMessageManager.resetState,
this.encryptionPublicKeyManager.resetState,
this.typedMessageManager.resetState,
this.swapsController.resetState,
this.ensController.resetState,
this.approvalController.clear.bind(this.approvalController),
// WE SHOULD ADD TokenListController.resetState here too. But it's not implemented yet.
];
if (isManifestV3) {
if (globalThis.isFirstTimeProfileLoaded === true) {
this.resetStates(resetMethods);
}
} else {
// it's always the first time in MV2
this.resetStates(resetMethods);
}
// Automatic login via config password or loginToken
if (
!this.isUnlocked() &&
this.onboardingController.store.getState().completedOnboarding
) {
this._loginUser();
} else {
this._startUISync();
}
// Lazily update the store with the current extension environment
this.extension.runtime.getPlatformInfo().then(({ os }) => {
this.appStateController.setBrowserEnvironment(
os,
// This method is presently only supported by Firefox
this.extension.runtime.getBrowserInfo === undefined
? 'chrome'
: 'firefox',
);
});
this.setupControllerEventSubscriptions();
// For more information about these legacy streams, see here:
// https://github.com/MetaMask/metamask-extension/issues/15491
// TODO:LegacyProvider: Delete
this.publicConfigStore = this.createPublicConfigStore();
// Multiple MetaMask instances launched warning
this.extension.runtime.onMessageExternal.addListener(onMessageReceived);
// Fire a ping message to check if other extensions are running
checkForMultipleVersionsRunning();
}
triggerNetworkrequests() {
this.accountTracker.start();
this.incomingTransactionsController.start();
if (this.preferencesController.store.getState().useCurrencyRateCheck) {
this.currencyRateController.start();
}
if (this.preferencesController.store.getState().useTokenDetection) {
this.tokenListController.start();
}
}
stopNetworkRequests() {
this.accountTracker.stop();
this.incomingTransactionsController.stop();
if (this.preferencesController.store.getState().useCurrencyRateCheck) {
this.currencyRateController.stop();
}
if (this.preferencesController.store.getState().useTokenDetection) {
this.tokenListController.stop();
}
}
canUseHardwareWallets() {
return !isManifestV3 || process.env.CONF?.HARDWARE_WALLETS_MV3;
}
resetStates(resetMethods) {
resetMethods.forEach((resetMethod) => {
try {
resetMethod();
} catch (err) {
console.error(err);
}
});
globalThis.isFirstTimeProfileLoaded = false;
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
/**
* Constructor helper for getting Snap permission specifications.
*/
getSnapPermissionSpecifications() {
return {
...buildSnapEndowmentSpecifications(),
...buildSnapRestrictedMethodSpecifications({
clearSnapState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:clearSnapState',
),
getMnemonic: this.getPrimaryKeyringMnemonic.bind(this),
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
this.appStateController,
),
getSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:get',
),
handleSnapRpcRequest: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:handleRequest',
),
getSnapState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:getSnapState',
),
showConfirmation: (origin, confirmationData) =>
this.approvalController.addAndShowApprovalRequest({
origin,
type: MESSAGE_TYPE.SNAP_DIALOG_CONFIRMATION,
requestData: confirmationData,
}),
showDialog: (origin, type, content, placeholder) =>
this.approvalController.addAndShowApprovalRequest({
origin,
type: SNAP_DIALOG_TYPES[type],
requestData: { content, placeholder },
}),
showNativeNotification: (origin, args) =>
this.controllerMessenger.call(
'RateLimitController:call',
origin,
'showNativeNotification',
origin,
args.message,
),
showInAppNotification: (origin, args) =>
this.controllerMessenger.call(
'RateLimitController:call',
origin,
'showInAppNotification',
origin,
args.message,
),
updateSnapState: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:updateSnapState',
),
}),
};
}
/**
* Deletes the specified notifications from state.
*
* @param {string[]} ids - The notifications ids to delete.
*/
dismissNotifications(ids) {
this.notificationController.dismiss(ids);
}
/**
* Updates the readDate attribute of the specified notifications.
*
* @param {string[]} ids - The notifications ids to mark as read.
*/
markNotificationsAsRead(ids) {
this.notificationController.markRead(ids);
}
///: END:ONLY_INCLUDE_IN
/**
* 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 documentation for
* `@metamask/base-controller` 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,
);
///: BEGIN:ONLY_INCLUDE_IN(flask)
// Record Snap metadata whenever a Snap is added to state.
this.controllerMessenger.subscribe(
`${this.snapController.name}:snapAdded`,
(snap, svgIcon = null) => {
const {
manifest: { proposedName },
version,
} = snap;
this.subjectMetadataController.addSubjectMetadata({
subjectType: SubjectType.Snap,
name: proposedName,
origin: snap.id,
version,
svgIcon,
});
},
);
this.controllerMessenger.subscribe(
`${this.snapController.name}:snapInstalled`,
(truncatedSnap) => {
this.metaMetricsController.trackEvent({
event: 'Snap Installed',
category: EVENT.CATEGORIES.SNAPS,
properties: {
snap_id: truncatedSnap.id,
version: truncatedSnap.version,
},
});
},
);
this.controllerMessenger.subscribe(
`${this.snapController.name}:snapUpdated`,
(newSnap, oldVersion) => {
this.metaMetricsController.trackEvent({
event: 'Snap Updated',
category: EVENT.CATEGORIES.SNAPS,
properties: {
snap_id: newSnap.id,
old_version: oldVersion,
new_version: newSnap.version,
},
});
},
);
this.controllerMessenger.subscribe(
`${this.snapController.name}:snapTerminated`,
(truncatedSnap) => {
const approvals = Object.values(
this.approvalController.state.pendingApprovals,
).filter(
(approval) =>
approval.origin === truncatedSnap.id &&
approval.type.startsWith(RestrictedMethods.snap_dialog),
);
for (const approval of approvals) {
this.approvalController.reject(
approval.id,
new Error('Snap was terminated.'),
);
}
},
);
this.controllerMessenger.subscribe(
`${this.snapController.name}:snapRemoved`,
(truncatedSnap) => {
const notificationIds = Object.values(
this.notificationController.state.notifications,
).reduce((idList, notification) => {
if (notification.origin === truncatedSnap.id) {
idList.push(notification.id);
}
return idList;
}, []);
this.dismissNotifications(notificationIds);
},
);
///: END:ONLY_INCLUDE_IN
}
/**
* 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.store.getState().provider;
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.store.getState().provider.chainId,
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,
appStateController,
nftController,
nftDetectionController,
currencyRateController,
detectTokensController,
ensController,
gasFeeController,
metaMetricsController,
networkController,
announcementController,
onboardingController,
permissionController,
preferencesController,
qrHardwareKeyring,
swapsController,
tokensController,
smartTransactionsController,
txController,
assetsContractController,
backupController,
approvalController,
} = 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,
),
setUseMultiAccountBalanceChecker:
preferencesController.setUseMultiAccountBalanceChecker.bind(
preferencesController,
),
setUseTokenDetection: preferencesController.setUseTokenDetection.bind(
preferencesController,
),
setUseNftDetection: preferencesController.setUseNftDetection.bind(
preferencesController,
),
setUseCurrencyRateCheck:
preferencesController.setUseCurrencyRateCheck.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),
getRequestAccountTabIds: this.getRequestAccountTabIds,
getOpenMetamaskTabsIds: this.getOpenMetamaskTabsIds,
markNotificationPopupAsAutomaticallyClosed: () =>
this.notificationManager.markAsAutomaticallyClosed(),
// approval
requestUserApproval:
approvalController.addAndShowApprovalRequest.bind(approvalController),
// 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),
removeNetworkConfiguration:
networkController.removeNetworkConfiguration.bind(networkController),
setActiveNetwork:
networkController.setActiveNetwork.bind(networkController),
upsertNetworkConfiguration:
this.networkController.upsertNetworkConfiguration.bind(
this.networkController,
),
// 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),
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,
),
setDisabledRpcMethodPreference:
preferencesController.setDisabledRpcMethodPreference.bind(
preferencesController,
),
getRpcMethodPreferences:
preferencesController.getRpcMethodPreferences.bind(
preferencesController,
),
setAdvancedGasFee: preferencesController.setAdvancedGasFee.bind(
preferencesController,
),
setTheme: preferencesController.setTheme.bind(preferencesController),
setTransactionSecurityCheckEnabled:
preferencesController.setTransactionSecurityCheckEnabled.bind(
preferencesController,
),
// AssetsContractController
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
// NftController
addNft: nftController.addNft.bind(nftController),
addNftVerifyOwnership:
nftController.addNftVerifyOwnership.bind(nftController),
removeAndIgnoreNft: nftController.removeAndIgnoreNft.bind(nftController),
removeNft: nftController.removeNft.bind(nftController),
checkAndUpdateAllNftsOwnershipStatus:
nftController.checkAndUpdateAllNftsOwnershipStatus.bind(nftController),
checkAndUpdateSingleNftOwnershipStatus:
nftController.checkAndUpdateSingleNftOwnershipStatus.bind(
nftController,
),
isNftOwner: nftController.isNftOwner.bind(nftController),
// 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,
),
setOutdatedBrowserWarningLastShown:
appStateController.setOutdatedBrowserWarningLastShown.bind(
appStateController,
),
setShowTestnetMessageInDropdown:
appStateController.setShowTestnetMessageInDropdown.bind(
appStateController,
),
setShowBetaHeader:
appStateController.setShowBetaHeader.bind(appStateController),
updateNftDropDownState:
appStateController.updateNftDropDownState.bind(appStateController),
setFirstTimeUsedNetwork:
appStateController.setFirstTimeUsedNetwork.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: this.exportAccount.bind(this),
// txController
cancelTransaction: txController.cancelTransaction.bind(txController),
updateTransaction: txController.updateTransaction.bind(txController),
updateAndApproveTransaction:
txController.updateAndApproveTransaction.bind(txController),
approveTransactionsWithSameNonce:
txController.approveTransactionsWithSameNonce.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),
createTransactionEventFragment:
txController.createTransactionEventFragment.bind(txController),
getTransactions: txController.getTransactions.bind(txController),
updateEditableParams:
txController.updateEditableParams.bind(txController),
updateTransactionGasFees:
txController.updateTransactionGasFees.bind(txController),
updateTransactionSendFlowHistory:
txController.updateTransactionSendFlowHistory.bind(txController),
updateSwapApprovalTransaction:
txController.updateSwapApprovalTransaction.bind(txController),
updateSwapTransaction:
txController.updateSwapTransaction.bind(txController),
updatePreviousGasParams:
txController.updatePreviousGasParams.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),
// permissions
removePermissionsFor: this.removePermissionsFor,
approvePermissionsRequest: this.acceptPermissionsRequest,
rejectPermissionsRequest: this.rejectPermissionsRequest,
...getPermissionBackgroundApiMethods(permissionController),
///: BEGIN:ONLY_INCLUDE_IN(flask)
// snaps
removeSnapError: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:removeSnapError',
),
disableSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:disable',
),
enableSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:enable',
),
removeSnap: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:remove',
),
handleSnapRequest: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:handleRequest',
),
dismissNotifications: this.dismissNotifications.bind(this),
markNotificationsAsRead: this.markNotificationsAsRead.bind(this),
// Desktop
getDesktopEnabled: this.desktopController.getDesktopEnabled.bind(
this.desktopController,
),
setDesktopEnabled: this.desktopController.setDesktopEnabled.bind(
this.desktopController,
),
generateDesktopOtp: this.desktopController.generateOtp.bind(
this.desktopController,
),
testDesktopConnection: this.desktopController.testDesktopConnection.bind(
this.desktopController,
),
disableDesktop: this.desktopController.disableDesktop.bind(
this.desktopController,
),
///: END:ONLY_INCLUDE_IN
// 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),
setSwapsFeatureFlags:
swapsController.setSwapsFeatureFlags.bind(swapsController),
setSwapsUserFeeLevel:
swapsController.setSwapsUserFeeLevel.bind(swapsController),
setSwapsQuotesPollingLimitEnabled:
swapsController.setSwapsQuotesPollingLimitEnabled.bind(swapsController),
// Smart Transactions
setSmartTransactionsOptInStatus:
smartTransactionsController.setOptInState.bind(
smartTransactionsController,
),
fetchSmartTransactionFees: smartTransactionsController.getFees.bind(
smartTransactionsController,
),
clearSmartTransactionFees: smartTransactionsController.clearFees.bind(
smartTransactionsController,
),
submitSignedTransactions:
smartTransactionsController.submitSignedTransactions.bind(
smartTransactionsController,
),
cancelSmartTransaction:
smartTransactionsController.cancelSmartTransaction.bind(
smartTransactionsController,
),
fetchSmartTransactionsLiveness:
smartTransactionsController.fetchLiveness.bind(
smartTransactionsController,
),
updateSmartTransaction:
smartTransactionsController.updateSmartTransaction.bind(
smartTransactionsController,
),
setStatusRefreshInterval:
smartTransactionsController.setStatusRefreshInterval.bind(
smartTransactionsController,
),
// 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: this.resolvePendingApproval,
rejectPendingApproval: this.rejectPendingApproval,
// Notifications
updateViewedNotifications: announcementController.updateViewed.bind(
announcementController,
),
// 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),
// BackupController
backupUserData: backupController.backupUserData.bind(backupController),
restoreUserData: backupController.restoreUserData.bind(backupController),
// DetectTokenController
detectNewTokens: detectTokensController.detectNewTokens.bind(
detectTokensController,
),
// DetectCollectibleController
detectNfts: nftDetectionController.detectNfts.bind(
nftDetectionController,
),
/** Token Detection V2 */
addDetectedTokens:
tokensController.addDetectedTokens.bind(tokensController),
addImportedTokens: tokensController.addTokens.bind(tokensController),
ignoreTokens: tokensController.ignoreTokens.bind(tokensController),
getBalancesInSingleCall:
assetsContractController.getBalancesInSingleCall.bind(
assetsContractController,
),
};
}
async exportAccount(address, password) {
await this.verifyPassword(password);
return this.keyringController.exportAccount(address, password);
}
async getTokenStandardAndDetails(address, userAddress, tokenId) {
const { tokenList } = this.tokenListController.state;
const { tokens } = this.tokensController.state;
const staticTokenListDetails =
STATIC_MAINNET_TOKEN_LIST[address.toLowerCase()] || {};
const tokenListDetails = tokenList[address.toLowerCase()] || {};
const userDefinedTokenDetails =
tokens.find(({ address: _address }) =>
isEqualCaseInsensitive(_address, address),
) || {};
const tokenDetails = {
...staticTokenListDetails,
...tokenListDetails,
...userDefinedTokenDetails,
};
const tokenDetailsStandardIsERC20 =
isEqualCaseInsensitive(tokenDetails.standard, TokenStandard.ERC20) ||
tokenDetails.erc20 === true;
const noEvidenceThatTokenIsAnNFT =
!tokenId &&
!isEqualCaseInsensitive(tokenDetails.standard, TokenStandard.ERC1155) &&
!isEqualCaseInsensitive(tokenDetails.standard, TokenStandard.ERC721) &&
!tokenDetails.erc721;
const otherDetailsAreERC20Like =
tokenDetails.decimals !== undefined && tokenDetails.symbol;
const tokenCanBeTreatedAsAnERC20 =
tokenDetailsStandardIsERC20 ||
(noEvidenceThatTokenIsAnNFT && otherDetailsAreERC20Like);
let details;
if (tokenCanBeTreatedAsAnERC20) {
try {
const balance = await fetchTokenBalance(
address,
userAddress,
this.provider,
);
details = {
address,
balance,
standard: TokenStandard.ERC20,
decimals: tokenDetails.decimals,
symbol: tokenDetails.symbol,
};
} catch (e) {
// If the `fetchTokenBalance` call failed, `details` remains undefined, and we
// fall back to the below `assetsContractController.getTokenStandardAndDetails` call
log.warning(`Failed to get token balance. Error: ${e}`);
}
}
// `details`` will be undefined if `tokenCanBeTreatedAsAnERC20`` is false,
// or if it is true but the `fetchTokenBalance`` call failed. In either case, we should
// attempt to retrieve details from `assetsContractController.getTokenStandardAndDetails`
if (details === undefined) {
details = await this.assetsContractController.getTokenStandardAndDetails(
address,
userAddress,
tokenId,
);
}
return {
...details,
decimals: details?.decimals?.toString(10),
balance: details?.balance?.toString(10),
};
}
//=============================================================================
// 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 {number[]} encodedSeedPhrase - The seed phrase, encoded as an array
* of UTF-8 bytes.
*/
async createNewVaultAndRestore(password, encodedSeedPhrase) {
const releaseLock = await this.createVaultMutex.acquire();
try {
let accounts, lastBalance;
const seedPhraseAsBuffer = Buffer.from(encodedSeedPhrase);
const { keyringController } = this;
// clear known identities
this.preferencesController.setAddresses([]);
// clear permissions
this.permissionController.clearState();
///: BEGIN:ONLY_INCLUDE_IN(flask)
// Clear snap state
this.snapController.clearState();
// Clear notification state
this.notificationController.clear();
///: END:ONLY_INCLUDE_IN
// 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,
seedPhraseAsBuffer,
);
const ethQuery = new EthQuery(this.provider);
accounts = await keyringController.getAccounts();
lastBalance = await this.getBalance(
accounts[accounts.length - 1],
ethQuery,
);
const [primaryKeyring] = keyringController.getKeyringsByType(
HardwareKeyringTypes.hdKeyTree,
);
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<object>} Parts of the state that we want to syncx
*/
async fetchInfoToSync() {
// Preferences
const { currentLocale, identities, selectedAddress, useTokenDetection } =
this.preferencesController.store.getState();
const isTokenDetectionInactiveInMainnet =
!useTokenDetection &&
this.networkController.store.getState().provider.chainId ===
CHAIN_IDS.MAINNET;
const { networkConfigurations } = this.networkController.store.getState();
const { tokenList } = this.tokenListController.state;
const caseInSensitiveTokenList = isTokenDetectionInactiveInMainnet
? STATIC_MAINNET_TOKEN_LIST
: tokenList;
const preferences = {
currentLocale,
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) {
// the tokenList will be holding only erc20 tokens
if (
caseInSensitiveTokenList[asset.address?.toLowerCase()] !==
undefined
) {
return true;
}
} else if (asset.isERC721 === false) {
return true;
}
return false;
});
});
});
// Accounts
const [hdKeyring] = this.keyringController.getKeyringsByType(
HardwareKeyringTypes.hdKeyTree,
);
const simpleKeyPairKeyrings = this.keyringController.getKeyringsByType(
HardwareKeyringTypes.hdKeyTree,
);
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(),
networkConfigurations,
};
}
/**
* 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<object>} 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);
}
// 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();
}
async _loginUser() {
try {
// Automatic login via config password
const password = process.env.CONF?.PASSWORD;
if (password) {
await this.submitPassword(password);
}
// Automatic login via storage encryption key
else if (isManifestV3) {
await this.submitEncryptionKey();
}
// Updating accounts in this.accountTracker before starting UI syncing ensure that
// state has account balance before it is synced with UI
await this.accountTracker._updateAccounts();
} finally {
this._startUISync();
}
}
_startUISync() {
// Message startUISync is used in MV3 to start syncing state with UI
// Sending this message after login is completed helps to ensure that incomplete state without
// account details are not flushed to UI.
this.emit('startUISync');
this.startUISync = true;
this.memStore.subscribe(this.sendUpdate.bind(this));
}
/**
* Submits a user's encryption key to log the user in via login token
*/
async submitEncryptionKey() {
try {
const { loginToken, loginSalt } = await browser.storage.session.get([
'loginToken',
'loginSalt',
]);
if (loginToken && loginSalt) {
const { vault } = this.keyringController.store.getState();
const jsonVault = JSON.parse(vault);
if (jsonVault.salt !== loginSalt) {
console.warn(
'submitEncryptionKey: Stored salt and vault salt do not match',
);
await this.clearLoginArtifacts();
return;
}
await this.keyringController.submitEncryptionKey(loginToken, loginSalt);
}
} catch (e) {
// If somehow this login token doesn't work properly,
// remove it and the user will get shown back to the unlock screen
await this.clearLoginArtifacts();
throw e;
}
}
async clearLoginArtifacts() {
await browser.storage.session.remove(['loginToken', 'loginSalt']);
}
/**
* 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.
* 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);
this.preferencesController.setSelectedAddress(address);
}
/**
* Gets the mnemonic of the user's primary keyring.
*/
getPrimaryKeyringMnemonic() {
const [keyring] = this.keyringController.getKeyringsByType(
HardwareKeyringTypes.hdKeyTree,
);
if (!keyring.mnemonic) {
throw new Error('Primary keyring mnemonic unavailable.');
}
return keyring.mnemonic;
}
//
// Hardware
//
async getKeyringForDevice(deviceName, hdPath = null) {
const keyringOverrides = this.opts.overrides?.keyrings;
let keyringName = null;
if (
deviceName !== HardwareDeviceNames.QR &&
!this.canUseHardwareWallets()
) {
throw new Error('Hardware wallets are not supported on this version.');
}
switch (deviceName) {
case HardwareDeviceNames.trezor:
keyringName = keyringOverrides?.trezor?.type || TrezorKeyring.type;
break;
case HardwareDeviceNames.ledger:
keyringName =
keyringOverrides?.ledger?.type || LedgerBridgeKeyring.type;
break;
case HardwareDeviceNames.qr:
keyringName = QRHardwareKeyring.type;
break;
case HardwareDeviceNames.lattice:
keyringName = keyringOverrides?.lattice?.type || LatticeKeyring.type;
break;
default:
throw new Error(
'MetamaskController:getKeyringForDevice - Unknown device',
);
}
let [keyring] = await this.keyringController.getKeyringsByType(keyringName);
if (!keyring) {
keyring = await this.keyringController.addNewKeyring(keyringName);
}
if (hdPath && keyring.setHdPath) {
keyring.setHdPath(hdPath);
}
if (deviceName === HardwareDeviceNames.lattice) {
keyring.appName = 'MetaMask';
}
if (deviceName === HardwareDeviceNames.trezor) {
const model = keyring.getModel();
this.appStateController.setTrezorModel(model);
}
keyring.network = this.networkController.store.getState().provider.type;
return keyring;
}
async attemptLedgerTransportCreation() {
const keyring = await this.getKeyringForDevice(HardwareDeviceNames.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<boolean>}
*/
async checkHardwareStatus(deviceName, hdPath) {
const keyring = await this.getKeyringForDevice(deviceName, hdPath);
return keyring.isUnlocked();
}
/**
* Clear
*
* @param deviceName
* @returns {Promise<boolean>}
*/
async forgetDevice(deviceName) {
const keyring = await this.getKeyringForDevice(deviceName);
keyring.forgetDevice();
return true;
}
/**
* Retrieves the keyring for the selected address and using the .type returns
* a subtype for the account. Either 'hardware', 'imported' or 'MetaMask'.
*
* @param {string} address - Address to retrieve keyring for
* @returns {'hardware' | 'imported' | 'MetaMask'}
*/
async getAccountType(address) {
const keyring = await this.keyringController.getKeyringForAccount(address);
switch (keyring.type) {
case HardwareKeyringTypes.trezor:
case HardwareKeyringTypes.lattice:
case HardwareKeyringTypes.qr:
case HardwareKeyringTypes.ledger:
return 'hardware';
case HardwareKeyringTypes.imported:
return 'imported';
default:
return 'MetaMask';
}
}
/**
* Retrieves the keyring for the selected address and using the .type
* determines if a more specific name for the device is available. Returns
* 'N/A' for non hardware wallets.
*
* @param {string} address - Address to retrieve keyring for
* @returns {'ledger' | 'lattice' | 'N/A' | string}
*/
async getDeviceModel(address) {
const keyring = await this.keyringController.getKeyringForAccount(address);
switch (keyring.type) {
case HardwareKeyringTypes.trezor:
return keyring.getModel();
case HardwareKeyringTypes.qr:
return keyring.getName();
case HardwareKeyringTypes.ledger:
// TODO: get model after ledger keyring exposes method
return HardwareDeviceNames.ledger;
case HardwareKeyringTypes.lattice:
// TODO: get model after lattice keyring exposes method
return HardwareDeviceNames.lattice;
default:
return 'N/A';
}
}
/**
* 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 === HardwareDeviceNames.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.
*
* @param accountCount
* @returns {} keyState
*/
async addNewAccount(accountCount) {
const [primaryKeyring] = this.keyringController.getKeyringsByType(
HardwareKeyringTypes.hdKeyTree,
);
if (!primaryKeyring) {
throw new Error('MetamaskController - No HD Key Tree found');
}
const { keyringController } = this;
const { identities: oldIdentities } =
this.preferencesController.store.getState();
if (Object.keys(oldIdentities).length === accountCount) {
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 };
}
return {
...keyringController.memStore.getState(),
identities: oldIdentities,
};
}
/**
* 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<number[]>} The seed phrase to be confirmed by the user,
* encoded as an array of UTF-8 bytes.
*/
async verifySeedPhrase() {
const [primaryKeyring] = this.keyringController.getKeyringsByType(
HardwareKeyringTypes.hdKeyTree,
);
if (!primaryKeyring) {
throw new Error('MetamaskController - No HD Key Tree found');
}
const serialized = await primaryKeyring.serialize();
const seedPhraseAsBuffer = Buffer.from(serialized.mnemonic);
const accounts = await primaryKeyring.getAccounts();
if (accounts.length < 1) {
throw new Error('MetamaskController - No accounts found');
}
try {
await seedPhraseVerifier.verifyAccounts(accounts, seedPhraseAsBuffer);
return Array.from(seedPhraseAsBuffer.values());
} 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<string>} 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.
* @param {boolean} [suppressUnauthorizedError] - Suppresses the unauthorized error.
* @returns {Promise<string[]>} The origin's permitted accounts, or an empty
* array.
*/
async getPermittedAccounts(
origin,
{ suppressUnauthorizedError = true } = {},
) {
try {
return await this.permissionController.executeRestrictedMethod(
origin,
RestrictedMethods.eth_accounts,
);
} catch (error) {
if (
suppressUnauthorizedError &&
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]);
const keyring = await this.keyringController.getKeyringForAccount(address);
// Remove account from the keyring
await this.keyringController.removeAccount(address);
const updatedKeyringAccounts = keyring ? await keyring.getAccounts() : {};
if (updatedKeyringAccounts?.length === 0) {
keyring.destroy?.();
}
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(
HardwareKeyringTypes.imported,
[privateKey],
);
const [firstAccount] = await keyring.getAccounts();
// update accounts in preferences controller
const allAccounts = await this.keyringController.getAccounts();
this.preferencesController.setAddresses(allAccounts);
// set new account as selected
this.preferencesController.setSelectedAddress(firstAccount);
}
// ---------------------------------------------------------------------------
// 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 { disabledRpcMethodPreferences } =
this.preferencesController.store.getState();
const { eth_sign } = disabledRpcMethodPreferences; // eslint-disable-line camelcase
const data = normalizeMsgData(msgParams.data);
let promise;
// eslint-disable-next-line camelcase
if (!eth_sign) {
throw ethErrors.rpc.methodNotFound(
'eth_sign has been disabled. You must enable it in the advanced settings',
);
}
// 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;
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
/**
* Gets an "app key" corresponding to an Ethereum address. An app key is more
* or less an addrdess hashed together with some string, in this case a
* subject identifier / origin.
*
* @todo Figure out a way to derive app keys that doesn't depend on the user's
* Ethereum addresses.
* @param {string} subject - The identifier of the subject whose app key to
* retrieve.
* @param {string} [requestedAccount] - The account whose app key to retrieve.
* The first account in the keyring will be used by default.
*/
async getAppKeyForSubject(subject, requestedAccount) {
let account;
if (requestedAccount) {
account = requestedAccount;
} else {
[account] = await this.keyringController.getAccounts();
}
return this.keyringController.exportAppKeyForAddress(account, subject);
}
///: END:ONLY_INCLUDE_IN
/**
* Signifies user intent to complete an eth_sign method.
*
* @param {object} msgParams - The params passed to eth_call.
* @returns {Promise<object>} 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<object>} 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<object>} 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<object>} 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 HardwareKeyringTypes.ledger: {
return new Promise((_, reject) => {
reject(
new Error('Ledger does not support eth_getEncryptionPublicKey.'),
);
});
}
case HardwareKeyringTypes.trezor: {
return new Promise((_, reject) => {
reject(
new Error('Trezor does not support eth_getEncryptionPublicKey.'),
);
});
}
case HardwareKeyringTypes.lattice: {
return new Promise((_, reject) => {
reject(
new Error('Lattice does not support eth_getEncryptionPublicKey.'),
);
});
}
case HardwareKeyringTypes.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<object>} 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
*/
async 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 options
* @returns {object} MetaMask state
*/
async createCancelTransaction(originalTxId, customGasSettings, options) {
await this.txController.createCancelTransaction(
originalTxId,
customGasSettings,
options,
);
const state = 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 options
* @returns {object} MetaMask state
*/
async createSpeedUpTransaction(originalTxId, customGasSettings, options) {
await this.txController.createSpeedUpTransaction(
originalTxId,
customGasSettings,
options,
);
const state = 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.
*/
/**
* A Snap sender object.
*
* @typedef {object} SnapSender
* @property {string} snapId - The ID of the snap.
*/
/**
* Used to create a multiplexed stream for connecting to an untrusted context
* like a Dapp or other extension.
*
* @param options - Options bag.
* @param {ReadableStream} options.connectionStream - The Duplex stream to connect to.
* @param {MessageSender | SnapSender} options.sender - The sender of the messages on this stream.
* @param {string} [options.subjectType] - The type of the sender, i.e. subject.
*/
setupUntrustedCommunication({ connectionStream, sender, subjectType }) {
const { usePhishDetect } = this.preferencesController.store.getState();
let _subjectType;
if (subjectType) {
_subjectType = subjectType;
} else if (sender.id && sender.id !== this.extension.runtime.id) {
_subjectType = SubjectType.Extension;
} else {
_subjectType = SubjectType.Website;
}
if (sender.url) {
const { hostname } = new URL(sender.url);
this.phishingController.maybeUpdateState();
// Check if new connection is blocked if phishing detection is on
const phishingTestResponse = this.phishingController.test(hostname);
if (usePhishDetect && phishingTestResponse?.result) {
this.sendPhishingWarning(connectionStream, hostname);
this.metaMetricsController.trackEvent({
event: EVENT_NAMES.PHISHING_PAGE_DISPLAYED,
category: EVENT.CATEGORIES.PHISHING,
properties: {
url: hostname,
},
});
return;
}
}
// setup multiplexing
const mux = setupMultiplex(connectionStream);
// messages between inpage and background
this.setupProviderConnection(
mux.createStream('metamask-provider'),
sender,
_subjectType,
);
// TODO:LegacyProvider: Delete
if (sender.url) {
// 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,
SubjectType.Internal,
);
}
/**
* Used to create a multiplexed stream for connecting to the phishing warning page.
*
* @param options - Options bag.
* @param {ReadableStream} options.connectionStream - The Duplex stream to connect to.
*/
setupPhishingCommunication({ connectionStream }) {
const { usePhishDetect } = this.preferencesController.store.getState();
if (!usePhishDetect) {
return;
}
// setup multiplexing
const mux = setupMultiplex(connectionStream);
const phishingStream = mux.createStream(PHISHING_SAFELIST);
// set up postStream transport
phishingStream.on(
'data',
createMetaRPCHandler(
{ safelistPhishingDomain: this.safelistPhishingDomain.bind(this) },
phishingStream,
),
);
}
/**
* 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,
this.store,
this.localStoreApiWrapper,
),
);
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);
const startUISync = () => {
if (outStream._writableState.ended) {
return;
}
// send notification to client-side
outStream.write({
jsonrpc: '2.0',
method: 'startUISync',
});
};
if (this.startUISync) {
startUISync();
} else {
this.once('startUISync', startUISync);
}
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 | SnapSender} sender - The sender of the messages on this stream
* @param {SubjectType} subjectType - The type of the sender, i.e. subject.
*/
setupProviderConnection(outStream, sender, subjectType) {
let origin;
if (subjectType === SubjectType.Internal) {
origin = ORIGIN_METAMASK;
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
else if (subjectType === SubjectType.Snap) {
origin = sender.snapId;
}
///: END:ONLY_INCLUDE_IN
else {
origin = new URL(sender.url).origin;
}
if (sender.id && sender.id !== this.extension.runtime.id) {
this.subjectMetadataController.addSubjectMetadata({
origin,
extensionId: sender.id,
subjectType: SubjectType.Extension,
});
}
let tabId;
if (sender.tab && sender.tab.id) {
tabId = sender.tab.id;
}
const engine = this.setupProviderEngine({
origin,
sender,
subjectType,
tabId,
});
// 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);
}
});
}
///: BEGIN:ONLY_INCLUDE_IN(flask)
/**
* For snaps running in workers.
*
* @param snapId
* @param connectionStream
*/
setupSnapProvider(snapId, connectionStream) {
this.setupUntrustedCommunication({
connectionStream,
sender: { snapId },
subjectType: SubjectType.Snap,
});
}
///: END:ONLY_INCLUDE_IN
/**
* 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 {MessageSender | SnapSender} options.sender - The sender object.
* @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, subjectType, sender, tabId }) {
// setup json rpc engine stack
const engine = new JsonRpcEngine();
const { blockTracker, provider } = 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),
);
if (isManifestV3) {
engine.push(createDupeReqFilterMiddleware());
}
// 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(this.permissionLogController.createMiddleware());
engine.push(
createRPCMethodTrackingMiddleware({
trackEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
getMetricsState: this.metaMetricsController.store.getState.bind(
this.metaMetricsController.store,
),
securityProviderRequest: this.securityProviderRequest.bind(this),
}),
);
// onboarding
if (subjectType === SubjectType.Website) {
engine.push(
createOnboardingMiddleware({
location: sender.url,
registerOnboarding: this.onboardingController.registerOnboarding,
}),
);
}
// Unrestricted/permissionless RPC method implementations
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 },
),
getCurrentChainId: () =>
this.networkController.store.getState().provider.chainId,
getCurrentRpcUrl: () =>
this.networkController.store.getState().provider.rpcUrl,
// network configuration-related
getNetworkConfigurations: () =>
this.networkController.store.getState().networkConfigurations,
upsertNetworkConfiguration:
this.networkController.upsertNetworkConfiguration.bind(
this.networkController,
),
setActiveNetwork: this.networkController.setActiveNetwork.bind(
this.networkController,
),
findNetworkConfigurationBy: this.findNetworkConfigurationBy.bind(this),
setProviderType: this.networkController.setProviderType.bind(
this.networkController,
),
// Web3 shim-related
getWeb3ShimUsageState: this.alertController.getWeb3ShimUsageState.bind(
this.alertController,
),
setWeb3ShimUsageRecorded:
this.alertController.setWeb3ShimUsageRecorded.bind(
this.alertController,
),
}),
);
///: BEGIN:ONLY_INCLUDE_IN(flask)
engine.push(
createSnapMethodMiddleware(subjectType === SubjectType.Snap, {
getAppKey: this.getAppKeyForSubject.bind(this, origin),
getUnlockPromise: this.appStateController.getUnlockPromise.bind(
this.appStateController,
),
getSnaps: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:getPermitted',
origin,
),
requestPermissions: async (requestedPermissions) => {
const [approvedPermissions] =
await this.permissionController.requestPermissions(
{ origin },
requestedPermissions,
);
return Object.values(approvedPermissions);
},
getPermissions: this.permissionController.getPermissions.bind(
this.permissionController,
origin,
),
getAccounts: this.getPermittedAccounts.bind(this, origin),
installSnaps: this.controllerMessenger.call.bind(
this.controllerMessenger,
'SnapController:install',
origin,
),
}),
);
///: END:ONLY_INCLUDE_IN
// filter and subscription polyfills
engine.push(filterMiddleware);
engine.push(subscriptionManager.middleware);
if (subjectType !== SubjectType.Internal) {
// permissions
engine.push(
this.permissionController.createPermissionMiddleware({
origin,
}),
);
}
engine.push(this.metamaskMiddleware);
// 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 === 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];
}
}
/**
* Closes all connections for the given origin, and removes the references
* to them.
* Ignores unknown origins.
*
* @param {string} origin - The origin string.
*/
removeAllConnections(origin) {
const connections = this.connections[origin];
if (!connections) {
return;
}
Object.keys(connections).forEach((id) => {
this.removeConnection(origin, id);
});
}
/**
* 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<void>}
* @private
*/
async _onKeyringControllerUpdate(state) {
const {
keyrings,
encryptionKey: loginToken,
encryptionSalt: loginSalt,
} = state;
const addresses = keyrings.reduce(
(acc, { accounts }) => acc.concat(accounts),
[],
);
if (isManifestV3) {
await browser.storage.session.set({ loginToken, loginSalt });
}
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),
},
};
});
this.unMarkPasswordForgotten();
// 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
//=============================================================================
getExternalPendingTransactions(address) {
return this.smartTransactionsController.getTransactions({
addressFrom: address,
status: 'pending',
});
}
/**
* Returns the nonce that will be associated with a transaction once approved
*
* @param {string} address - The hex string address for the transaction
* @returns {Promise<number>}
*/
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<number>}
*/
async getNextNonce(address) {
const nonceLock = await this.txController.nonceTracker.getNonceLock(
address,
);
nonceLock.releaseLock();
return nonceLock.nextNonce;
}
//=============================================================================
// CONFIG
//=============================================================================
/**
* Returns the first network configuration 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 network configurations list
*/
findNetworkConfigurationBy(rpcInfo) {
const { networkConfigurations } = this.networkController.store.getState();
const networkConfiguration = Object.values(networkConfigurations).find(
(configuration) => {
return Object.keys(rpcInfo).some((key) => {
return configuration[key] === rpcInfo[key];
});
},
);
return networkConfiguration || null;
}
/**
* Sets the Ledger Live preference to use for Ledger hardware wallet support
*
* @param {string} transportType - The Ledger transport type.
*/
async setLedgerTransportPreference(transportType) {
if (!this.canUseHardwareWallets()) {
return undefined;
}
const currentValue =
this.preferencesController.getLedgerTransportPreference();
const newValue =
this.preferencesController.setLedgerTransportPreference(transportType);
const keyring = await this.getKeyringForDevice(HardwareDeviceNames.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(
HardwareKeyringTypes.trezor,
);
if (trezorKeyring) {
trezorKeyring.dispose();
}
const [ledgerKeyring] = this.keyringController.getKeyringsByType(
HardwareKeyringTypes.ledger,
);
ledgerKeyring?.destroy?.();
if (isManifestV3) {
this.clearLoginArtifacts();
}
return this.keyringController.setLocked();
}
removePermissionsFor = (subjects) => {
try {
this.permissionController.revokePermissions(subjects);
} catch (exp) {
if (!(exp instanceof PermissionsRequestNotFoundError)) {
throw exp;
}
}
};
///: BEGIN:ONLY_INCLUDE_IN(flask)
updateCaveat = (origin, target, caveatType, caveatValue) => {
try {
this.controllerMessenger.call(
'PermissionController:updateCaveat',
origin,
target,
caveatType,
caveatValue,
);
} catch (exp) {
if (!(exp instanceof PermissionsRequestNotFoundError)) {
throw exp;
}
}
};
///: END:ONLY_INCLUDE_IN
rejectPermissionsRequest = (requestId) => {
try {
this.permissionController.rejectPermissionsRequest(requestId);
} catch (exp) {
if (!(exp instanceof PermissionsRequestNotFoundError)) {
throw exp;
}
}
};
acceptPermissionsRequest = (request) => {
try {
this.permissionController.acceptPermissionsRequest(request);
} catch (exp) {
if (!(exp instanceof PermissionsRequestNotFoundError)) {
throw exp;
}
}
};
resolvePendingApproval = (id, value) => {
try {
this.approvalController.accept(id, value);
} catch (exp) {
if (!(exp instanceof ApprovalRequestNotFoundError)) {
throw exp;
}
}
};
rejectPendingApproval = (id, error) => {
try {
this.approvalController.reject(
id,
new EthereumRpcError(error.code, error.message, error.data),
);
} catch (exp) {
if (!(exp instanceof ApprovalRequestNotFoundError)) {
throw exp;
}
}
};
async securityProviderRequest(requestData, methodName) {
const { currentLocale, transactionSecurityCheckEnabled } =
this.preferencesController.store.getState();
if (transactionSecurityCheckEnabled) {
const chainId = Number(
hexToDecimal(this.networkController.store.getState().provider.chainId),
);
try {
const securityProviderResponse = await securityProviderCheck(
requestData,
methodName,
chainId,
currentLocale,
);
return securityProviderResponse;
} catch (err) {
log.error(err.message);
throw err;
}
}
return null;
}
}